Version 1

This commit is contained in:
Christoph Hagen 2019-03-15 13:19:19 +01:00
parent bd63eb38e2
commit 2806733b71
42 changed files with 3561 additions and 259 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/build/*
/Pods/*
Podfile.lock

View File

@ -8,6 +8,14 @@
/* Begin PBXBuildFile section */
043EC7C35065DD26F6BB496F /* Pods_CapCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */; };
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252EB21A837FB005B1179 /* Squeezenet.mlmodel */; };
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252ED21A837FB005B1179 /* Resnet.mlmodel */; };
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */; };
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBA82174CBB800EC84BB /* SettingsController.swift */; };
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBAA21762D9600EC84BB /* UserDefaults.swift */; };
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
@ -15,9 +23,6 @@
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
CE56CEF9209D83B800932C01 /* CapImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE1209D83B200932C01 /* CapImages.swift */; };
CE56CEFA209D83B800932C01 /* CapNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE2209D83B200932C01 /* CapNames.swift */; };
CE56CEFC209D83B800932C01 /* DropboxUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE4209D83B300932C01 /* DropboxUpload.swift */; };
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE5209D83B300932C01 /* NameFile.swift */; };
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
@ -33,16 +38,21 @@
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
CE56CF0C209D83B800932C01 /* DropboxDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF4209D83B600932C01 /* DropboxDownload.swift */; };
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */; };
CEF38744209D8476001C8D3C /* Resnet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = CEF38742209D8476001C8D3C /* Resnet.mlmodel */; };
CEF38745209D8476001C8D3C /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.release.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.release.xcconfig"; sourceTree = "<group>"; };
5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; };
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
591252ED21A837FB005B1179 /* Resnet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Resnet.mlmodel; path = ../../../../Dropbox/Models/Resnet.mlmodel; sourceTree = "<group>"; };
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; };
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = ImageClassifier.mlmodel; path = ../../../../Dropbox/Models/ImageClassifier.mlmodel; sourceTree = "<group>"; };
59C1BBA82174CBB800EC84BB /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CapCollector.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -53,9 +63,6 @@
CE56CED7209D81E000932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
CE56CED9209D81E000932C01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CE56CEE0209D83B200932C01 /* CapCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapCell.swift; sourceTree = "<group>"; };
CE56CEE1209D83B200932C01 /* CapImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapImages.swift; sourceTree = "<group>"; };
CE56CEE2209D83B200932C01 /* CapNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapNames.swift; sourceTree = "<group>"; };
CE56CEE4209D83B300932C01 /* DropboxUpload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxUpload.swift; sourceTree = "<group>"; };
CE56CEE5209D83B300932C01 /* NameFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameFile.swift; sourceTree = "<group>"; };
CE56CEE6209D83B300932C01 /* RoundedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = "<group>"; };
CE56CEE7209D83B300932C01 /* CameraController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
@ -71,13 +78,9 @@
CE56CEF1209D83B500932C01 /* Classifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
CE56CEF2209D83B600932C01 /* CropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropView.swift; sourceTree = "<group>"; };
CE56CEF3209D83B600932C01 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
CE56CEF4209D83B600932C01 /* DropboxDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxDownload.swift; sourceTree = "<group>"; };
CE56CEF5209D83B600932C01 /* ImageSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelector.swift; sourceTree = "<group>"; };
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureHandler.swift; sourceTree = "<group>"; };
CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = "<group>"; };
CE92CA6120CAD38600D5DAA4 /* Xcode.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = Xcode.mlmodel; sourceTree = "<group>"; };
CEF38742209D8476001C8D3C /* Resnet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Resnet.mlmodel; path = ../../../../Dropbox/Models/Resnet.mlmodel; sourceTree = "<group>"; };
CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
DBD72193E502C23E06DA913D /* Pods-CapCollector.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -102,6 +105,16 @@
name = Pods;
sourceTree = "<group>";
};
591252E921A837B4005B1179 /* Models */ = {
isa = PBXGroup;
children = (
591252ED21A837FB005B1179 /* Resnet.mlmodel */,
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */,
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */,
);
name = Models;
sourceTree = "<group>";
};
9EAE4B3CEE704AF443897B44 /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -132,10 +145,9 @@
isa = PBXGroup;
children = (
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
CEF38742209D8476001C8D3C /* Resnet.mlmodel */,
CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */,
CE92CA6120CAD38600D5DAA4 /* Xcode.mlmodel */,
CE56CED1209D81DE00932C01 /* Main.storyboard */,
CE56CEF1209D83B500932C01 /* Classifier.swift */,
591252E921A837B4005B1179 /* Models */,
CEF3874D209D9378001C8D3C /* Capture */,
CEF3874E209D9390001C8D3C /* Sync */,
CEF38750209D93D1001C8D3C /* Data */,
@ -143,8 +155,6 @@
CEF3874F209D93A6001C8D3C /* Presentation */,
CEF3874C209D935E001C8D3C /* Extensions */,
CE56CEF3209D83B600932C01 /* Logger.swift */,
CE56CED1209D81DE00932C01 /* Main.storyboard */,
59C1BBA82174CBB800EC84BB /* SettingsController.swift */,
CE56CEDF209D81FD00932C01 /* Support */,
);
path = CapCollector;
@ -163,6 +173,7 @@
CEF3874B209D932E001C8D3C /* View Components */ = {
isa = PBXGroup;
children = (
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */,
CE56CEF2209D83B600932C01 /* CropView.swift */,
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */,
CE56CEE6209D83B300932C01 /* RoundedButton.swift */,
@ -195,8 +206,6 @@
isa = PBXGroup;
children = (
CE56CEE9209D83B400932C01 /* DropBoxController.swift */,
CE56CEF4209D83B600932C01 /* DropboxDownload.swift */,
CE56CEE4209D83B300932C01 /* DropboxUpload.swift */,
);
path = Sync;
sourceTree = "<group>";
@ -205,9 +214,13 @@
isa = PBXGroup;
children = (
CE56CEEB209D83B400932C01 /* TableView.swift */,
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
CE56CEE0209D83B200932C01 /* CapCell.swift */,
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
59C1BBA82174CBB800EC84BB /* SettingsController.swift */,
5904C3392199C9FA0046A573 /* SortController.swift */,
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
);
path = Presentation;
sourceTree = "<group>";
@ -215,9 +228,8 @@
CEF38750209D93D1001C8D3C /* Data */ = {
isa = PBXGroup;
children = (
CE56CEE1209D83B200932C01 /* CapImages.swift */,
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */,
CE56CEE2209D83B200932C01 /* CapNames.swift */,
591832CD21A2A97E00E5987D /* Cap.swift */,
CE56CEF0209D83B500932C01 /* DiskManager.swift */,
CE56CEE5209D83B300932C01 /* NameFile.swift */,
);
@ -258,7 +270,12 @@
TargetAttributes = {
CE56CEC9209D81DD00932C01 = {
CreatedOnToolsVersion = 9.4;
LastSwiftMigration = 1000;
LastSwiftMigration = 1020;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 0;
};
};
};
};
};
@ -340,33 +357,35 @@
buildActionMask = 2147483647;
files = (
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */,
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */,
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
CE56CF01209D83B800932C01 /* DropBoxController.swift in Sources */,
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
CE56CF08209D83B800932C01 /* DiskManager.swift in Sources */,
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */,
CE56CEF9209D83B800932C01 /* CapImages.swift in Sources */,
CEF38745209D8476001C8D3C /* Squeezenet.mlmodel in Sources */,
CEF38744209D8476001C8D3C /* Resnet.mlmodel in Sources */,
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */,
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */,
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */,
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
CE56CEFA209D83B800932C01 /* CapNames.swift in Sources */,
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
CE56CF00209D83B800932C01 /* UIAlertControllerExtensions.swift in Sources */,
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
CE56CF0C209D83B800932C01 /* DropboxDownload.swift in Sources */,
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
CE56CEFC209D83B800932C01 /* DropboxUpload.swift in Sources */,
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>CapCollector.xcscheme</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>CapCollector.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:CapCollector.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array/>
</plist>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
type = "0"
version = "2.0">
</Bucket>

View File

@ -10,9 +10,29 @@ import UIKit
import CoreData
import SwiftyDropbox
var shouldLaunchCamera = false
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
enum ShortcutIdentifier: String {
case first
// MARK: - Initializers
init?(fullType: String) {
guard let last = fullType.components(separatedBy: ".").last else { return nil }
self.init(rawValue: last)
}
// MARK: - Properties
var type: String {
return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
}
}
// MARK: - Static Properties
/// Main tint color of the app
static let tintColor = UIColor(red: 122/255, green: 155/255, blue: 41/255, alpha: 1)
@ -26,6 +46,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
DropboxController.shared.handle(url: url)
return true
}
private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
event("Shortcut pressed")
shouldLaunchCamera = true
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
Cap.uploadRemainingImages()
guard shouldLaunchCamera else { return }
shouldLaunchCamera = false
if let c = (frontmostViewController as? UINavigationController)?.topViewController as? TableView {
c.performSegue(withIdentifier: "showCamera", sender: nil)
}
}
/*
Called when the user activates your application by selecting a shortcut on the home screen, except when
application(_:,willFinishLaunchingWithOptions:) or application(_:didFinishLaunchingWithOptions) returns `false`.
You should handle the shortcut in those callbacks and return `false` if possible. In that case, this
callback is used if your application is already launched in the background.
*/
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
let handledShortCutItem = handleShortCutItem(shortcutItem)
completionHandler(handledShortCutItem)
}
var frontmostViewController: UIViewController? {
var controller = window?.rootViewController
while let presentedViewController = controller?.presentedViewController {
@ -34,9 +85,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return controller
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
DropboxController.shared.handle(url: url)
return true
}
}
extension AppDelegate: Logger {
static let logToken = "[AppDelegate]"
}

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qlf-I7-aOI">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qlf-I7-aOI">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -20,17 +20,42 @@
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="separatorColor" red="0.4408732927524982" green="0.4408732927524982" blue="0.4408732927524982" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<sections>
<tableViewSection headerTitle="Mosaic" footerTitle="Show a grid view of all caps." id="gNs-aR-ZXg">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="41x-gC-Qt0" style="IBUITableViewCellStyleDefault" id="jeG-JT-PbE">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="jeG-JT-PbE" id="h7b-Hm-qFm">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Show mosaic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="41x-gC-Qt0">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<connections>
<segue destination="wzG-WL-mtW" kind="show" identifier="showMosaic" id="bU4-Uu-zLI"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Database" footerTitle="New caps can't be found through matching. Update the app to the newest version to receive new matching models." id="t1t-6Z-uZp">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="xVf-9U-8dt" detailTextLabel="cwp-eR-aXo" style="IBUITableViewCellStyleSubtitle" id="pQw-5h-loP">
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<rect key="frame" x="0.0" y="175" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pQw-5h-loP" id="Hkl-nU-jPG">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="1167 caps, 5600 images" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xVf-9U-8dt">
<rect key="frame" x="16" y="5" width="185.5" height="20.5"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="1167 caps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xVf-9U-8dt">
<rect key="frame" x="16" y="5" width="76" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@ -47,12 +72,33 @@
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="NAMES" footerTitle="Updates names and image counts of the index" id="fnu-vJ-6gE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="og4-Az-LlV" detailTextLabel="wCH-gF-GY2" style="IBUITableViewCellStyleSubtitle" id="5k2-RN-OpJ">
<rect key="frame" x="0.0" y="219" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5k2-RN-OpJ" id="l2t-Ds-k5L">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="5600 images" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="og4-Az-LlV">
<rect key="frame" x="16" y="5" width="101" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="6.15 images/cap, lowest count: 4 (36x)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wCH-gF-GY2">
<rect key="frame" x="16" y="25.5" width="219.5" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NLm-th-Ww3" detailTextLabel="Zdl-lL-KYt" style="IBUITableViewCellStyleSubtitle" id="M1J-fv-ztL">
<rect key="frame" x="0.0" y="207" width="375" height="44"/>
<rect key="frame" x="0.0" y="263" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="M1J-fv-ztL" id="r9E-Qt-wSx">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -78,10 +124,68 @@
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Refresh" footerTitle="Refreshes the image count of all caps. This will take a while." id="3rn-AC-q60">
<tableViewSection headerTitle="Classifier" footerTitle="Choose which of the available classifiers should be used for image comparison." id="wUs-7C-Kzz">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="U7G-Xu-7Xd" style="IBUITableViewCellStyleDefault" id="h7a-jf-vVw">
<rect key="frame" x="0.0" y="414.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="h7a-jf-vVw" id="ZoJ-cw-jsL">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Squeezenet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="U7G-Xu-7Xd">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="IQ2-te-8MM" style="IBUITableViewCellStyleDefault" id="EBb-eO-HMg">
<rect key="frame" x="0.0" y="458.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EBb-eO-HMg" id="zG0-L8-y2b">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Resnet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IQ2-te-8MM">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="voV-Qq-csv" style="IBUITableViewCellStyleDefault" id="NLd-me-0rl">
<rect key="frame" x="0.0" y="502.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="NLd-me-0rl" id="ZNG-Cc-N8D">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Xcode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="voV-Qq-csv">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Refresh" footerTitle="Update ressources that might be outdated." id="3rn-AC-q60">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="tgU-ma-Xhz" style="IBUITableViewCellStyleDefault" id="GoI-GJ-dx1">
<rect key="frame" x="0.0" y="326.5" width="375" height="44"/>
<rect key="frame" x="0.0" y="638" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GoI-GJ-dx1" id="AOV-5g-KkH">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -98,12 +202,30 @@
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NgT-am-9HS" style="IBUITableViewCellStyleDefault" id="d8A-F9-8yL">
<rect key="frame" x="0.0" y="682" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d8A-F9-8yL" id="aA7-ai-YNA">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Update thumbnails" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="NgT-am-9HS">
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Dropbox" footerTitle="Sign in to dropbox to access the cap database." id="Nw5-uf-OcQ">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dW2-Yd-L4J" style="IBUITableViewCellStyleDefault" id="Hie-e3-jja">
<rect key="frame" x="0.0" y="462" width="375" height="44"/>
<rect key="frame" x="0.0" y="801.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hie-e3-jja" id="z9u-9f-fqP">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -129,10 +251,13 @@
</connections>
</tableView>
<connections>
<outlet property="countsLabel" destination="tgU-ma-Xhz" id="ztT-tr-bKu"/>
<outlet property="databaseUpdatesLabel" destination="Zdl-lL-KYt" id="akt-Mb-Aqe"/>
<outlet property="dropboxAccountLabel" destination="dW2-Yd-L4J" id="lIc-yn-YxK"/>
<outlet property="imagesStatsLabel" destination="wCH-gF-GY2" id="p5q-E4-hcp"/>
<outlet property="recognizedCapsLabel" destination="cwp-eR-aXo" id="Nvh-8s-sRU"/>
<outlet property="totalCapsLabel" destination="xVf-9U-8dt" id="LeK-Xt-loM"/>
<outlet property="totalImagesLabel" destination="og4-Az-LlV" id="sVW-t6-1MF"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="rhf-CV-Ipz" userLabel="First Responder" sceneMemberID="firstResponder"/>
@ -221,7 +346,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="xnW-hF-IOj" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4097" y="2262"/>
<point key="canvasLocation" x="2740" y="2262"/>
</scene>
<!--New image-->
<scene sceneID="5iQ-Nr-LFc">
@ -352,6 +477,157 @@
</objects>
<point key="canvasLocation" x="4096.8000000000002" y="1557.5712143928038"/>
</scene>
<!--Grid View Controller-->
<scene sceneID="Hjr-cD-s35">
<objects>
<viewController storyboardIdentifier="GridView" id="wzG-WL-mtW" customClass="GridViewController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="9sc-l8-fcK">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TCx-cV-mMG">
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
</scrollView>
</subviews>
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="TCx-cV-mMG" firstAttribute="top" secondItem="WAE-if-wuA" secondAttribute="top" id="C5g-UD-eI2"/>
<constraint firstItem="WAE-if-wuA" firstAttribute="bottom" secondItem="TCx-cV-mMG" secondAttribute="bottom" id="J5D-Xs-Unl"/>
<constraint firstItem="TCx-cV-mMG" firstAttribute="leading" secondItem="WAE-if-wuA" secondAttribute="leading" id="fXw-7M-B0G"/>
<constraint firstItem="WAE-if-wuA" firstAttribute="trailing" secondItem="TCx-cV-mMG" secondAttribute="trailing" id="sY8-ka-loB"/>
</constraints>
<viewLayoutGuide key="safeArea" id="WAE-if-wuA"/>
</view>
<connections>
<outlet property="scrollView" destination="TCx-cV-mMG" id="Isn-jV-DBf"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="9Os-vj-mkT" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4096.8000000000002" y="2261.9190404797605"/>
</scene>
<!--Sort Controller-->
<scene sceneID="fFw-OX-Mag">
<objects>
<tableViewController storyboardIdentifier="SortController" id="xVJ-JZ-U8g" customClass="SortController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" scrollEnabled="NO" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="LcF-G6-Sln">
<rect key="frame" x="0.0" y="0.0" width="150" height="310"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="separatorColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<sections>
<tableViewSection headerTitle="Sort order" id="XuT-wG-qHV">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="2cU-Pz-MYZ" rowHeight="40" style="IBUITableViewCellStyleDefault" id="vYb-0s-NQp">
<rect key="frame" x="0.0" y="55.5" width="150" height="40"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vYb-0s-NQp" id="8bD-vd-HWF">
<rect key="frame" x="0.0" y="0.0" width="110" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Ascending" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2cU-Pz-MYZ">
<rect key="frame" x="16" y="0.0" width="94" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Sort by" id="a86-13-jdq">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="jBZ-Qh-cQp" rowHeight="40" style="IBUITableViewCellStyleDefault" id="RIw-UJ-LAh">
<rect key="frame" x="0.0" y="151.5" width="150" height="40"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="RIw-UJ-LAh" id="84U-pf-v8B">
<rect key="frame" x="0.0" y="0.0" width="110" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Id" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="jBZ-Qh-cQp">
<rect key="frame" x="16" y="0.0" width="94" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Vbx-7v-vhu" rowHeight="40" style="IBUITableViewCellStyleDefault" id="V6N-Bw-TfC">
<rect key="frame" x="0.0" y="191.5" width="150" height="40"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="V6N-Bw-TfC" id="kjo-xX-12y">
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Vbx-7v-vhu">
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="neG-u0-8YU" rowHeight="40" style="IBUITableViewCellStyleDefault" id="YR5-96-lup">
<rect key="frame" x="0.0" y="231.5" width="150" height="40"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="YR5-96-lup" id="Bae-Lt-m8T">
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Image count" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="neG-u0-8YU">
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="hUh-3J-pzj" rowHeight="40" style="IBUITableViewCellStyleDefault" id="3yB-nf-zbV">
<rect key="frame" x="0.0" y="271.5" width="150" height="40"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3yB-nf-zbV" id="xiR-G2-PuL">
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hUh-3J-pzj">
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="xVJ-JZ-U8g" id="27V-wO-lvY"/>
<outlet property="delegate" destination="xVJ-JZ-U8g" id="TdR-BV-8pz"/>
</connections>
</tableView>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<size key="freeformSize" width="150" height="310"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="l35-YK-LmX" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3589" y="1052"/>
</scene>
<!--Caps-->
<scene sceneID="3Dy-nA-zIw">
<objects>
@ -360,37 +636,47 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsVerticalScrollIndicator="NO" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Djy-MM-jNn">
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsVerticalScrollIndicator="NO" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Djy-MM-jNn">
<rect key="frame" x="0.0" y="154" width="375" height="457"/>
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="separatorColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="cap" rowHeight="100" id="ubX-SO-ltt" customClass="CapCell" customModule="CapCollector" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ubX-SO-ltt" id="BD9-Ic-hwS">
<rect key="frame" x="0.0" y="0.0" width="375" height="100"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="99.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="eHr-Uk-WC9" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
<rect key="frame" x="2" y="2" width="96" height="96"/>
<rect key="frame" x="2" y="2" width="95.5" height="95.5"/>
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="eHr-Uk-WC9" secondAttribute="height" multiplier="1:1" id="640-1g-g0v"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="71 % match" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pe1-sR-2RT">
<rect key="frame" x="106" y="2.5" width="128" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="# images" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pe1-sR-2RT">
<rect key="frame" x="105.5" y="10" width="70" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Some brand with a long name of more than 2 lines" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sij-H0-zYC">
<rect key="frame" x="106" y="25.5" width="261" height="49"/>
<rect key="frame" x="105.5" y="33" width="261.5" height="49"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<color key="textColor" red="0.83921568629999999" green="0.9379758883" blue="0.54901960780000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="# caps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nJ1-TQ-ZT3">
<rect key="frame" x="315.5" y="2.5" width="51.5" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="71 % match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nJ1-TQ-ZT3">
<rect key="frame" x="279" y="10" width="88" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<nil key="highlightedColor"/>
@ -398,24 +684,23 @@
</subviews>
<constraints>
<constraint firstItem="Pe1-sR-2RT" firstAttribute="leading" secondItem="eHr-Uk-WC9" secondAttribute="trailing" constant="8" id="1bU-zq-83L"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="Sij-H0-zYC" secondAttribute="bottom" constant="8" id="3e4-bs-Jka"/>
<constraint firstItem="eHr-Uk-WC9" firstAttribute="leading" secondItem="BD9-Ic-hwS" secondAttribute="leading" constant="2" id="BH6-4m-jIT"/>
<constraint firstItem="Sij-H0-zYC" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="bottom" constant="2" id="FNF-xm-BjR"/>
<constraint firstItem="Pe1-sR-2RT" firstAttribute="top" secondItem="BD9-Ic-hwS" secondAttribute="top" constant="10" id="KY6-3Z-p5Q"/>
<constraint firstItem="Sij-H0-zYC" firstAttribute="leading" secondItem="Pe1-sR-2RT" secondAttribute="leading" id="SdR-AF-gcG"/>
<constraint firstAttribute="bottom" secondItem="eHr-Uk-WC9" secondAttribute="bottom" constant="2" id="UGo-B1-mHf"/>
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="leading" secondItem="Pe1-sR-2RT" secondAttribute="trailing" constant="81.5" id="WQ3-ZG-Hva"/>
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pe1-sR-2RT" secondAttribute="trailing" constant="10" id="WQ3-ZG-Hva"/>
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="top" id="WcF-d6-GHl"/>
<constraint firstItem="Sij-H0-zYC" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="bottom" constant="2" id="cCu-FA-TJ5"/>
<constraint firstAttribute="trailing" secondItem="Sij-H0-zYC" secondAttribute="trailing" constant="8" id="hKh-2e-iDu"/>
<constraint firstItem="eHr-Uk-WC9" firstAttribute="centerY" secondItem="BD9-Ic-hwS" secondAttribute="centerY" id="lRZ-Qc-0MI"/>
<constraint firstItem="Sij-H0-zYC" firstAttribute="centerY" secondItem="BD9-Ic-hwS" secondAttribute="centerY" id="mIz-nP-Mtz"/>
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="trailing" secondItem="Sij-H0-zYC" secondAttribute="trailing" id="r1z-vT-brQ"/>
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="centerY" secondItem="Pe1-sR-2RT" secondAttribute="centerY" id="xBh-YR-DhC"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<connections>
<outlet property="capImage" destination="eHr-Uk-WC9" id="sQH-w5-lp6"/>
<outlet property="countLabel" destination="nJ1-TQ-ZT3" id="Vde-d4-EhH"/>
<outlet property="matchLabel" destination="Pe1-sR-2RT" id="ZPR-ro-2Yg"/>
<outlet property="countLabel" destination="Pe1-sR-2RT" id="ODc-sB-fNO"/>
<outlet property="matchLabel" destination="nJ1-TQ-ZT3" id="M8b-pT-ano"/>
<outlet property="nameLabel" destination="Sij-H0-zYC" id="Wc1-h9-i2E"/>
</connections>
</tableViewCell>
@ -451,6 +736,14 @@
<constraints>
<constraint firstAttribute="width" secondItem="TLY-ak-c4W" secondAttribute="height" multiplier="1:1" id="SU9-Ph-0PV"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Wyc-2T-CIk" customClass="RoundedButton" customModule="CapCollector" customModuleProvider="target">
<rect key="frame" x="96" y="30" width="131.5" height="30"/>
@ -529,6 +822,11 @@
<segue destination="x62-XO-Rsu" kind="show" identifier="showSettings" id="TWW-Vh-GTa"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" title="Sort" id="Bii-kx-Exm">
<connections>
<action selector="showSortOptions:" destination="VSb-c5-JF6" id="pzj-fP-M6S"/>
</connections>
</barButtonItem>
</navigationItem>
<nil key="simulatedBottomBarMetrics"/>
<connections>
@ -551,7 +849,7 @@
<!--Navigation Controller-->
<scene sceneID="cDZ-9F-oGg">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qlf-I7-aOI" sceneMemberID="viewController">
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qlf-I7-aOI" customClass="NavigationController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="tO3-6d-IEo">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>

