// // TableView.swift // CapFinder // // Created by User on 22.04.18. // Copyright © 2018 User. All rights reserved. // import UIKit class TableView: UIViewController { @IBOutlet weak var table: UITableView! @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var searchBarConstraint: NSLayoutConstraint! @IBOutlet weak var imageButtonConstraint: NSLayoutConstraint! private let imageViewDistance: CGFloat = 90 override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } // MARK: - Life cycle override func viewDidLoad() { super.viewDidLoad() Classifier.shared.delegate = self Cap.delegate = self tableViewSetup() imageButtonSetup() searchBar.delegate = self searchBarSetup() self.imageViewContraint.constant = imageViewDistance DropboxController.shared.initializeDatabase() } private var caps = [Cap]() private var shownCaps = [Cap]() private var sortType: SortCriteria = .id private var sortAscending: Bool = false /// This will be set to a cap id when adding a cap to it private var capToAddImageTo: Cap? // MARK: - Sort /** Resets the cap list to its original state, discarding any previous sorting. */ private func showAllCapsByDescendingId() { sortType = .id sortAscending = false showAllCapsAndScrollToTop() } /** Display all caps in the table, and scrolls to the top */ private func showAllCapsAndScrollToTop() { caps = Cap.capList(sortedBy: sortType, ascending: sortAscending) shownCaps = caps table.reloadData() tableViewScrollToTop() } // MARK: - TableView private func tableViewSetup() { table.dataSource = self table.delegate = self table.rowHeight = 100 } /** Scroll the table view to the top */ private func tableViewScrollToTop() { guard shownCaps.count > 0 else { return } let path = IndexPath(row: 0, section: 0) table.scrollToRow(at: path, at: .top, animated: true) } private func rename(cap: Cap, at indexPath: IndexPath) { let alertController = UIAlertController( title: "Enter name", message: "Choose a name for the image", preferredStyle: .alert, blurStyle: .dark) alertController.addTextField { textField in textField.text = cap.name textField.placeholder = "Cap name" textField.keyboardType = .default } let action = UIAlertAction(title: "Save", style: .default) { _ in guard let name = alertController.textFields?.first?.text else { return } cap.name = name self.table.reloadRows(at: [indexPath], with: .none) } alertController.addAction(action) let cancel = UIAlertAction(title: "Cancel", style: .cancel) alertController.addAction(cancel) self.present(alertController, animated: true) alertController.view.tintColor = AppDelegate.tintColor } // MARK: - Search bar private func searchBarSetup() { searchBar.text = nil searchBar.setShowsCancelButton(false, animated: false) registerKeyboardNotifications() } private func setSearchBarTextColor() { for subView in searchBar.subviews { for view in subView.subviews { if let textView = view as? UITextField { textView.textColor = AppDelegate.tintColor return } } } } private func registerKeyboardNotifications() { NotificationCenter.default.addObserver( self, selector: #selector(TableView.animateWithKeyboard), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver( self, selector: #selector(TableView.animateWithKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) } @objc func animateWithKeyboard(notification: NSNotification) { let userInfo = notification.userInfo! let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! UInt let moveUp = (notification.name == UIResponder.keyboardWillShowNotification) searchBarConstraint.constant = moveUp ? keyboardHeight + 1 : 0 searchBar.setShowsCancelButton(moveUp, animated: false) imageButtonConstraint.constant = moveUp ? -56 : 0 let options = UIView.AnimationOptions(rawValue: curve << 16) UIView.animate(withDuration: duration, delay: 0, options: options, animations: { self.view.layoutIfNeeded() }) } // MARK: - Image/clear button @IBOutlet weak var imageClearButton: UIButton! private func imageButtonSetup() { let tint = AppDelegate.tintColor imageClearButton.setImage(UIImage.templateImage(named: "camera_square"), for: .normal) imageClearButton.tintColor = tint } @IBAction func imageClearButtonPressed() { discardImage() } // MARK: - Image view @IBOutlet weak var imageView: UIView! @IBOutlet weak var imageViewContraint: NSLayoutConstraint! private func discardImage() { searchBar.resignFirstResponder() searchBar.text = nil Cap.hasMatches = false showAllCapsByDescendingId() imageView(shouldBeVisible: false) } private func showImageView(with image: UIImage) { newImage.image = image showImageView() } private func imageView(shouldBeVisible: Bool) { shouldBeVisible ? showImageView() : dismissImageView() } private func showImageView() { UIView.animate(withDuration: 0.5) { self.imageViewContraint.constant = 0 } } private func dismissImageView() { UIView.animate(withDuration: 0.5, animations: { self.imageViewContraint.constant = self.imageViewDistance }) { _ in self.newImage.image = nil } } @IBOutlet weak var newImage: RoundedImageView! @IBOutlet weak var saveButton: RoundedButton! @IBAction func saveButtonPressed() { if let image = newImage.image { saveNewCap(for: image) } } private func saveNewCap(for image: UIImage) { let alertController = UIAlertController( title: "Enter name", message: "Choose a name for the image", preferredStyle: .alert, blurStyle: .dark) alertController.addTextField { textField in //textField.backgroundColor = UIColor.darkGray textField.textColor = AppDelegate.tintColor textField.placeholder = "Cap name" textField.keyboardType = .default } let action = UIAlertAction(title: "Save", style: .default) { _ in guard let name = alertController.textFields?.first?.text else { self.showAlert("No name for cap") return } let _ = Cap(image: image, name: name) self.discardImage() } let cancel = UIAlertAction(title: "Cancel", style: .cancel) alertController.addAction(action) alertController.addAction(cancel) self.present(alertController, animated: true) alertController.view.tintColor = AppDelegate.tintColor } @IBOutlet weak var deleteButton: RoundedButton! @IBAction func deleteButtonPressed() { discardImage() } // MARK: - Navigation override func prepare(for segue: UIStoryboardSegue, sender: Any?) { self.giveFeedback(.medium) guard let id = segue.identifier else { error("No identifier for segue") return } switch id { case "showCamera": (segue.destination as! CameraController).delegate = self case "showSettings": break default: error("Invalid segue identifier \(id)") } } @IBAction func showSortOptions(_ sender: UIBarButtonItem) { 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.barButtonItem = sender presentationController.permittedArrowDirections = [.up] self.present(controller, animated: true) } } // MARK: - SortControllerDelegate extension TableView: SortControllerDelegate { func sortController(didSelect sortType: SortCriteria, ascending: Bool) { self.sortType = sortType self.sortAscending = ascending if sortType != .match { Cap.hasMatches = false } showAllCapsAndScrollToTop() } } // MARK: - CameraControllerDelegate extension TableView: CameraControllerDelegate { func didCapture(image: UIImage) { if let cap = capToAddImageTo { cap.add(image: image) { success in guard success else { self.error("Could not save image") return } self.capToAddImageTo = nil } } else { // Hand image to classifier, delegate is ListViewController Classifier.shared.recognise(image: image) } } func didCancel() { capToAddImageTo = nil } } // MARK: - UITableViewDataSource extension TableView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cap") as! CapCell let cap = shownCaps[indexPath.row] cell.cap = cap return cell } func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return shownCaps.count } } // MARK: - UITableViewDelegate extension TableView: UITableViewDelegate { private func takeImage(for cap: Cap) { self.capToAddImageTo = cap self.performSegue(withIdentifier: "showCamera", sender: nil) } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let cap = shownCaps[indexPath.row] if let image = newImage.image { cap.add(image: image) { success in guard success else { self.error("Could not save image") return } self.giveFeedback(.medium) self.updateCell(for: cap.id) self.discardImage() } } else { self.giveFeedback(.medium) takeImage(for: cap) } table.deselectRow(at: indexPath, animated: true) } private func updateCell(for capId: Int) { let cell = table.visibleCells.first { cell in let item = cell as! CapCell return item.cap.id == capId } (cell as! CapCell).updateCell() } private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { let generator = UIImpactFeedbackGenerator(style: style) generator.impactOccurred() } func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let cap = shownCaps[indexPath.row] let rename = UIContextualAction(style: .normal, title: "Rename\ncap") { (_, _, success) in success(true) self.rename(cap: cap, at: indexPath) self.giveFeedback(.medium) } rename.backgroundColor = .blue let image = UIContextualAction(style: .normal, title: "Change\nimage") { (_, _, 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]) } func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let cap = shownCaps[indexPath.row] let count = UIContextualAction(style: .normal, title: "Update\ncount") { (_, _, success) in self.giveFeedback(.medium) cap.updateCount { result in self.updateCell(for: cap.id) success(result) } } count.backgroundColor = .orange let similar = UIContextualAction(style: .normal, title: "Similar\ncaps") { (_, _, success) in self.giveFeedback(.medium) self.imageView(shouldBeVisible: false) if let image = cap.image { Classifier.shared.recognise(image: image, reportingImage: false) } success(true) } similar.backgroundColor = .blue return UISwipeActionsConfiguration(actions: [similar, count]) } } // MARK: - ClassifierDelegate extension TableView: ClassifierDelegate { func classifier(finished image: UIImage?) { if let img = image { showImageView(with: img) } sortType = .match sortAscending = false Cap.hasMatches = true showAllCapsAndScrollToTop() } func classifier(error: String) { self.showAlert(error) } } // MARK: - UISearchBarDelegate extension TableView: UISearchBarDelegate { func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { searchBar.resignFirstResponder() searchBar.text = nil showAllCapsAndScrollToTop() } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { searchBar.resignFirstResponder() } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { guard searchText != "" else { showAllCapsAndScrollToTop() return } DispatchQueue.global(qos: .userInteractive).async { let cleaned = searchText.clean let filteredCaps = self.caps.filter { cap in let name = cap.cleanName // For each part of text, check if name contains it for textItem in cleaned.components(separatedBy: " ") { if textItem != "" && !name.contains(textItem) { return false } } return true } DispatchQueue.main.async { self.shownCaps = filteredCaps self.table.reloadData() self.tableViewScrollToTop() } } } } // MARK: - Logging extension TableView: Logger { static let logToken = "[TableView]" } // MARK: - Protocol CapsDelegate extension TableView: CapsDelegate { func capHasUpdates(_ cap: Cap) { if let cell = table.visibleCells.first(where: { ($0 as! CapCell).cap == cap }) { (cell as! CapCell).updateCell() } else if !caps.contains(cap) { // Reload the table when new cap is added showAllCapsAndScrollToTop() } } func capsLoaded() { showAllCapsByDescendingId() } }