531 lines
16 KiB
Swift
531 lines
16 KiB
Swift
//
|
|
// 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) {
|
|
DispatchQueue.main.async {
|
|
if let cell = self.table.visibleCells.first(where: { ($0 as! CapCell).cap == cap }) {
|
|
(cell as! CapCell).updateCell()
|
|
} else if !self.caps.contains(cap) {
|
|
// Reload the table when new cap is added
|
|
self.showAllCapsAndScrollToTop()
|
|
}
|
|
}
|
|
}
|
|
|
|
func capsLoaded() {
|
|
showAllCapsByDescendingId()
|
|
}
|
|
}
|