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
2021-01-13 21:43:46 +01:00
enum NavigationBarDataType {
case appInfo
case upload
case thumbnails
}
protocol NavigationBarDataSource {
var title : String { get }
var subtitle : String { get }
var id : NavigationBarDataType { get }
}
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-08-09 21:04:30 +02:00
@IBOutlet weak var infoButton : UIBarButtonItem !
2021-01-13 21:43:46 +01:00
private lazy var classifier : Classifier ? = loadClassifier ( )
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-06-18 22:55:51 +02:00
private var isUnlocked = false
2021-01-13 21:43:46 +01:00
var imageProvider : ImageProvider {
app . database . storage
}
2021-01-10 16:11:31 +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
2020-06-18 22:55:51 +02:00
guard capCount > 0 , isUnlocked else {
2020-05-16 11:21:55 +02:00
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
2020-08-09 21:04:30 +02:00
@IBAction func updateInfo ( _ sender : UIBarButtonItem , forEvent event : UIEvent ) {
2021-01-13 21:43:46 +01:00
guard let touch = event . allTouches ? . first , touch . tapCount > 0 else {
return
}
guard ! app . database . isInOfflineMode else {
2020-08-09 21:04:30 +02:00
showOfflineDialog ( )
return
}
2021-01-13 21:43:46 +01:00
app . database . startInitialDownload ( )
2020-05-16 11:21:55 +02:00
}
@IBAction func showMosaic ( _ sender : UIBarButtonItem ) {
2020-06-18 22:55:51 +02:00
checkThumbnailsAndColorsBeforShowingGrid ( )
2020-05-16 11:21:55 +02:00
}
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
2020-06-18 22:55:51 +02:00
controller . options = [ . id , . name ]
if isUnlocked { controller . options . append ( . count ) }
if matches != nil { controller . options . append ( . match ) }
2020-05-16 11:21:55 +02:00
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
tableView . rowHeight = 100
accessory = SearchAndDisplayAccessory ( width : self . view . frame . width )
accessory ? . delegate = self
2020-08-09 21:04:30 +02:00
initInfoButton ( )
2020-06-18 22:55:51 +02:00
app . database . delegate = self
let count = app . database . capCount
if count = = 0 {
log ( " No caps found, downloading names " )
2021-01-13 21:43:46 +01:00
app . database . startInitialDownload ( )
2020-06-18 22:55:51 +02:00
} else {
log ( " Loaded \( count ) caps " )
reloadCapsFromDatabase ( )
}
2020-05-16 11:21:55 +02:00
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
( navigationController as ? NavigationController ) ? . allowLandscape = false
2020-06-18 22:55:51 +02:00
isUnlocked = app . isUnlocked
log ( isUnlocked ? " App is unlocked " : " App is locked " )
2021-01-13 21:43:46 +01:00
app . database . startBackgroundWork ( )
2020-05-16 11:21:55 +02:00
}
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 ( )
}
2020-08-09 21:04:30 +02:00
private func initInfoButton ( ) {
let offline = app . database . isInOfflineMode
setInfoButtonIcon ( offline : offline )
}
private func setInfoButtonIcon ( offline : Bool ) {
let symbol = offline ? " icloud.slash " : " arrow.clockwise.icloud "
infoButton . image = UIImage ( systemName : symbol )
}
2020-05-16 11:21:55 +02:00
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 )
2020-06-18 22:55:51 +02:00
let longPress = UILongPressGestureRecognizer ( target : self , action : #selector ( attemptChangeOfUserPermissions ) )
stackView . addGestureRecognizer ( longPress )
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
private func set ( title : String , subtitle : String ) {
DispatchQueue . main . async {
self . titleLabel ? . text = title
self . subtitleLabel ? . text = subtitle
}
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func updateNavigationItemTitleView ( ) {
DispatchQueue . main . async {
2020-09-20 13:28:22 +02:00
self . titleLabel ? . text = self . titleText
self . subtitleLabel ? . text = self . subtitleText
2020-05-16 11:21:55 +02:00
}
}
// MARK: S t a r t i n g u p d a t e s
2020-06-18 22:55:51 +02:00
private func checkThumbnailsAndColorsBeforShowingGrid ( ) {
2021-01-13 21:43:46 +01:00
let colors = app . database . pendingCapsForColorCreation
let thumbs = app . database . pendingCapForThumbnailCreation
guard colors = = 0 && thumbs = = 0 else {
app . database . startBackgroundWork ( )
showAlert ( " Please wait until all background work is completed. \( colors ) colors and \( thumbs ) thumbnails need to be created. " , title : " Mosaic not ready " )
2020-06-18 22:55:51 +02:00
return
}
2021-01-13 21:43:46 +01:00
showGrid ( )
2020-06-18 22:55:51 +02:00
}
private func showGrid ( ) {
let vc = app . mainStoryboard . instantiateViewController ( withIdentifier : " GridView " ) as ! GridViewController
guard let nav = navigationController as ? NavigationController else {
return
}
if let tileImage = app . database . tileImage ( named : " default " ) {
log ( " Showing existing tile image " )
vc . load ( tileImage : tileImage )
} else {
let tileImage = Database . TileImage ( name : " default " , width : 40 , caps : [ ] )
log ( " Showing default tile image " )
vc . load ( tileImage : tileImage )
}
nav . pushViewController ( vc , animated : true )
nav . allowLandscape = true
}
2020-08-09 21:04:30 +02:00
private func showOfflineDialog ( ) {
let offline = app . database . isInOfflineMode
if offline {
2020-08-19 19:25:17 +02:00
print ( " Marking as online " )
app . database . isInOfflineMode = false
2021-01-13 21:43:46 +01:00
app . database . startBackgroundWork ( )
2020-08-19 19:25:17 +02:00
self . showAlert ( " Offline mode was disabled " , title : " Online " )
} else {
print ( " Marking as offline " )
app . database . isInOfflineMode = true
self . showAlert ( " Offline mode was enabled " , title : " Offline " )
2020-08-09 21:04:30 +02:00
}
}
2020-05-16 11:21:55 +02:00
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 {
2020-06-18 22:55:51 +02:00
self . showAlert ( " Name could not be set. " , title : " Update failed " )
2020-05-16 11:21:55 +02:00
return
}
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
2020-06-18 22:55:51 +02:00
DispatchQueue . global ( qos : . userInitiated ) . async {
guard app . database . createCap ( image : image , name : text ) else {
self . showAlert ( " Cap not added " , title : " Database error " )
return
}
self . accessory ! . discardImage ( )
2020-05-16 11:21:55 +02:00
}
}
}
2019-03-15 13:19:19 +01:00
2020-06-18 22:55:51 +02:00
private func updateShownCaps ( _ newList : [ Cap ] , insertedId id : Int ) {
// M a i n q u e u e
guard shownCaps . count = = newList . count - 1 else {
log ( " Cap list refresh mismatch: was \( shownCaps . count ) , is \( newList . count ) " )
show ( sortedCaps : newList )
return
}
guard let index = newList . firstIndex ( where : { $0 . id = = id } ) else {
log ( " Cap list refresh without new cap \( id ) " )
show ( sortedCaps : newList )
return
}
self . tableView . beginUpdates ( )
self . shownCaps = newList
let indexPath = IndexPath ( row : index , section : 0 )
self . tableView . insertRows ( at : [ indexPath ] , with : . automatic )
self . tableView . endUpdates ( )
}
2020-05-16 11:21:55 +02:00
// MARK: U s e r i n t e r a c t i o n
2020-06-18 22:55:51 +02:00
@objc private func attemptChangeOfUserPermissions ( ) {
guard isUnlocked else {
attemptAppUnlock ( )
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
log ( " Locking app. " )
app . lock ( )
isUnlocked = false
showAllCapsAndScrollToTop ( )
updateNavigationItemTitleView ( )
showAlert ( " The app was locked to prevent modifications. " , title : " Locked " )
}
private func attemptAppUnlock ( ) {
log ( " Presenting unlock dialog to user " )
askUserForText ( " Enter pin " , detail : " Enter the correct pin to unlock write permissions for the app. " , placeholder : " Pin " , yesText : " Unlock " ) { text in
guard let pin = Int ( text ) , app . checkUnlock ( with : pin ) else {
self . unlockFailed ( )
return
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
self . unlockDidSucceed ( )
2020-05-16 11:21:55 +02:00
}
}
2020-06-18 22:55:51 +02:00
private func unlockFailed ( ) {
showAlert ( " The pin you entered is incorrect. " , title : " Invalid pin " )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
private func unlockDidSucceed ( ) {
showAlert ( " The app was successfully unlocked. " , title : " Unlocked " )
isUnlocked = true
showAllCapsAndScrollToTop ( )
updateNavigationItemTitleView ( )
}
2021-01-13 21:43:46 +01:00
private func loadClassifier ( ) -> Classifier ? {
guard let model = app . database . storage . recognitionModel else {
return nil
2020-06-18 22:55:51 +02:00
}
2021-01-13 21:43:46 +01:00
return Classifier ( model : model )
2020-06-18 22:55:51 +02:00
}
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 . async {
2020-05-16 11:21:55 +02:00
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
2020-06-18 22:55:51 +02:00
private func presentUserBinaryChoice ( _ title : String , detail : String , yesText : String , noText : String = " Cancel " , dismissed : ( ( ) -> Void ) ? = nil , confirmed : @ escaping ( ) -> Void ) {
2020-05-16 11:21:55 +02:00
let alert = UIAlertController ( title : title , message : detail , preferredStyle : . alert )
let confirm = UIAlertAction ( title : yesText , style : . default ) { _ in
confirmed ( )
}
2020-06-18 22:55:51 +02:00
let cancel = UIAlertAction ( title : noText , style : . cancel ) { _ in
dismissed ? ( )
}
2020-05-16 11:21:55 +02:00
alert . addAction ( confirm )
alert . addAction ( cancel )
2021-01-13 21:43:46 +01:00
DispatchQueue . main . async {
self . present ( alert , animated : true )
2020-05-16 11:21:55 +02:00
}
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 ( )
2020-06-18 22:55:51 +02:00
DispatchQueue . global ( qos : . background ) . async {
app . database . update ( recognizedCaps : Set ( matches . keys ) )
}
}
}
}
private func classifyDummyImage ( ) {
guard let classifier = self . classifier else {
return
}
DispatchQueue . global ( qos : . userInitiated ) . async {
classifier . recognize ( image : UIImage ( named : " launch " ) ! ) { matches in
guard let matches = matches else {
self . log ( " Failed to classify dummy image " )
self . matches = nil
return
}
self . log ( " Dummy classification finished " )
DispatchQueue . global ( qos : . background ) . async {
app . database . update ( recognizedCaps : Set ( matches . keys ) )
}
2020-05-16 11:21:55 +02:00
}
}
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
2021-01-13 21:43:46 +01:00
private func didDownloadClassifier ( ) {
guard let model = app . database . storage . recognitionModel else {
classifier = nil
2020-05-16 11:21:55 +02:00
return
}
2021-01-13 21:43:46 +01:00
classifier = Classifier ( model : model )
2020-05-16 11:21:55 +02:00
guard let image = accessory ! . currentImage else {
2020-06-18 22:55:51 +02:00
classifyDummyImage ( )
2020-05-16 11:21:55 +02:00
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 ( ) {
2020-06-18 22:55:51 +02:00
guard shownCaps . count > 0 else { return }
2020-05-16 11:21:55 +02:00
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 ( )
}
}
// 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 {
2020-09-20 13:28:22 +02:00
accessory ! . showImageView ( with : image )
2020-05-16 11:21:55 +02:00
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-06-18 22:55:51 +02:00
configure ( cell : cell , for : cap )
2019-03-15 13:19:19 +01:00
return cell
}
2020-06-18 22:55:51 +02:00
private func configure ( cell : CapCell , for cap : Cap ) {
let matchText = cap . matchLabelText ( match : match ( for : cap . id ) , appIsUnlocked : self . isUnlocked )
let countText = cap . countLabelText ( appIsUnlocked : self . isUnlocked )
cell . id = cap . id
cell . set ( name : cap . name )
cell . set ( matchLabel : matchText )
cell . set ( countLabel : countText )
2021-01-13 21:43:46 +01:00
if let image = imageProvider . image ( for : cap . id ) {
2020-06-18 22:55:51 +02:00
cell . set ( image : image )
} else {
cell . set ( image : nil )
2021-01-10 16:11:31 +01:00
app . database . downloadImage ( for : cap . id ) { _ in
2020-06-18 22:55:51 +02:00
// D e l e g a t e c a l l w i l l u p d a t e i m a g e
}
}
}
2019-03-15 13:19:19 +01:00
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-06-18 22:55:51 +02:00
override func tableView ( _ tableView : UITableView , willSelectRowAt indexPath : IndexPath ) -> IndexPath ? {
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m s e l e c t i n g c a p s
isUnlocked ? indexPath : nil
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
defer {
tableView . deselectRow ( at : indexPath , animated : true )
}
2020-06-18 22:55:51 +02:00
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
guard isUnlocked else {
return
}
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 ) {
2020-06-18 22:55:51 +02:00
UIImpactFeedbackGenerator ( style : style ) . impactOccurred ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2020-06-18 22:55:51 +02:00
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
guard isUnlocked else {
return nil
}
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
2021-01-13 21:43:46 +01:00
controller . imageProvider = self . imageProvider
2019-03-15 13:19:19 +01:00
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 ]
2020-06-18 22:55:51 +02:00
var actions = [ UIContextualAction ] ( )
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
if isUnlocked {
let count = UIContextualAction ( style : . normal , title : " Update \n count " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
success ( true )
DispatchQueue . global ( qos : . userInitiated ) . async {
app . database . downloadImageCount ( for : cap . id )
2020-05-16 11:21:55 +02:00
}
2019-03-15 13:19:19 +01:00
}
2020-06-18 22:55:51 +02:00
count . backgroundColor = . orange
actions . append ( count )
2019-03-15 13:19:19 +01:00
}
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 ( )
2021-01-13 21:43:46 +01:00
guard let image = self . imageProvider . image ( for : cap . id , version : 0 ) else {
2020-05-16 11:21:55 +02:00
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
2020-06-18 22:55:51 +02:00
actions . append ( similar )
2019-03-15 13:19:19 +01:00
2020-06-18 22:55:51 +02:00
return UISwipeActionsConfiguration ( actions : actions )
2019-03-15 13:19:19 +01:00
}
}
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 {
2021-01-13 21:43:46 +01:00
func database ( needsUserConfirmation title : String , body : String , shouldProceed : @ escaping ( Bool ) -> Void ) {
presentUserBinaryChoice ( title , detail : body , yesText : " Download " , noText : " Later " , dismissed : {
shouldProceed ( false )
} ) {
shouldProceed ( true )
}
}
func databaseHasNewClassifier ( ) {
didDownloadClassifier ( )
}
func database ( completedBackgroundWorkItem title : String , subtitle : String ) {
set ( title : title , subtitle : subtitle )
}
func database ( didFailBackgroundWork title : String , subtitle : String ) {
set ( title : title , subtitle : subtitle )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + . seconds ( 10 ) ) {
self . updateNavigationItemTitleView ( )
}
}
func databaseDidFinishBackgroundWork ( ) {
// s e t ( t i t l e : " A l l t a s k s c o m p l e t e d " , s u b t i t l e : t i t l e T e x t )
self . updateNavigationItemTitleView ( )
// D i s p a t c h Q u e u e . m a i n . a s y n c A f t e r ( d e a d l i n e : . n o w ( ) + . s e c o n d s ( 5 ) ) {
// s e l f . u p d a t e N a v i g a t i o n I t e m T i t l e V i e w ( )
// }
}
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
2020-06-18 22:55:51 +02:00
func database ( didChangeCap id : Int ) {
updateNavigationItemTitleView ( )
guard let cap = app . database . cap ( for : id ) else {
log ( " Changed cap \( id ) not found in database " )
2019-03-15 13:19:19 +01:00
return
}
2020-06-18 22:55:51 +02:00
if let index = caps . firstIndex ( where : { $0 . id = = id } ) {
caps [ index ] = cap
} else {
log ( " Cap not found in full list " )
}
if let index = shownCaps . firstIndex ( where : { $0 . id = = id } ) {
shownCaps [ index ] = cap
}
guard let cell = visibleCell ( for : id ) else {
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
configure ( cell : cell , for : cap )
}
func database ( didLoadImageForCap cap : Int ) {
2021-01-13 21:43:46 +01:00
DispatchQueue . main . async {
guard let cell = self . visibleCell ( for : cap ) else {
return
}
guard let image = self . imageProvider . image ( for : cap ) else {
self . log ( " No image for cap \( cap ) , although it should be loaded " )
return
}
cell . set ( image : image )
2019-03-15 13:19:19 +01:00
}
2021-01-13 21:43:46 +01:00
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
func databaseNeedsFullRefresh ( ) {
2020-05-16 11:21:55 +02:00
reloadCapsFromDatabase ( )
}
2020-06-18 22:55:51 +02:00
private func visibleCell ( for cap : Int ) -> CapCell ? {
tableView . visibleCells
. map { $0 as ! CapCell }
. first { $0 . id = = cap }
}
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 ) {
2020-09-20 13:28:22 +02:00
guard isUnlocked else {
return
}
2020-05-16 11:21:55 +02:00
saveNewCap ( for : image )
}
func capAccessoryCameraButtonPressed ( ) {
showCameraView ( )
}
2021-01-10 16:11:31 +01:00
}