View File

@ -17,7 +17,7 @@ protocol CameraControllerDelegate {
class CameraController: UIViewController {
private let imageSize = 299 // New for XCode models, 227 for turicreate
private let imageSize = 299 // New for XCode models, 227/229 for turicreate
// MARK: Outlets
@ -35,16 +35,29 @@ class CameraController: UIViewController {
var delegate: CameraControllerDelegate?
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return false
}
// MARK: Actions
@IBAction func backButtonPressed() {
self.giveFeedback(.medium)
delegate?.didCancel()
self.dismiss(animated: true)
}
@IBAction func imageButtonPressed() {
self.giveFeedback(.medium)
imageButton.isEnabled = false
event("Taking image")
cameraView.capture()
}
@ -84,6 +97,11 @@ class CameraController: UIViewController {
cancelButton.set(template: "cancel", with: tint)
}
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
// MARK: Alerts
private func showNoCameraAccessAlert() {
@ -105,6 +123,10 @@ class CameraController: UIViewController {
alert.addAction(settingsAction)
self.present(alert, animated: true, completion: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
cameraView.didReceiveTouch(touches.first!)
}
}
extension CameraController: PhotoCaptureHandlerDelegate {

View File

@ -52,6 +52,8 @@ class CameraView: UIView {
private let photoOutput = AVCapturePhotoOutput()
private var cameraDevice: AVCaptureDevice?
private let photoCaptureProcessor = PhotoCaptureHandler()
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
@ -146,52 +148,80 @@ class CameraView: UIView {
session.sessionPreset = .photo
// Add video input.
do {
guard let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
print("No camera on device")
error("No camera on device")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
let videoDeviceInput = try AVCaptureDeviceInput(device: backCameraDevice)
let videoDeviceInput: AVCaptureDeviceInput
do {
videoDeviceInput = try AVCaptureDeviceInput(device: backCameraDevice)
} catch {
self.error("Could not create video device input: \(error)")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
guard session.canAddInput(videoDeviceInput) else {
error("Could not add video device input to the session")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
if session.canAddInput(videoDeviceInput) {
session.addInput(videoDeviceInput)
self.videoDeviceInput = videoDeviceInput
self.cameraDevice = backCameraDevice
DispatchQueue.main.async {
self.videoPreviewLayer.connection?.videoOrientation = .portrait
}
} else {
print("Could not add video device input to the session")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
} catch {
print("Could not create video device input: \(error)")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
// Add photo output.
if session.canAddOutput(photoOutput) {
guard session.canAddOutput(photoOutput) else {
error("Could not add photo output to the session")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
session.addOutput(photoOutput)
photoOutput.isHighResolutionCaptureEnabled = true
photoOutput.isDepthDataDeliveryEnabled = false
photoOutput.isDualCameraDualPhotoDeliveryEnabled = false
photoOutput.isLivePhotoCaptureEnabled = false
} else {
print("Could not add photo output to the session")
setupResult = .configurationFailed
session.commitConfiguration()
return
}
session.commitConfiguration()
func didReceiveTouch(_ touch: UITouch) {
let screenSize = bounds.size
let location = touch.location(in: self)
let focusPoint = CGPoint(x: location.y / screenSize.height, y: 1.0 - location.x / screenSize.width)
event("Focusing on point (\(focusPoint.x),\(focusPoint.y))")
if let device = cameraDevice {
do {
try device.lockForConfiguration()
if device.isFocusPointOfInterestSupported {
device.focusPointOfInterest = focusPoint
device.focusMode = .autoFocus
}
// if device.isExposurePointOfInterestSupported {
// device.exposurePointOfInterest = focusPoint
// device.exposureMode = .autoExpose
// }
device.unlockForConfiguration()
} catch {
self.error("Could not lock device for configuration: \(error)")
}
}
}
}
extension CameraView: Logger {
static let logToken = "CameraView"
}

View File

@ -0,0 +1,201 @@
//
// VisionHandler.swift
// CapFinder
//
// Created by User on 12.02.18.
// Copyright © 2018 User. All rights reserved.
//
import Foundation
import Vision
import CoreML
import UIKit
/// Notify the delegate about
protocol ClassifierDelegate {
/// Features found
func classifier(finished image: UIImage?)
/// Error handler
func classifier(error: String)
}
/// Recognise categories in images
class Classifier: Logger {
static let logToken = "[Classifier]"
static var shared = Classifier()
/// Handles errors and recognised features
var delegate: ClassifierDelegate?
// MARK: Stored predictions
private var predictions = [[Int : Float]]()
private var notify = false
private var image: UIImage?
private func request(for model: MLModel, name: String) -> VNCoreMLRequest {
let model = try! VNCoreMLModel(for: model)
let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
self?.process(request: request, error: error)
self?.event("Finished \(name) prediction (\(self!.predictions.count)/\(self!.requestCount))")
})
request.imageCropAndScaleOption = .centerCrop
return request
}
private var requestCount = 0
private var requests: [VNCoreMLRequest] {
var reqs = [VNCoreMLRequest]()
if Persistence.squeezenet {
reqs.append(request(for: Squeezenet().model, name: "Squeezenet"))
}
if Persistence.resnet {
reqs.append(request(for: Resnet().model, name: "Resnet"))
}
if Persistence.xcode {
reqs.append(request(for: ImageClassifier().model, name: "Xcode"))
}
requestCount = reqs.count
return reqs
}
/**
Classify an image
- parameter image: The image to classify
- parameter reportingImage: Set to true, if the delegate should receive the image
*/
func recognise(image: UIImage, reportingImage: Bool = true) {
predictions.removeAll()
self.image = image
notify = reportingImage
performClassifications()
}
private func performClassifications() {
let orientation = CGImagePropertyOrientation(image!.imageOrientation)
guard let ciImage = CIImage(image: image!) else {
report(error: "Unable to create CIImage")
return
}
DispatchQueue.global(qos: .userInitiated).async {
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
let requests = self.requests
guard requests.count > 0 else {
self.report(error: "No classifiers selected")
return
}
do {
try handler.perform(requests)
} catch {
DispatchQueue.main.async {
self.report(error: "Classification failed: \(error.localizedDescription)")
}
}
}
}
private func process(request: VNRequest, error: Error?) {
guard let result = request.results as? [VNClassificationObservation],
result.isEmpty == false else {
report(error: "Unable to classify image: \(error?.localizedDescription ?? "No error thrown")")
return
}
let current = dict(from: result)
predictions.append(current)
if predictions.count == requestCount {
updateRecognizedCapsCount()
combine()
}
}
/// Create a dictionary from a vision prediction
private func dict(from results: [VNClassificationObservation]) -> [Int : Float] {
let array = results.map{ item -> (Int, Float) in
return (Int(item.identifier) ?? 0, item.confidence)
}
return [Int : Float](uniqueKeysWithValues: array)
}
/// Combine two predictions
private func combine() {
Cap.unsortedCaps.forEach { cap in
var result: Float = 0
for index in 0..<predictions.count {
result = max(predictions[index][cap.id] ?? 0, result)
}
cap.match = result
}
Cap.hasMatches = true
report()
}
private func updateRecognizedCapsCount() {
let recognizedCaps = predictions.map { prediction in
return prediction.count
}
Persistence.recognizedCapCount = recognizedCaps.max()!
}
// MARK: Callbacks
private func cleanup() {
predictions.removeAll()
image = nil
}
private func report(error message: String) {
guard delegate != nil else {
error("No delegate: " + message)
return
}
DispatchQueue.main.async {
self.cleanup()
self.delegate?.classifier(error: message)
}
}
private func report() {
guard delegate != nil else {
error("No delegate")
return
}
DispatchQueue.main.async {
let img = self.notify ? self.image : nil
self.cleanup()
self.delegate?.classifier(finished: img)
}
}
}
extension CGImagePropertyOrientation {
/**
Converts a `UIImageOrientation` to a corresponding
`CGImagePropertyOrientation`. The cases for each
orientation are represented by different raw values.
- Tag: ConvertOrientation
*/
init(_ orientation: UIImage.Orientation) {
switch orientation {
case .up: self = .up
case .upMirrored: self = .upMirrored
case .down: self = .down
case .downMirrored: self = .downMirrored
case .left: self = .left
case .leftMirrored: self = .leftMirrored
case .right: self = .right
case .rightMirrored: self = .rightMirrored
}
}
}

746
CapCollector/Data/Cap.swift Normal file
View File

@ -0,0 +1,746 @@
//
// Cap.swift
// CapCollector
//
// Created by Christoph on 19.11.18.
// Copyright © 2018 CH. All rights reserved.
//
import Foundation
import UIKit
import SwiftyDropbox
protocol CapsDelegate: class {
func capHasUpdates(_ cap: Cap)
func capsLoaded()
}
final class Cap {
// MARK: - Static variables
static let jpgQuality: CGFloat = 0.3
private static let mosaicColumns = 40
static let mosaicCellSize: CGFloat = 60
private static let mosaicRowHeight = mosaicCellSize * 0.866
private static let mosaicMargin = mosaicCellSize - mosaicRowHeight
static var delegate: CapsDelegate?
static var shouldSave = true {
didSet {
save()
}
}
static var hasMatches = false {
didSet {
guard !hasMatches else { return }
all.forEach { _, cap in
cap.match = nil
}
}
}
static var nextUnusedId: Int {
return (all.keys.max() ?? 0) + 1
}
/// The number of caps currently in the database
static var totalCapCount: Int {
return all.count
}
/// The total number of images for all caps
static var imageCount: Int {
return all.reduce(0) { sum, cap in
return sum + cap.value.count
}
}
/**
Match all cap names against the given string and return matches.
- note: Each space-separated part of the string is matched individually
*/
static func caps(matching text: String) -> [Cap] {
let cleaned = text.clean
let found = all.compactMap { (_,cap) -> Cap? in
// For each part of text, check if name contains it
for textItem in cleaned.components(separatedBy: " ") {
if textItem != "" && !cap.name.contains(textItem) { return nil }
}
return cap
}
return found
}
// MARK: - Variables
/// The unique number of the cap
let id: Int
/// The tile position of the cap
var tile: Int
/// The name of the cap
var name: String {
didSet {
cleanName = name.clean
Cap.save()
event("Updated name for cap \(id) to \(name)")
Cap.delegate?.capHasUpdates(self)
}
}
/// The name of the cap wothout special characters
private(set) var cleanName: String
/// The number of images existing for the cap
private(set) var count: Int {
didSet {
Cap.save()
event("Updated count for cap \(id) to \(count)")
Cap.delegate?.capHasUpdates(self)
}
}
/// The similarity of the cap to the currently processed image
var match: Float? = nil
// MARK: - All caps
/// A dictionary of all known caps
static var all = [Int : Cap]()
// MARK: - Tile information
/// A dictionary of the caps for the tiles
static var tiles = [Int : Cap]()
/**
Get the cap image for a tile.
*/
static func tileImage(tile: Int) -> UIImage? {
return tiles[tile]?.thumbnail
}
/**
Switch two tiles.
*/
static func switchTiles(_ lhs: Int, _ rhs: Int) {
let l = tiles[lhs]!
let r = tiles[rhs]!
l.tile = rhs
r.tile = lhs
tiles[rhs] = l
tiles[lhs] = r
event("Switched tiles \(lhs) and \(rhs)")
}
// MARK: - Initialization
/**
Create a new cap with an image
- parameter image: The main image of the cap
- parameter name: The name of the cap
*/
init?(image: UIImage, name: String) {
self.id = Cap.nextUnusedId
self.tile = id - 1
self.name = name
self.count = 1
self.cleanName = name.clean
guard save(mainImage: image) else {
return nil
}
upload(mainImage: image) { success in
guard success else { return }
Cap.all[self.id] = self
Cap.tiles[self.id] = self
Cap.save()
Cap.updateMosaicWithNewCap(id: self.id, image)
Cap.delegate?.capHasUpdates(self)
}
}
/**
Create a cap from a line in the cap list file
*/
init?(line: String) {
guard line != "" else {
return nil
}
let parts = line.components(separatedBy: ";")
guard parts.count == 4 else {
Cap.error("Cap names: Invalid line \(line)")
return nil
}
guard let nr = Int(parts[0]) else {
Cap.error("Invalid id in line \(line)")
return nil
}
guard let count = Int(parts[2]) else {
Cap.error("Invalid count in line \(line)")
return nil
}
guard let tile = Int(parts[3]) else {
Cap.error("Invalid tile in line \(line)")
return nil
}
self.id = nr
self.name = parts[1]
self.count = count
self.cleanName = name.clean
self.tile = tile
Cap.tiles[tile] = self
Cap.all[id] = self
}
// MARK: - Images
/// The main image of the cap
var image: UIImage? {
guard let data = DiskManager.image(for: id) else {
return nil
}
return UIImage(data: data)
}
/// The main image of the cap
var thumbnail: UIImage? {
if let data = DiskManager.thumbnail(for: id) {
return UIImage(data: data)
}
return makeThumbnail()
}
@discardableResult
func makeThumbnail() -> UIImage? {
guard let img = image else {
return nil
}
let len = GridViewController.len * 2
let thumb = img.resize(to: CGSize.init(width: len, height: len))
guard let data = thumb.pngData() else {
error("Failed to get PNG data from thumbnail for cap \(id)")
return nil
}
_ = DiskManager.save(thumbnailData: data, for: id)
event("Created thumbnail for cap \(id)")
return thumb
}
/**
Download a specified image of the cap.
- Note: If the downloaded image is the main image, it is automatically saved to disk
- Note: If the main image is requested and already downloaded, it is returned directly
- parameter number: The number of the image
- parameter completion: The completion handler, called with the image if successful
- parameter image: The image, if the download was successful, or nil on error
*/
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
if number == 0, let image = self.image {
event("Main image for cap \(id) already downloaded")
completion(image)
return
}
let path = "/Images/\(id)/\(id)-\(number).jpg"
DropboxController.client.files.download(path: path).response { data, dbError in
if let error = dbError {
self.error("Failed to download image data (\(number)) for cap \(self.id): \(error)")
completion(nil)
return
}
guard let d = data?.1 else {
self.error("Failed to download image data (\(number)) for cap \(self.id)")
completion(nil)
return
}
guard let image = UIImage(data: d) else {
self.error("Corrupted image data (\(number)) for cap \(self.id)")
completion(nil)
return
}
if number == 0 {
guard self.save(mainImage: image) else {
completion(nil)
return
}
}
self.event("Downloaded image data (\(number)) for cap \(self.id)")
completion(image)
}
}
func save(mainImage: UIImage) -> Bool {
guard let data = mainImage.jpegData(compressionQuality: Cap.jpgQuality) else {
error("Failed to convert image to data")
return false
}
guard DiskManager.save(imageData: data, for: id) else {
error("Failed to save main image for cap \(id)")
return false
}
event("Saved main image for cap \(id) to disk")
guard let _ = makeThumbnail() else {
return true
}
Cap.delegate?.capHasUpdates(self)
return true
}
func add(image: UIImage, completion: @escaping (Bool) -> Void) {
upload(image: image) { saved in
guard saved else {
completion(false)
return
}
// Increment cap count
self.count += 1
completion(true)
}
}
// MARK: - Image upload
private func upload(mainImage: UIImage, completion: @escaping (_ success: Bool) -> Void) {
self.createFolder { created in
guard created else { return }
self.upload(image: mainImage, number: 0, savedCallback: completion)
}
}
private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) {
let path = "/Images"
DropboxController.client.files.listFolder(path: path).response { response, error in
if let e = error {
self.error("Failed to get image folder list: \(e)")
completion(nil)
return
}
guard let result = response else {
self.error("Failed to get image folder list")
completion(nil)
return
}
let exists = result.entries.contains { $0.name == "\(self.id)" }
completion(exists)
}
}
private func createFolder(completion: @escaping (_ success: Bool) -> Void) {
// Create folder for cap
let path = "/Images/\(id)"
DropboxController.client.files.createFolderV2(path: path).response { _, error in
if let err = error {
self.event("Could not create folder for new cap \(self.id): \(err)")
completion(false)
} else {
self.event("Created folder for new cap \(self.id)")
completion(true)
}
}
}
private func upload(image: UIImage, savedCallback: @escaping (Bool) -> Void) {
upload(image: image, number: count, savedCallback: savedCallback)
}
private func upload(image: UIImage, number: Int, savedCallback: @escaping (Bool) -> Void) {
// Convert to data
guard let data = image.jpegData(compressionQuality: Cap.jpgQuality) else {
error("Failed to convert image to data")
return
}
let fileName = "\(id)-\(number).jpg"
// Save image to upload folder
guard let url = DiskManager.saveForUpload(imageData: data, name: fileName) else {
error("Could not save image for cap \(id) to upload folder")
savedCallback(false)
return
}
event("Saved image \(number) for cap \(id) for upload")
savedCallback(true)
Cap.upload(url: url) { success in
}
}
private static func upload(url: URL, completion: @escaping (Bool) -> Void) {
let cap = Int(url.lastPathComponent.components(separatedBy: "-").first!)!
let path = "/Images/\(cap)/" + url.lastPathComponent
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
self.error("Could not read data from url \(url.path): \(error)")
completion(false)
return
}
DropboxController.client.files.upload(path: path, input: data).response { response, error in
if let err = error {
self.error("Failed to upload file at url: \(url): \(err)")
completion(false)
return
}
Cap.event("Uploaded image \(path)")
guard DiskManager.removeFromUpload(url: url) else {
self.error("Could not delete uploaded image for cap \(cap) at url \(url)")
completion(false)
return
}
completion(true)
}
}
static func uploadRemainingImages() {
guard let list = DiskManager.pendingUploads else {
return
}
guard list.count != 0 else {
event("No pending uploads")
return
}
event("\(list.count) image uploads pending")
for url in list {
upload(url: url) { didUpload in
// Delete image from disk if uploaded
guard didUpload else {
self.error("Could not upload image at url \(url)")
return
}
guard DiskManager.removeFromUpload(url: url) else {
self.error("Could not delete uploaded image at url \(url)")
return
}
}
}
}
// MARK: - Counts
func updateCount(completion: @escaping (Bool) -> Void) {
getImageCount { response in
guard let count = response else {
self.error("Could not update count for cap \(self.id)")
completion(false)
return
}
self.count = count
completion(true)
}
}
private func getImageCount(completion: @escaping (Int?) -> Void) {
DropboxController.client.files.listFolder(path: "/Images/\(id)").response { response, error in
if let err = error {
self.error("Error getting folder content of cap \(self.id): \(err)")
completion(nil)
return
}
guard let files = response?.entries else {
self.error("No content for folder of cap \(self.id)")
completion(nil)
return
}
completion(files.count)
}
}
// MARK: - Sorted caps
static var unsortedCaps: Set<Cap> {
return Set(all.values)
}
static func capList(sortedBy criteria: SortCriteria, ascending: Bool) -> [Cap] {
if ascending {
return sorted([Cap](all.values), ascendingBy: criteria)
} else {
return sorted([Cap](all.values), descendingBy: criteria)
}
}
private static func sorted(_ list: [Cap], ascendingBy parameter: SortCriteria) -> [Cap] {
switch parameter {
case .id: return list.sorted { $0.id < $1.id }
case .count: return list.sorted { $0.count < $1.count }
case .name: return list.sorted { $0.name < $1.name }
case .match: return list.sorted { $0.match ?? 0 < $1.match ?? 0 }
}
}
private static func sorted(_ list: [Cap], descendingBy parameter: SortCriteria) -> [Cap] {
switch parameter {
case .id: return list.sorted { $0.id > $1.id }
case .count: return list.sorted { $0.count > $1.count }
case .name: return list.sorted { $0.name > $1.name }
case .match: return list.sorted { $0.match ?? 0 > $1.match ?? 0 }
}
}
// MARK: - Loading, Saving & Uploading cap list
/**
Either load the names from disk or download them from dropbox.
- parameter completion: The handler that is called with true on success, false on failure
*/
static func load() {
NameFile.makeAvailable { content in
guard let lines = content else {
return
}
self.readNames(from: lines)
DispatchQueue.main.async {
createAndSaveMosaic()
}
}
}
/// Read all caps from the content of a file
private static func readNames(from fileContent: String) {
let parts = fileContent.components(separatedBy: "\n")
for line in parts {
_ = Cap(line: line)
}
event("Loaded \(totalCapCount) caps from file")
delegate?.capsLoaded()
}
static func getCapStatistics() -> [Int] {
let counts = all.values.map { $0.count }
var c = [Int](repeating: 0, count: counts.max()! + 1)
counts.forEach { c[$0] += 1 }
return c
}
static func save() {
guard shouldSave else { return }
let content = namesAsString()
NameFile.save(names: content)
}
static func saveAndUpload(completion: @escaping (Bool) -> Void) {
let content = namesAsString()
NameFile.saveAndUpload(names: content) { success in
guard success else {
completion(false)
return
}
Persistence.lastUploadedCapCount = totalCapCount
Persistence.lastUploadedImageCount = imageCount
completion(true)
}
}
private static func namesAsString() -> String {
return capList(sortedBy: .id, ascending: true).reduce("") { $0 + $1.description }
}
// MARK: - GridView
private static func size(for tiles: Int) -> CGSize {
let columns = CGFloat(mosaicColumns)
// Add half of a cell due to row shift
let width = (columns + 0.5) * mosaicCellSize
let rows = (CGFloat(tiles) / columns).rounded(.up)
// Add margin because the last row does not overlap
let height = rows * mosaicRowHeight + mosaicMargin
return CGSize(width: width, height: height)
}
static func origin(for tile: Int) -> CGPoint {
let row = tile / mosaicColumns
let column = tile - row * mosaicColumns
let x = ( CGFloat(column) + (row.isEven ? 0 : 0.5) ) * mosaicCellSize
let y = CGFloat(row) * mosaicRowHeight
return CGPoint(x: x, y: y)
}
private static func makeTile(_ tile: Int, image: UIImage) -> RoundedImageView {
let point = origin(for: tile)
let frame = CGRect(origin: point, size: CGSize(width: mosaicCellSize, height: mosaicCellSize))
let view = RoundedImageView(frame: frame)
view.image = image
return view
}
private static func makeMosaicCanvas() -> UIView {
let view = UIView(frame: CGRect(origin: .zero, size: size(for: Cap.totalCapCount)))
view.backgroundColor = UIColor(red: 36/255, green: 36/255, blue: 36/255, alpha: 1)
return view
}
private static func makeMosaic() -> UIImage? {
let canvas = makeMosaicCanvas()
for cap in Cap.all.values {
if let img = cap.image {
let view = makeTile(cap.id - 1, image: img)
canvas.addSubview(view)
} else {
error("No image for cap \(cap.id)")
}
}
return render(view: canvas)
}
private static func render(view: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
defer { UIGraphicsEndImageContext() }
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
return UIGraphicsGetImageFromCurrentImageContext()
}
private static func createAndSaveMosaic() {
guard !DiskManager.mosaicExists else {
event("Mosaic already created")
return
}
updateMosaic()
}
static var mosaic: UIImage? {
guard let data = DiskManager.mosaicData else {
error("No mosaic data on disk")
return nil
}
guard let image = UIImage(data: data, scale: UIScreen.main.scale) else {
error("Failed to create image from mosaic data")
return nil
}
return image
}
private static func updateMosaicWithNewCap(id: Int, _ image: UIImage) {
guard let old = mosaic else { return }
let view = UIImageView(image: old)
let canvas = makeMosaicCanvas()
let tile = makeTile(id - 1, image: image)
canvas.addSubview(view)
canvas.addSubview(tile)
guard let img = render(view: canvas) else {
error("Failed to update mosaic for cap \(id)")
return
}
saveMosaic(img)
}
static func tile(for point: CGPoint) -> Int? {
let s = point.y.truncatingRemainder(dividingBy: mosaicRowHeight)
let row = Int(point.y / mosaicRowHeight)
guard s > mosaicMargin else {
return nil
}
let column: CGFloat
if row.isEven {
column = point.x / mosaicCellSize
} else {
column = (point.x - mosaicCellSize / 2) / mosaicCellSize
}
return row * mosaicColumns + Int(column)
}
static func switchTilesInMosaic(_ mosaic: UIImageView, tile1: Int, tile2: Int) {
let tileView1 = makeTile(tile1, image: Cap.tileImage(tile: tile1)!)
let tileView2 = makeTile(tile2, image: Cap.tileImage(tile: tile2)!)
mosaic.addSubview(tileView1)
mosaic.addSubview(tileView2)
defer {
tileView1.removeFromSuperview()
tileView2.removeFromSuperview()
}
guard let img = render(view: mosaic) else {
error("Failed to switch \(tile1) and \(tile2) in mosaic")
return
}
mosaic.image = img
saveMosaic(img)
}
static func updateMosaic() {
guard let image = makeMosaic() else {
error("No mosaik image created")
return
}
saveMosaic(image)
}
static func saveMosaic(_ image: UIImage) {
guard let data = image.pngData() else {
error("Failed to convert mosaic to data")
return
}
guard DiskManager.saveMosaicData(data) else {
error("Failed to write mosaic to disk")
return
}
event("Mosaic saved")
}
}
// MARK: - Protocol Hashable
extension Cap: Hashable {
static func == (lhs: Cap, rhs: Cap) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// MARK: - Protocol CustomStringConvertible
extension Cap: CustomStringConvertible {
var description: String {
return "\(id);\(name);\(count);\(tile)\n"
}
}
// MARK: - Protocol Logger
extension Cap: Logger {
static let logToken = "[CAP]"
}
// MARK: - String extension
extension String {
var clean: String {
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
}
}
// MARK: - Int extension
private extension Int {
var isEven: Bool {
return self % 2 == 0
}
}

View File

@ -0,0 +1,214 @@
//
// DiskManager.swift
// CapFinder
//
// Created by User on 23.04.18.
// Copyright © 2018 User. All rights reserved.
//
import Foundation
import UIKit
final class DiskManager {
enum LocalDirectory: String {
/// Folder for new images to upload
case upload = "Upload"
/// Folder for downloaded images
case images = "Images"
/// Folder for downloaded images
case thumbnails = "Thumbnails"
/// Directory for name file
case files = "Files"
private static let fm = FileManager.default
/// The url to the file sstem
var url: URL {
return URL(fileURLWithPath: self.rawValue, isDirectory: true, relativeTo: DiskManager.documentsDirectory)
}
fileprivate func create() -> Bool {
return DiskManager.create(directory: url)
}
var content: [URL]? {
do {
return try LocalDirectory.fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
} catch {
print("[LocalDirectory] Could not read directory \(self.rawValue): \(error)")
return nil
}
}
}
/// The folder where images and name list are stored
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
private static let fm = FileManager.default
// MARK: - First launch
@discardableResult static func setupOnFirstLaunch() -> Bool {
return LocalDirectory.files.create() &&
LocalDirectory.images.create() &&
LocalDirectory.thumbnails.create() &&
LocalDirectory.upload.create()
}
private static func create(directory: URL) -> Bool {
do {
if !fm.fileExists(atPath: directory.path) {
try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
}
return true
} catch {
event("Could not create \(directory): \(error)")
return false
}
}
// MARK: - Image retrieval
/**
Check if an image exists for a cap
- parameter cap: The id of the cap
- returns: True, if an image exists
*/
static func hasImage(for cap: Int) -> Bool {
let url = localUrl(for: cap)
return fm.fileExists(atPath: url.path)
}
private static func localUrl(for cap: Int) -> URL {
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url)
}
private static func thumbnailUrl(for cap: Int) -> URL {
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.thumbnails.url)
}
/**
Get the image for a cap.
If the image exists on disk, it is returned.
If no image exists locally, then this function returns nil.
- parameter cap: The id of the cap
- returns: The image data, or `nil`
*/
static func image(for cap: Int) -> Data? {
// If the image exists on disk, get it
let url = localUrl(for: cap)
return readData(from: url)
}
private static let mosaicURL: URL = documentsDirectory.appendingPathComponent("mosaic.png")
static var mosaicExists: Bool {
return fm.fileExists(atPath: mosaicURL.path)
}
static func saveMosaicData(_ data: Data) -> Bool {
return write(data, to: mosaicURL)
}
static var mosaicData: Data? {
return readData(from: mosaicURL)
}
/**
Get the thumbnail for a cap.
If the image exists on disk, it is returned.
If no image exists locally, then this function returns nil.
- parameter cap: The id of the cap
- returns: The image data, or `nil`
*/
static func thumbnail(for cap: Int) -> Data? {
// If the image exists on disk, get it
let url = thumbnailUrl(for: cap)
return readData(from: url)
}
private static func readData(from url: URL) -> Data? {
guard fm.fileExists(atPath: url.path) else {
return nil
}
do {
return try Data(contentsOf: url)
} catch {
self.error("Could not read data from \(url): \(error)")
return nil
}
}
/**
Save an image to the download folder
- parameter imageData: The data of the image
- parameter cap: The cap id
- returns: True, if the image was saved
*/
static func save(imageData: Data, for cap: Int) -> Bool {
let url = localUrl(for: cap)
return write(imageData, to: url)
}
/**
Save a thumbnail to the download folder
- parameter thumbnailData: The data of the image
- parameter cap: The cap id
- returns: True, if the image was saved
*/
static func save(thumbnailData: Data, for cap: Int) -> Bool {
let url = thumbnailUrl(for: cap)
return write(thumbnailData, to: url)
}
private static func write(_ data: Data, to url: URL) -> Bool {
do {
try data.write(to: url)
} catch {
self.error("Could not write data to \(url): \(error)")
return false
}
return true
}
static func removeFromUpload(url: URL) -> Bool {
guard fm.fileExists(atPath: url.path) else {
return true
}
do {
try fm.removeItem(at: url)
return true
} catch {
self.error("Could not delete file \(url.path)")
return false
}
}
static var pendingUploads: [URL]? {
return LocalDirectory.upload.content
}
static var availableImages: [Int]? {
return LocalDirectory.images.content?.compactMap {
Int($0.lastPathComponent.components(separatedBy: ".").first ?? "")
}
}
/**
Save an image to the uploads folder for later
*/
static func saveForUpload(imageData: Data, name: String) -> URL? {
let url = LocalDirectory.upload.url.appendingPathComponent(name)
return write(imageData, to: url) ? url : nil
}
}
extension DiskManager: Logger {
static let logToken = "[DiskManager]"
}

View File

@ -0,0 +1,145 @@
//
// NameFile.swift
// CapFinder
//
// Created by User on 23.04.18.
// Copyright © 2018 User. All rights reserved.
//
import Foundation
import SwiftyDropbox
final class NameFile: Logger {
static let logToken = "[NameFile]"
/// The name of the file
private static let fileName = "names.txt"
private static let path = "/" + fileName
/// The url of the file on disk
private static let url = DiskManager.documentsDirectory.appendingPathComponent(fileName)
private static let fm = FileManager.default
// MARK: - Reading from disk
/// Indicates if the name list was written to disk
private static var nameFileExistsOnDisk: Bool {
return fm.fileExists(atPath: url.path)
}
// MARK: - Public API
@discardableResult static func save(names: String) -> Bool {
let data = names.data(using: .utf8)!
return save(names: data)
}
static func saveAndUpload(names: String, completion: @escaping (Bool) -> Void) {
let data = names.data(using: .utf8)!
guard save(names: data) else {
return
}
let client = DropboxController.client
client.files.upload(path: path, mode: .overwrite, input: data).response { _ , error in
if let error = error {
self.error("Error uploading name list: \(error)")
completion(false)
} else {
self.event("Uploaded name list")
completion(true)
}
}
}
// MARK: - Private
/// The content of the name file as a String
private static var content: String? {
do {
return try String(contentsOf: url, encoding: .utf8)
} catch {
self.error("Error reading \(url): \(error)")
return nil
}
}
/**
Save the name file to disk
- parameter names: The new name file content
- returns: True, if the data was written to disk
*/
@discardableResult private static func save(names: Data) -> Bool {
do {
try names.write(to: url, options: .atomic)
event("Name file saved to disk")
return true
} catch {
self.error("Could not save names to file: \(error)")
return false
}
}
static func makeAvailable(completion: ((String?) -> Void)? = nil) {
if nameFileExistsOnDisk {
completion?(self.content)
} else {
download() { success in
guard success else {
completion?(nil)
return
}
completion?(self.content)
}
}
}
/// The data of the name list
private static var data: Data? {
guard nameFileExistsOnDisk else { return nil }
do {
return try Data(contentsOf: url)
} catch {
self.error("Could not read data from \(url): \(error)")
return nil
}
}
/**
Delete the file on disk
- returns: True, if the file no longer exists on disk
*/
@discardableResult private static func delete() -> Bool {
guard nameFileExistsOnDisk else {
event("No name file to delete")
return true
}
do {
try fm.removeItem(at: url)
} catch {
self.error("Could not delete name file: \(error)")
return false
}
event("Deleted name file on disk")
return true
}
private static func download(completion: ((Bool) -> Void)? = nil) {
let client = DropboxController.client
event("Downloading names from Dropbox")
client.files.download(path: path).response { response, error in
guard let data = response?.1 else {
self.error("Error downloading file: \(error!)")
completion?(false)
return
}
self.event("Downloaded name file")
completion?(NameFile.save(names: data))
}
}
}

View File

@ -28,21 +28,51 @@ final class Persistence {
}
}
static var notUploadedCapCount: Int {
static var lastUploadedCapCount: Int {
get {
return UserDefaults.standard.integer(forKey: "notUploadedCaps")
return UserDefaults.standard.integer(forKey: "lastUploadedCaps")
}
set {
UserDefaults.standard.set(newValue, forKey: "notUploadedCaps")
UserDefaults.standard.set(newValue, forKey: "lastUploadedCaps")
}
}
static var notUploadedImageCount: Int {
static var lastUploadedImageCount: Int {
get {
return UserDefaults.standard.integer(forKey: "notUploadedImages")
return UserDefaults.standard.integer(forKey: "lastUploadedImages")
}
set {
UserDefaults.standard.set(newValue, forKey: "notUploadedImages")
UserDefaults.standard.set(newValue, forKey: "lastUploadedImages")
}
}
static var squeezenet: Bool {
get {
return UserDefaults.standard.bool(forKey: "squeezenet")
}
set {
UserDefaults.standard.set(newValue, forKey: "squeezenet")
}
}
static var resnet: Bool {
get {
return UserDefaults.standard.bool(forKey: "resnet")
}
set {
UserDefaults.standard.set(newValue, forKey: "resnet")
}
}
static var xcode: Bool {
get {
return UserDefaults.standard.bool(forKey: "xcode")
}
set {
UserDefaults.standard.set(newValue, forKey: "xcode")
}
}
}

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.6</string>
<string>1.3</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@ -40,6 +40,24 @@
<true/>
<key>NSCameraUsageDescription</key>
<string>Take images to identify matching caps and register new ones</string>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeCapturePhoto</string>
<key>UIApplicationShortcutItemSubtitle</key>
<string>Compare a new image</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Take image</string>
<key>UIApplicationShortcutItemType</key>
<string>firstShortcut</string>
<key>UIApplicationShortcutItemUserInfo</key>
<dict>
<key>firstShortcutKey1</key>
<string>firstShortcutKeyValue1</string>
</dict>
</dict>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -53,6 +71,8 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>

35
CapCollector/Logger.swift Normal file
View File

@ -0,0 +1,35 @@
//
// Logger.swift
// CapFinder
//
// Created by User on 11.04.18.
// Copyright © 2018 User. All rights reserved.
//
import Foundation
protocol Logger {
static var logToken: String { get }
}
extension Logger {
func error(_ message: String) {
print(Self.logToken + " ERROR: " + message)
}
func event(_ message: String) {
print(Self.logToken + " " + message)
}
static func error(_ message: String) {
print(logToken + " ERROR: " + message)
}
static func event(_ message: String) {
print(logToken + " " + message)
}
}

View File

@ -0,0 +1,46 @@
//
// CapCell.swift
// CapFinder
//
// Created by User on 22.04.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
class CapCell: UITableViewCell {
@IBOutlet private weak var capImage: RoundedImageView!
@IBOutlet private weak var matchLabel: UILabel!
@IBOutlet private weak var nameLabel: UILabel!
@IBOutlet weak var countLabel: UILabel!
var id = 0
var cap: Cap! {
didSet {
updateCell()
}
}
func updateCell() {
capImage.image = cap.image
//capImage.borderColor = AppDelegate.tintColor
matchLabel.text = text(for: cap.match)
nameLabel.text = cap.name
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
}
private func text(for value: Float?) -> String? {
guard let nr = value else {
return nil
}
let percent = Int((nr * 100).rounded())
return String(format: "%d %%", arguments: [percent])
}
}

View File

@ -0,0 +1,273 @@
//
// GridViewController.swift
// CapCollector
//
// Created by Christoph on 07.01.19.
// Copyright © 2019 CH. All rights reserved.
//
import UIKit
class GridViewController: UIViewController {
private let columns = 40
static let len: CGFloat = 60
private lazy var rowHeight = GridViewController.len * 0.866
private lazy var margin = GridViewController.len - rowHeight
private var myView: UIView!
private var canvasSize: CGSize = .zero
@IBOutlet weak var scrollView: UIScrollView!
private var selectedTile: Int? = nil
private weak var selectionView: RoundedButton!
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
let height = (CGFloat(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
canvasSize = CGSize(width: width, height: height)
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
scrollView.addSubview(myView)
scrollView.contentSize = canvasSize
scrollView.delegate = self
scrollView.zoomScale = 0.5
scrollView.maximumZoomScale = 1
setZoomRange()
/*
guard let image = Cap.mosaic else {
error("No mosaic")
return
}
imageView.image = image
imageHeight.constant = image.size.height
imageWidth.constant = image.size.width
scrollView.contentSize = image.size
let button = RoundedButton(frame: CGRect(origin: .zero, size: CGSize(width: Cap.mosaicCellSize, height: Cap.mosaicCellSize)))
imageView.addSubview(button)
selectionView = button
button.borderColor = AppDelegate.tintColor
button.borderWidth = 3
button.isHidden = true
scrollView.delegate = self
scrollView.zoomScale = 1
scrollView.maximumZoomScale = 1
setZoomRange()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
scrollView.addGestureRecognizer(tapRecognizer)
event("did load")
*/
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateTiles()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
myView.addGestureRecognizer(tapRecognizer)
}
override func viewDidLayoutSubviews() {
setZoomRange()
updateTiles()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
Cap.save()
}
private func setZoomRange() {
let size = scrollView.frame.size
let a = size.width / canvasSize.width
let b = size.height / canvasSize.height
let scale = min(a,b)
scrollView.minimumZoomScale = min(a,b)
if scrollView.zoomScale < scale {
scrollView.setZoomScale(scale, animated: true)
}
}
@objc func handleTap(_ sender: UITapGestureRecognizer) {
let loc = sender.location(in: myView)
let y = loc.y
let s = y.truncatingRemainder(dividingBy: rowHeight)
let row = Int(y / rowHeight)
guard s > margin else {
return
}
let column: CGFloat
if row.isEven {
column = loc.x / GridViewController.len
} else {
column = (loc.x - GridViewController.len / 2) / GridViewController.len
}
handleTileTapped(tile: row * columns + Int(column))
/*
event("Tapped")
let loc = sender.location(in: imageView)
guard let tile = Cap.tile(for: loc) else {
event("No tile for location \(loc)")
return
}
handleTileTapped(tile: tile)
*/
}
private func handleTileTapped(tile: Int) {
if let selected = selectedTile {
switchTiles(oldTile: selected, newTile: tile)
} else {
showSelection(tile: tile)
}
}
/*
private func showSelection(tile: Int) {
event("Selecting tile \(tile)")
let point = Cap.origin(for: tile)
selectionView.frame = CGRect(origin: point, size: selectionView.frame.size)
selectionView.isHidden = false
selectedTile = tile
}
private func switchTiles(oldTile: Int, newTile: Int) {
event("Switching tiles \(oldTile) and \(newTile)")
selectionView.isHidden = true
selectedTile = nil
guard oldTile != newTile else {
return
}
Cap.switchTiles(oldTile, newTile)
Cap.switchTilesInMosaic(imageView, tile1: oldTile, tile2: newTile)
}
*/
private var installedTiles = [Int : RoundedImageView]()
private func showSelection(tile: Int) {
clearTileSelection()
if let view = installedTiles[tile] {
view.borderWidth = 3
view.borderColor = AppDelegate.tintColor
selectedTile = tile
} else {
selectedTile = nil
}
}
private func tileIsVisible(tile: Int, in rect: CGRect) -> Bool {
return rect.intersects(frame(for: tile))
}
private func makeTile(_ tile: Int) {
let view = RoundedImageView(frame: frame(for: tile))
myView.addSubview(view)
view.image = Cap.tileImage(tile: tile)
installedTiles[tile] = view
}
private func frame(for tile: Int) -> CGRect {
let row = tile / columns
let column = tile - row * columns
let x = CGFloat(column) * GridViewController.len + (row.isEven ? 0 : GridViewController.len / 2)
let y = CGFloat(row) * rowHeight
return CGRect(x: x, y: y, width: GridViewController.len, height: GridViewController.len)
}
private func switchTiles(oldTile: Int, newTile: Int) {
if oldTile != newTile {
Cap.switchTiles(oldTile, newTile)
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
}
clearTileSelection()
}
private func clearTileSelection() {
guard let tile = selectedTile else {
return
}
installedTiles[tile]?.borderWidth = 0
selectedTile = nil
}
private func showTiles(in rect: CGRect) {
for i in 0..<Cap.totalCapCount {
if tileIsVisible(tile: i, in: rect) {
if installedTiles[i] != nil {
continue
}
makeTile(i)
} else if let tile = installedTiles[i] {
tile.removeFromSuperview()
installedTiles[i] = nil
}
}
}
private func updateTiles() {
guard #available(iOS 12.0, *) else {
return
}
let scale = scrollView.zoomScale
let offset = scrollView.contentOffset
let size = scrollView.visibleSize
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
showTiles(in: rect)
}
}
extension GridViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return myView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateTiles()
}
}
private extension Int {
var isEven: Bool {
return self % 2 == 0
}
}
extension GridViewController: Logger {
static let logToken: String = "[Grid]"
}

View File

@ -0,0 +1,15 @@
//
// ImageCell.swift
// CapFinder
//
// Created by User on 07.02.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
class ImageCell: UICollectionViewCell {
@IBOutlet weak var capView: UIImageView!
}

View File

@ -0,0 +1,166 @@
//
// ListViewController.swift
// CapFinder
//
// Created by User on 22.02.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
import SwiftyDropbox
class ImageSelector: UIViewController {
// MARK: - Constants
/// The number of items per row
private let itemsPerRow: CGFloat = 3
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return false
}
// MARK: - CollectionView
private var images = [UIImage?]()
var cap: Cap!
@IBOutlet weak var collection: UICollectionView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
collection.dataSource = self
collection.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
downloadImages()
}
// MARK: Image download
private func downloadImages() {
images = [UIImage?](repeating: nil, count: cap.count)
event("\(cap.count) images for cap \(cap.id)")
for number in 0..<cap.count {
cap.downloadImage(number) { image in
self.images[number] = image
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
}
}
}
// MARK: Select
private func selectedImage(nr: Int) {
guard images[nr] != nil else {
return
}
let tempId = cap.count
let tempFile = "/Images/\(cap.id)/\(cap.id)-\(tempId).jpg"
let oldFile = "/Images/\(cap.id)/\(cap.id)-0.jpg"
let newFile = "/Images/\(cap.id)/\(cap.id)-\(nr).jpg"
guard oldFile != newFile else {
return
}
DropboxController.shared.move(file: oldFile, to: tempFile) { success in
guard success else {
self.error("Could not move \(oldFile) to \(tempFile)")
return
}
DropboxController.shared.move(file: newFile, to: oldFile) { success in
guard success else {
self.error("Could not move \(newFile) to \(oldFile)")
return
}
DropboxController.shared.move(file: tempFile, to: newFile) { success in
if !success {
self.error("Could not move \(tempFile) to \(newFile)")
}
self.finish(with: nr)
}
}
}
}
private func finish(with nr: Int) {
let image = images[nr]!
guard cap.save(mainImage: image) else {
return
}
event("Successfully switched image")
}
}
// MARK: - UICollectionViewDataSource
extension ImageSelector: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "Image",
for: indexPath) as! ImageCell
cell.capView.image = images[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedImage(nr: indexPath.row)
navigationController?.popViewController(animated: true)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ImageSelector : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = collectionView.frame.width / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
extension ImageSelector: Logger {
static let logToken = "[ImageSelector]"
}

View File

@ -0,0 +1,33 @@
//
// NavigationController.swift
// CapCollector
//
// Created by Christoph on 08.01.19.
// Copyright © 2019 CH. All rights reserved.
//
import UIKit
class NavigationController: UINavigationController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return allowLandscape ? .allButUpsideDown : .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return allowLandscape
}
var allowLandscape: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}

