Caps-iOS/CapCollector/TableView.swift
2019-04-12 13:45:31 +02:00

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()
}
}