2019-03-15 13:19:19 +01:00
//
// T a b l e V i e w . s w i f t
// C a p F i n d e r
//
// C r e a t e d b y U s e r o n 2 2 . 0 4 . 1 8 .
// C o p y r i g h t © 2 0 1 8 U s e r . A l l r i g h t s r e s e r v e d .
//
import UIKit
2020-05-16 11:21:55 +02:00
import JGProgressHUD
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
class TableView : UITableViewController {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var classifier : Classifier ?
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var accessory : SearchAndDisplayAccessory ?
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var titleLabel : UILabel !
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var subtitleLabel : UILabel !
2019-03-15 13:19:19 +01:00
private var caps = [ Cap ] ( )
private var shownCaps = [ Cap ] ( )
2020-05-16 11:21:55 +02:00
private var matches : [ Int : Float ] ?
2019-03-15 13:19:19 +01:00
private var sortType : SortCriteria = . id
2020-05-16 11:21:55 +02:00
private var searchText : String ? = nil
2019-03-15 13:19:19 +01:00
private var sortAscending : Bool = false
// / T h i s w i l l b e s e t t o a c a p i d w h e n a d d i n g a c a p t o i t
2020-05-16 11:21:55 +02:00
private var capToAddImageTo : Int ?
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// MARK: C o m p u t e d p r o p e r t i e s
private var titleText : String {
let recognized = app . database . recognizedCapCount
let all = app . database . capCount
switch all {
case 0 :
return " No caps "
case 1 :
return " 1 cap "
case recognized :
return " \( all ) caps "
default :
return " \( all ) caps ( \( all - recognized ) new) "
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private var subtitleText : String {
let capCount = app . database . capCount
guard capCount > 0 else {
return " "
}
let allImages = app . database . imageCount
let ratio = Float ( allImages ) / Float ( capCount )
return String ( format : " %d images (%.2f per cap) " , allImages , ratio )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: O v e r r i d e s
override var inputAccessoryView : UIView ? {
get { return accessory }
}
override var canBecomeFirstResponder : Bool {
return true
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - A c t i o n s
@IBAction func updateInfo ( _ sender : UIBarButtonItem ) {
downloadNewestClassifierIfNeeded ( )
downloadImageCounts ( )
checkIfCapImagesNeedDownload ( )
}
@IBAction func showMosaic ( _ sender : UIBarButtonItem ) {
let vc = app . mainStoryboard . instantiateViewController ( withIdentifier : " GridView " ) as ! GridViewController
guard let nav = navigationController as ? NavigationController else {
return
}
nav . pushViewController ( vc , animated : true )
nav . allowLandscape = true
}
func showCameraView ( ) {
let storyboard = UIStoryboard ( name : " Main " , bundle : nil )
let controller = storyboard . instantiateViewController ( withIdentifier : " NewImageController " ) as ! CameraController
controller . delegate = self
self . present ( controller , animated : true )
}
@objc private func titleWasTapped ( ) {
let storyboard = UIStoryboard ( name : " Main " , bundle : nil )
let controller = storyboard . instantiateViewController ( withIdentifier : " SortController " ) as ! SortController
controller . selected = sortType
controller . ascending = sortAscending
controller . delegate = self
let presentationController = AlwaysPresentAsPopover . configurePresentation ( forController : controller )
presentationController . sourceView = navigationItem . titleView !
presentationController . permittedArrowDirections = [ . up ]
self . present ( controller , animated : true )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - L i f e c y c l e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
app . database . add ( listener : self )
tableView . rowHeight = 100
accessory = SearchAndDisplayAccessory ( width : self . view . frame . width )
accessory ? . delegate = self
reloadCapsFromDatabase ( )
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
( navigationController as ? NavigationController ) ? . allowLandscape = false
}
override func viewDidAppear ( _ animated : Bool ) {
super . viewDidAppear ( animated )
checkDatabaseIsDownloaded ( )
checkClassifierIsDownloaded ( )
}
override func didMove ( toParent parent : UIViewController ? ) {
super . didMove ( toParent : parent )
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
guard parent != nil && self . navigationItem . titleView = = nil else {
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
initNavigationItemTitleView ( )
}
private func initNavigationItemTitleView ( ) {
self . titleLabel = UILabel ( )
titleLabel . text = titleText
titleLabel . font = . preferredFont ( forTextStyle : . headline )
titleLabel . textColor = . label
self . subtitleLabel = UILabel ( )
subtitleLabel . text = subtitleText
subtitleLabel . font = . preferredFont ( forTextStyle : . footnote )
subtitleLabel . textColor = . secondaryLabel
let stackView = UIStackView ( arrangedSubviews : [ titleLabel , subtitleLabel ] )
stackView . distribution = . equalCentering
stackView . alignment = . center
stackView . axis = . vertical
self . navigationItem . titleView = stackView
let recognizer = UITapGestureRecognizer ( target : self , action : #selector ( titleWasTapped ) )
stackView . isUserInteractionEnabled = true
stackView . addGestureRecognizer ( recognizer )
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func updateNavigationItemTitleView ( ) {
DispatchQueue . main . async {
self . titleLabel . text = self . titleText
self . subtitleLabel . text = self . subtitleText
}
}
// MARK: S t a r t i n g u p d a t e s
private func downloadImageCounts ( ) {
app . database . downloadImageCounts ( )
}
private func downloadNewestClassifierIfNeeded ( ) {
app . database . hasNewClassifier { version , size in
guard let version = version else {
2019-03-15 13:19:19 +01:00
return
}
2020-05-16 11:21:55 +02:00
DispatchQueue . main . async {
self . askUserToDownload ( classifier : version , size : size )
}
}
}
private func rename ( cap : Cap , at indexPath : IndexPath ) {
let detail = " Choose a new name for the cap "
askUserForText ( " Enter new name " , detail : detail , existingText : cap . name , yesText : " Save " ) { text in
guard app . database . update ( name : text , for : cap . id ) else {
return
}
guard let newCap = app . database . cap ( for : cap . id ) else {
return
}
self . shownCaps [ indexPath . row ] = newCap
self . tableView . reloadRows ( at : [ indexPath ] , with : . none )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
private func saveNewCap ( for image : UIImage ) {
let detail = " Choose a name for the image "
askUserForText ( " Enter name " , detail : detail , existingText : accessory ! . searchBar . text , yesText : " Save " ) { text in
guard app . database . createCap ( image : image , name : text ) else {
self . showAlert ( " Cap not added " )
return
}
self . accessory ! . discardImage ( )
}
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// MARK: U s e r i n t e r a c t i o n
private func checkClassifierIsDownloaded ( ) {
guard let model = app . storage . recognitionModel else {
downloadNewestClassifierIfNeeded ( )
return
}
classifier = Classifier ( model : model )
}
private func checkDatabaseIsDownloaded ( ) {
guard app . needsDownload else {
return
}
log ( " Server database not available, getting database size " )
app . database . getServerDatabaseSize { size in
DispatchQueue . main . async {
self . askUserToDownloadServerDatabase ( size : size )
}
}
}
private func askUserToDownloadServerDatabase ( size : Int64 ? ) {
let detail = " The server database needs to be downloaded for the app to function properly. Would you like to download it now? "
let sizeText = size != nil ? " ( \( ByteCountFormatter . string ( fromByteCount : size ! , countStyle : . file ) ) ) " : " "
presentUserBinaryChoice ( " Download server database " , detail : detail + sizeText , yesText : " Download " , noText : " Later " ) {
self . downloadServerDatabase ( )
}
}
private func checkIfCapImagesNeedDownload ( ) {
let count = app . database . capsWithoutImages
guard count > 0 else {
log ( " No cap images to download " )
return
}
DispatchQueue . main . async {
self . askUserToDownload ( capImages : count )
}
}
private func askUserToDownload ( capImages : Int ) {
let detail = " \( capImages ) caps have no image. Would you like to download them now? ( \( ByteCountFormatter . string ( fromByteCount : Int64 ( capImages * 10000 ) , countStyle : . file ) ) ) "
presentUserBinaryChoice ( " New classifier " , detail : detail , yesText : " Download " , noText : " Later " ) {
self . downloadAllCapImages ( )
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func askUserToDownload ( classifier version : Int , size : Int64 ? ) {
let detail = " Version \( version ) of the classifier is available for download (You have version \( app . database . classifierVersion ) ). Would you like to download it now? "
let sizeText = size != nil ? " ( \( ByteCountFormatter . string ( fromByteCount : size ! , countStyle : . file ) ) ) " : " "
presentUserBinaryChoice ( " New classifier " , detail : detail + sizeText , yesText : " Download " ) {
self . downloadClassifier ( )
}
}
private func askUserForText ( _ title : String , detail : String , existingText : String ? = nil , _ placeholder : String ? = " Cap name " , yesText : String , noText : String = " Cancel " , confirmed : @ escaping ( _ text : String ) -> Void ) {
DispatchQueue . main . sync {
let alertController = UIAlertController (
title : title ,
message : detail ,
preferredStyle : . alert )
alertController . addTextField { textField in
textField . placeholder = placeholder
textField . keyboardType = . default
textField . text = existingText
}
let action = UIAlertAction ( title : yesText , style : . default ) { _ in
guard let name = alertController . textFields ? . first ? . text else {
2019-03-15 13:19:19 +01:00
return
}
2020-05-16 11:21:55 +02:00
confirmed ( name )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
let cancel = UIAlertAction ( title : noText , style : . cancel )
alertController . addAction ( action )
alertController . addAction ( cancel )
self . present ( alertController , animated : true )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
private func presentUserBinaryChoice ( _ title : String , detail : String , yesText : String , noText : String = " Cancel " , confirmed : @ escaping ( ) -> Void ) {
let alert = UIAlertController ( title : title , message : detail , preferredStyle : . alert )
let confirm = UIAlertAction ( title : yesText , style : . default ) { _ in
confirmed ( )
}
let cancel = UIAlertAction ( title : noText , style : . cancel )
alert . addAction ( confirm )
alert . addAction ( cancel )
self . present ( alert , animated : true )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: S t a r t i n g d o w n l o a d s
private func downloadClassifier ( ) {
let hud = JGProgressHUD ( style : . dark )
hud . vibrancyEnabled = true
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete "
hud . textLabel . text = " Downloading classifier "
hud . show ( in : self . view )
app . database . downloadClassifier ( progress : { progress , received , total in
DispatchQueue . main . async {
hud . progress = progress
let t = ByteCountFormatter . string ( fromByteCount : total , countStyle : . file )
let r = ByteCountFormatter . string ( fromByteCount : received , countStyle : . file )
hud . detailTextLabel . text = String ( format : " %.0f " , progress * 100 ) + " % complete ( \( r ) / \( t ) ) "
}
} ) { success in
DispatchQueue . main . async {
hud . dismiss ( )
self . didDownloadClassifier ( successfully : success )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func downloadServerDatabase ( ) {
let hud = JGProgressHUD ( style : . dark )
hud . vibrancyEnabled = true
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete "
hud . textLabel . text = " Downloading server database "
hud . show ( in : self . view )
app . database . downloadServerDatabase ( progress : { progress , received , total in
DispatchQueue . main . async {
hud . progress = progress
let t = ByteCountFormatter . string ( fromByteCount : total , countStyle : . file )
let r = ByteCountFormatter . string ( fromByteCount : received , countStyle : . file )
hud . detailTextLabel . text = String ( format : " %.0f " , progress ) + " % complete ( \( r ) / \( t ) ) "
}
} , completion : { success in
guard success else {
self . log ( " Failed to download server database " )
hud . detailTextLabel . text = " Download failed "
hud . dismiss ( afterDelay : 2.0 )
return
}
DispatchQueue . main . async {
hud . textLabel . text = " Processing data "
hud . progress = 0.2
hud . detailTextLabel . text = " Please wait... "
}
app . needsDownload = false
} ) {
DispatchQueue . main . async {
hud . dismiss ( )
self . checkIfCapImagesNeedDownload ( )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func downloadAllCapImages ( ) {
let hud = JGProgressHUD ( style : . dark )
hud . vibrancyEnabled = true
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete "
hud . textLabel . text = " Downloading cap images "
hud . show ( in : self . view )
app . database . downloadMainCapImages { ( done , total ) in
let progress = Float ( done ) / Float ( total )
let percent = Int ( ( progress * 100 ) . rounded ( ) )
hud . detailTextLabel . text = " \( percent ) % complete ( \( done ) / \( total ) ) "
hud . progress = progress
if done >= total {
hud . dismiss ( afterDelay : 1.0 )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: C l a s s i f i c a t i o n
// / T h e s i m i l a r i t y o f t h e c a p t o t h e c u r r e n t l y p r o c e s s e d i m a g e
private func match ( for cap : Int ) -> Float ? {
matches ? [ cap ]
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func clearClassifierMatches ( ) {
matches = nil
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func classify ( image : UIImage ) {
guard let classifier = self . classifier else {
return
}
DispatchQueue . global ( qos : . userInitiated ) . async {
self . log ( " Classification starting... " )
classifier . recognize ( image : image ) { matches in
guard let matches = matches else {
self . log ( " Failed to classify image " )
self . matches = nil
return
}
self . log ( " Classification finished " )
self . matches = matches
self . sortType = . match
self . sortAscending = false
self . showAllCapsAndScrollToTop ( )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: F i n i s h i n g d o w n l o a d s
private func didDownloadClassifier ( successfully success : Bool ) {
guard success else {
self . log ( " Failed to download classifier " )
return
}
self . log ( " Classifier was downloaded. " )
guard let image = accessory ! . currentImage else {
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
classify ( image : image )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - S h o w i n g c a p s
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func reloadCapsFromDatabase ( ) {
caps = app . database ? . caps ? ? [ ]
showCaps ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
/* *
Match all cap names against the given string and return matches .
- note : Each space - separated part of the string is matched individually
*/
private func showCaps ( matching text : String ? = nil ) {
DispatchQueue . global ( qos : . userInteractive ) . async {
self . searchText = text
guard let t = text else {
self . show ( caps : self . caps )
return
}
let found = self . filter ( caps : self . caps , matching : t )
self . show ( caps : found )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
private func show ( caps : [ Cap ] ) {
show ( sortedCaps : sorted ( caps : caps ) )
}
private func show ( sortedCaps caps : [ Cap ] ) {
shownCaps = caps
DispatchQueue . main . async {
self . tableView . reloadData ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
}
private func filter ( caps : [ Cap ] , matching text : String ) -> [ Cap ] {
let textParts = text . components ( separatedBy : " " ) . filter { $0 != " " }
return caps . compactMap { cap -> Cap ? in
// F o r e a c h p a r t o f t e x t , c h e c k i f n a m e c o n t a i n s i t
for textItem in textParts {
if ! cap . cleanName . contains ( textItem ) {
return nil
}
}
return cap
}
}
private func sorted ( caps : [ Cap ] ) -> [ Cap ] {
if sortAscending {
switch sortType {
case . id : return caps . sorted { $0 . id < $1 . id }
case . count : return caps . sorted { $0 . count < $1 . count }
case . name : return caps . sorted { $0 . name < $1 . name }
case . match : return caps . sorted { match ( for : $0 . id ) ? ? 0 < match ( for : $1 . id ) ? ? 0 }
}
} else {
switch sortType {
case . id : return caps . sorted { $0 . id > $1 . id }
case . count : return caps . sorted { $0 . count > $1 . count }
case . name : return caps . sorted { $0 . name > $1 . name }
case . match : return caps . sorted { match ( for : $0 . id ) ? ? 0 > match ( for : $1 . id ) ? ? 0 }
2019-03-15 13:19:19 +01:00
}
}
}
2020-05-16 11:21:55 +02:00
// / R e s e t s t h e c a p l i s t t o i t s o r i g i n a l s t a t e , d i s c a r d i n g a n y p r e v i o u s s o r t i n g .
private func showAllCapsByDescendingId ( ) {
sortType = . id
sortAscending = false
showAllCapsAndScrollToTop ( )
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// / D i s p l a y a l l c a p s i n t h e t a b l e , a n d s c r o l l s t o t h e t o p
private func showAllCapsAndScrollToTop ( ) {
showCaps ( )
tableViewScrollToTop ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - T a b l e V i e w
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
/* *
Scroll the table view to the top
*/
private func tableViewScrollToTop ( ) {
guard caps . count > 0 else { return }
let path = IndexPath ( row : 0 , section : 0 )
DispatchQueue . main . async {
self . tableView . scrollToRow ( at : path , at : . top , animated : true )
2019-03-15 13:19:19 +01:00
}
}
}
// MARK: - S o r t C o n t r o l l e r D e l e g a t e
extension TableView : SortControllerDelegate {
func sortController ( didSelect sortType : SortCriteria , ascending : Bool ) {
self . sortType = sortType
self . sortAscending = ascending
if sortType != . match {
2020-05-16 11:21:55 +02:00
clearClassifierMatches ( )
2019-03-15 13:19:19 +01:00
}
showAllCapsAndScrollToTop ( )
}
2020-05-16 11:21:55 +02:00
var sortControllerShouldIncludeMatchOption : Bool {
matches != nil
}
2019-03-15 13:19:19 +01:00
}
// MARK: - C a m e r a C o n t r o l l e r D e l e g a t e
extension TableView : CameraControllerDelegate {
func didCapture ( image : UIImage ) {
2020-05-16 11:21:55 +02:00
guard let cap = capToAddImageTo else {
accessory ! . showImageView ( with : image )
classify ( image : image )
return
}
guard app . database . add ( image : image , for : cap ) else {
self . error ( " Could not save image " )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
log ( " Added image for cap \( cap ) " )
self . capToAddImageTo = nil
2019-03-15 13:19:19 +01:00
}
func didCancel ( ) {
capToAddImageTo = nil
}
}
// MARK: - U I T a b l e V i e w D a t a S o u r c e
2020-05-16 11:21:55 +02:00
extension TableView {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
2019-03-15 13:19:19 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : " cap " ) as ! CapCell
let cap = shownCaps [ indexPath . row ]
2020-05-16 11:21:55 +02:00
cell . set ( cap : cap , match : match ( for : cap . id ) )
2019-03-15 13:19:19 +01:00
return cell
}
2020-05-16 11:21:55 +02:00
override func numberOfSections ( in tableView : UITableView ) -> Int {
2019-03-15 13:19:19 +01:00
return 1
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
2019-03-15 13:19:19 +01:00
return shownCaps . count
}
}
// MARK: - U I T a b l e V i e w D e l e g a t e
2020-05-16 11:21:55 +02:00
extension TableView {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func takeImage ( for cap : Int ) {
2019-03-15 13:19:19 +01:00
self . capToAddImageTo = cap
2020-05-16 11:21:55 +02:00
showCameraView ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
defer {
tableView . deselectRow ( at : indexPath , animated : true )
}
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
2020-05-16 11:21:55 +02:00
guard let image = accessory ? . capImage . image else {
2019-03-15 13:19:19 +01:00
self . giveFeedback ( . medium )
2020-05-16 11:21:55 +02:00
takeImage ( for : cap . id )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
guard app . database . add ( image : image , for : cap . id ) else {
self . giveFeedback ( . heavy )
self . error ( " Could not save image " )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . giveFeedback ( . medium )
// D e l e g a t e c a l l w i l l u p d a t e c e l l
self . accessory ? . discardImage ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
2019-03-15 13:19:19 +01:00
private func giveFeedback ( _ style : UIImpactFeedbackGenerator . FeedbackStyle ) {
let generator = UIImpactFeedbackGenerator ( style : style )
generator . impactOccurred ( )
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
let rename = UIContextualAction ( style : . normal , title : " Rename \n cap " ) { ( _ , _ , success ) in
success ( true )
self . rename ( cap : cap , at : indexPath )
self . giveFeedback ( . medium )
}
rename . backgroundColor = . blue
let image = UIContextualAction ( style : . normal , title : " Change \n image " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
let storyboard = UIStoryboard ( name : " Main " , bundle : nil )
let controller = storyboard . instantiateViewController ( withIdentifier : " ImageSelector " ) as ! ImageSelector
controller . cap = cap
self . navigationController ? . pushViewController ( controller , animated : true )
success ( true )
}
image . backgroundColor = . red
return UISwipeActionsConfiguration ( actions : [ rename , image ] )
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , leadingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
let count = UIContextualAction ( style : . normal , title : " Update \n count " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
2020-05-16 11:21:55 +02:00
success ( true )
DispatchQueue . global ( qos : . userInitiated ) . async {
app . database . download . imageCount ( for : cap . id ) { count in
guard let count = count else {
return
}
guard app . database . update ( count : count , for : cap . id ) else {
return
}
// D e l e g a t e c a l l w i l l u p d a t e t h e c e l l
}
2019-03-15 13:19:19 +01:00
}
}
count . backgroundColor = . orange
let similar = UIContextualAction ( style : . normal , title : " Similar \n caps " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
2020-05-16 11:21:55 +02:00
self . accessory ? . hideImageView ( )
guard let image = cap . image else {
success ( false )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . classify ( image : image )
2019-03-15 13:19:19 +01:00
success ( true )
}
similar . backgroundColor = . blue
return UISwipeActionsConfiguration ( actions : [ similar , count ] )
}
}
2020-05-16 11:21:55 +02:00
// MARK: - L o g g i n g
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : Logger { }
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// MARK: - P r o t o c o l D a t a b a s e D e l e g a t e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : DatabaseDelegate {
func database ( didChangeCap id : Int ) {
updateNavigationItemTitleView ( )
guard let cap = app . database . cap ( for : id ) else {
return
}
if let index = caps . firstIndex ( where : { $0 . id = = id } ) {
caps [ index ] = cap
}
if let index = shownCaps . firstIndex ( where : { $0 . id = = id } ) {
shownCaps [ index ] = cap
}
let match = self . match ( for : id )
DispatchQueue . main . async {
if let cell = self . tableView . visibleCells . first ( where : { ( $0 as ! CapCell ) . id = = id } ) {
( cell as ! CapCell ) . set ( cap : cap , match : match )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
func database ( didAddCap cap : Cap ) {
caps . append ( cap )
updateNavigationItemTitleView ( )
guard let text = searchText else {
// A l l c a p s a r e s h o w n
let newList = sorted ( caps : caps )
updateShownCaps ( newList , insertedId : cap . id )
return
}
guard filter ( caps : [ cap ] , matching : text ) != [ ] else {
// C a p i s n o t s h o w n , s o d o n ' t r e l o a d
return
}
let newList = sorted ( caps : filter ( caps : caps , matching : text ) )
updateShownCaps ( newList , insertedId : cap . id )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func updateShownCaps ( _ newList : [ Cap ] , insertedId id : Int ) {
guard shownCaps . count = = newList . count - 1 else {
log ( " Cap list refresh mismatch: was \( shownCaps . count ) , is \( newList . count ) " )
show ( sortedCaps : newList )
2019-03-15 13:19:19 +01:00
return
}
2020-05-16 11:21:55 +02:00
guard let index = newList . firstIndex ( where : { $0 . id = = id } ) else {
log ( " Cap list refresh without new cap \( id ) " )
show ( sortedCaps : newList )
return
}
DispatchQueue . main . async {
self . shownCaps = newList
self . tableView . beginUpdates ( )
let indexPath = IndexPath ( row : index , section : 0 )
self . tableView . insertRows ( at : [ indexPath ] , with : . automatic )
self . tableView . endUpdates ( )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
func databaseRequiresFullRefresh ( ) {
updateNavigationItemTitleView ( )
reloadCapsFromDatabase ( )
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - P r o t o c o l C a p S e a r c h D e l e g a t e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : CapAccessoryDelegate {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
func capSearchWasDismissed ( ) {
showAllCapsAndScrollToTop ( )
}
func capSearch ( didChange text : String ) {
let cleaned = text . clean
guard cleaned != " " else {
self . showCaps ( matching : nil )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . showCaps ( matching : cleaned )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
func capAccessoryDidDiscardImage ( ) {
matches = nil
2019-03-15 13:19:19 +01:00
showAllCapsByDescendingId ( )
}
2020-05-16 11:21:55 +02:00
func capAccessory ( shouldSave image : UIImage ) {
saveNewCap ( for : image )
}
func capAccessoryCameraButtonPressed ( ) {
showCameraView ( )
}
2019-03-15 13:19:19 +01:00
}