View File

@ -0,0 +1,239 @@
//
// SettingsController.swift
// CapCollector
//
// Created by Christoph on 15.10.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
class SettingsController: UITableViewController {
@IBOutlet weak var totalCapsLabel: UILabel!
@IBOutlet weak var totalImagesLabel: UILabel!
@IBOutlet weak var recognizedCapsLabel: UILabel!
@IBOutlet weak var imagesStatsLabel: UILabel!
@IBOutlet weak var databaseUpdatesLabel: UILabel!
@IBOutlet weak var dropboxAccountLabel: UILabel!
@IBOutlet weak var countsLabel: UILabel!
private var nameFileChanges = false
private var isUpdatingCounts = false
private var isUpdatingThumbnails = false
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
updateDropboxStatus()
updateNameFileStats()
updateDatabaseStats()
(navigationController as! NavigationController).allowLandscape = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setAccessories()
}
private func updateThumbnails() {
isUpdatingThumbnails = true
for cap in Cap.all.values {
cap.makeThumbnail()
}
isUpdatingThumbnails = false
}
private func updateDatabaseStats() {
let totalCaps = Cap.totalCapCount
totalCapsLabel.text = "\(totalCaps) caps"
totalImagesLabel.text = "\(Cap.imageCount) images"
let recognizedCaps = Persistence.recognizedCapCount
let newCapCount = totalCaps - recognizedCaps
recognizedCapsLabel.text = "\(newCapCount) new caps"
let ratio = Float(Cap.imageCount)/Float(Cap.totalCapCount)
let (images, count) = Cap.getCapStatistics().enumerated().first(where: { $0.element != 0 })!
imagesStatsLabel.text = String(format: "%.2f images per cap, lowest count: %d (%dx)", ratio, images, count)
}
private func updateNameFileStats() {
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
nameFileChanges = capCount > 0 || imageCount > 0
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
}
private func updateDropboxStatus() {
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
}
private func setAccessories() {
tableView.cellForRow(at: IndexPath(row: 0, section: 2))?.accessoryType = Persistence.squeezenet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 1, section: 2))?.accessoryType = Persistence.resnet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 2, section: 2))?.accessoryType = Persistence.xcode ? .checkmark : .none
}
private func toggleClassifier(index: Int) {
switch index {
case 0: Persistence.squeezenet = !Persistence.squeezenet
case 1: Persistence.resnet = !Persistence.resnet
case 2: Persistence.xcode = !Persistence.xcode
default:
return
}
setAccessories()
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section {
case 0: // Mosaic
return true
case 1: // Database
return indexPath.row == 2 && nameFileChanges
case 2: // Choose models
return true
case 3: // Refresh
if indexPath.row == 0 {
return !isUpdatingCounts
} else {
return !isUpdatingThumbnails
}
case 4: // Dropbox account
return true
default: return false
}
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch indexPath.section {
case 0: // Mosaic
return indexPath
case 1: // Database
return (indexPath.row == 2 && nameFileChanges) ? indexPath : nil
case 2: // Choose models
return indexPath
case 3: // Refresh count
if indexPath.row == 0 {
return isUpdatingCounts ? nil : indexPath
} else {
return isUpdatingThumbnails ? nil : indexPath
}
case 4: // Dropbox account
return indexPath
default: return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 1: // Upload
if indexPath.row == 2 && nameFileChanges {
uploadNameFile()
}
case 2: // Choose models
toggleClassifier(index: indexPath.row)
case 3: // Refresh count
if indexPath.row == 0 {
updateCounts()
} else {
updateThumbnails()
}
default:
break
}
}
private func updateCounts() {
isUpdatingCounts = true
Cap.shouldSave = false
// TODO: Don't make all requests at the same time
DispatchQueue.global(qos: .userInitiated).async {
let list = Cap.all.keys.sorted()
let total = list.count
var finished = 0
let chunks = list.chunked(into: 10)
for chunk in chunks {
self.updateCounts(ids: chunk)
finished += 10
DispatchQueue.main.async {
self.countsLabel.text = "\(finished) / \(total) finished"
}
}
self.isUpdatingCounts = false
Cap.shouldSave = true
DispatchQueue.main.async {
self.countsLabel.text = "Refresh image counts"
self.updateDatabaseStats()
}
}
}
private func updateCounts(ids: [Int]) {
var count = ids.count
let s = DispatchSemaphore(value: 0)
for cap in ids {
Cap.all[cap]!.updateCount { _ in
count -= 1
if count == 0 {
s.signal()
}
}
}
_ = s.wait(timeout: .now() + .seconds(30))
event("Finished updating ids \(ids.first!) to \(ids.last!)")
}
private func uploadNameFile() {
Cap.saveAndUpload() { _ in
self.updateNameFileStats()
}
}
private func toggleDropbox() {
guard !DropboxController.shared.isEnabled else {
DropboxController.shared.signOut()
updateDropboxStatus()
return
}
DropboxController.shared.setup(in: self)
updateDropboxStatus()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let id = segue.identifier, id == "showMosaic" else {
return
}
(navigationController as! NavigationController).allowLandscape = true
}
}
extension SettingsController: Logger {
static let logToken = "[Settings]"
}
private extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}

View File

@ -0,0 +1,92 @@
//
// SortController.swift
// CapCollector
//
// Created by Christoph on 12.11.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
enum SortCriteria: Int {
case id = 0
case name = 1
case count = 2
case match = 3
}
protocol SortControllerDelegate {
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
}
class SortController: UITableViewController {
var selected: SortCriteria = .count
var ascending: Bool = true
var delegate: SortControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let height = Cap.hasMatches ? 310 : 270
preferredContentSize = CGSize(width: 200, height: height)
}
private func setCell() {
for i in 0..<4 {
let index = IndexPath(row: i, section: 1)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = i == selected.rawValue ? .checkmark : .none
}
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = ascending ? .checkmark : .none
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setCell()
}
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.section == 1 else {
ascending = !ascending
setCell()
delegate?.sortController(didSelect: selected, ascending: ascending)
giveFeedback(.light)
return
}
giveFeedback(.medium)
selected = SortCriteria(rawValue: indexPath.row)!
delegate?.sortController(didSelect: selected, ascending: ascending)
self.dismiss(animated: true)
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard indexPath.row == 3 else { return indexPath }
return Cap.hasMatches ? indexPath : nil
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}

View File

@ -1,144 +0,0 @@
//
// SettingsController.swift
// CapCollector
//
// Created by Christoph on 15.10.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
class SettingsController: UITableViewController {
@IBOutlet weak var totalCapsLabel: UILabel!
@IBOutlet weak var recognizedCapsLabel: UILabel!
@IBOutlet weak var databaseUpdatesLabel: UILabel!
@IBOutlet weak var dropboxAccountLabel: UILabel!
private var nameFileChanges = false
private var isUpdatingCounts = false
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
updateDropboxStatus()
updateNameFileStats()
updateDatabaseStats()
}
private func updateDatabaseStats() {
let totalCaps = CapNames.shared.count
totalCapsLabel.text = "\(totalCaps) caps, \(CapNames.shared.imageCount) images"
let recognizedCaps = Persistence.recognizedCapCount
let newCapCount = totalCaps - recognizedCaps
recognizedCapsLabel.text = "\(recognizedCaps) recognized, \(newCapCount) new"
}
private func updateNameFileStats() {
let capCount = Persistence.notUploadedCapCount
let imageCount = Persistence.notUploadedImageCount
nameFileChanges = capCount > 0 || imageCount > 0
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
}
private func updateDropboxStatus() {
print("Dropbox enabled: \(DropboxController.shared.isEnabled)")
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section {
case 0: // Database
return false
case 1: // Upload
return nameFileChanges
case 2: // Refresh count
return !isUpdatingCounts
case 3: // Dropbox account
return true
default: return false
}
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch indexPath.section {
case 0: // Database
return nil
case 1: // Upload
return nameFileChanges ? indexPath : nil
case 2: // Refresh count
return isUpdatingCounts ? nil : indexPath
case 3: // Dropbox account
return indexPath
default: return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0:
break
case 1: // Upload
if nameFileChanges {
uploadNameFile()
}
case 2: // Refresh count
updateCounts()
case 3: // Dropbox account
break
default: break
}
}
private func updateCounts() {
isUpdatingCounts = true
CapImages.shared.updateCounts() {
self.isUpdatingCounts = false
self.updateDatabaseStats()
}
}
private func uploadNameFile() {
CapNames.shared.saveAndUpload()
updateNameFileStats()
}
private func toggleDropbox() {
guard !DropboxController.shared.isEnabled else {
DropboxController.shared.signOut()
updateDropboxStatus()
return
}
DropboxController.shared.setup(in: self)
updateDropboxStatus()
}
}
extension SettingsController: DropboxControllerDelegate {
func dropboxControllerDidFinishLoadingCaps() {
}
func dropboxControllerDidLoadNames() {
}
func dropboxController(didLoad caps: [Int]) {
}
func dropboxController(didLoad cap: Int) {
}
}

View File

@ -0,0 +1,89 @@
//
// DropboxController.swift
// CapFinder
//
// Created by User on 08.04.18.
// Copyright © 2018 User. All rights reserved.
//
import Foundation
import SwiftyDropbox
import UIKit
class DropboxController: Logger {
static var logToken = "[Dropbox]"
static var shared = DropboxController()
// MARK: Dropbox API
static var client: DropboxClient {
return DropboxClientsManager.authorizedClient!
}
var isEnabled: Bool {
return DropboxClientsManager.authorizedClient != nil
}
// MARK: - Setup
init() {
}
/** Register dropbox, load names and images.
- parameter viewController: The controller launching the request
*/
func setup(in viewController: UIViewController) {
guard isEnabled == false else {
event("Enabled")
return
}
event("Requesting access")
DropboxClientsManager.authorizeFromController(
UIApplication.shared,
controller: viewController) {
UIApplication.shared.open($0, options: [:])
}
}
func signOut() {
DropboxClientsManager.unlinkClients()
}
/// Process the response of the dropbox registration handler
func handle(url: URL) {
guard let authResult = DropboxClientsManager.handleRedirectURL(url) else {
return
}
switch authResult {
case .success:
event("enabled")
case .cancel:
error("Authorization flow canceled")
case .error(_, let description):
error("Error on authentification: \(description)")
}
}
/// Download name list and cap images
func initializeDatabase() {
guard isEnabled else {
event("Dropbox not enabled")
return
}
Cap.load()
}
func move(file: String, to: String, completion: @escaping (Bool) -> Void) {
DropboxController.client.files.moveV2(fromPath: file, toPath: to).response { _, error in
if let err = error {
self.error("Failed to move file: \(err)")
completion(false)
return
}
completion(true)
}
}
}

View File

@ -0,0 +1,528 @@
//
// 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()
}
}

View File

@ -0,0 +1,42 @@
// Copyright 2018, Ralf Ebert
// License https://opensource.org/licenses/MIT
// License https://creativecommons.org/publicdomain/zero/1.0/
// Source https://www.ralfebert.de/ios-examples/uikit/choicepopover/
import UIKit
/**
By default, when you use:
```
controller.modalPresentationStyle = .popover
```
in a horizontally compact environment (iPhone in portrait mode), this option behaves the same as fullScreen.
You can make it to always show a popover by using:
```
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
```
*/
class AlwaysPresentAsPopover : NSObject, UIPopoverPresentationControllerDelegate {
// `sharedInstance` because the delegate property is weak - the delegate instance needs to be retained.
private static let sharedInstance = AlwaysPresentAsPopover()
private override init() {
super.init()
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
static func configurePresentation(forController controller : UIViewController) -> UIPopoverPresentationController {
controller.modalPresentationStyle = .popover
let presentationController = controller.presentationController as! UIPopoverPresentationController
presentationController.delegate = AlwaysPresentAsPopover.sharedInstance
return presentationController
}
}

View File

@ -0,0 +1,52 @@
//
// CropView.swift
// CapFinder
//
// Created by User on 31.01.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
@IBDesignable class CropView: UIView {
@IBInspectable var lineColor: UIColor = UIColor.black
@IBInspectable var lineWidth: Float = 2
@IBInspectable var relativeSize: Float = 0.6 {
didSet { size = CGFloat(relativeSize) / 2 }
}
private var size: CGFloat = 0.3
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
let height = rect.height
let width = rect.width
let length = height > width ? width : height
let center = CGPoint(x: width / 2, y: height / 2)
let path = UIBezierPath()
path.lineWidth = CGFloat(self.lineWidth)
lineColor.setStroke()
path.addArc(
withCenter: center,
radius: length * size,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
path.stroke()
}
}

Binary file not shown.

8
Podfile Normal file
View File

@ -0,0 +1,8 @@
platform :ios, '11.0'
use_frameworks!
target 'CapCollector' do
# Pods for CapFinder
pod 'SwiftyDropbox'
end

View File

@ -1,12 +1,15 @@
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/SettingsController.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Classifier.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/SortController.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Logger.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropBoxController.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIViewExtensions.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/NavigationController.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Cap.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DiskManager.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIImageExtensions.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapImages.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/TableView.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/GridViewController.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/NameFile.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UserDefaults.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/AppDelegate.o
@ -15,14 +18,13 @@
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ViewControllerExtensions.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/PhotoCaptureHandler.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/RoundedButton.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapNames.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ImageCell.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIAlertControllerExtensions.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CameraView.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CropView.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropboxDownload.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/AlwaysShowPopup.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/RoundedImageView.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropboxUpload.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapCell.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Squeezenet.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ImageClassifier.o
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Resnet.o

View File

@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>LastAccessedDate</key>
<date>2018-10-17T09:43:52Z</date>
<date>2019-03-15T12:04:14Z</date>
<key>WorkspacePath</key>
<string>/Users/christoph/Documents/GitHub/CapCollector/CapCollector.xcworkspace</string>
</dict>

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SuppressMissingCheckoutsAlert</key>
<true/>
<key>Version</key>
<integer>5</integer>
</dict>

View File

@ -1 +1 @@
1539769432.274418: Module build session file for module cache at Path(str: "/Users/christoph/Documents/GitHub/CapCollector/build/ModuleCache.noindex")
1552651454.561245: Module build session file for module cache at Path(str: "/Users/christoph/Documents/GitHub/CapCollector/build/ModuleCache.noindex")