diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc6431b --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +.DS_Store +MyPlayground.playground/ + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +.build/ + +# CocoaPods - Refactored to standalone file + +# Carthage - Refactored to standalone file + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output + +### Xcode ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated + +## Various settings + +## Other + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno diff --git a/PhotoRank.xcodeproj/project.pbxproj b/PhotoRank.xcodeproj/project.pbxproj index 75564d3..88ff483 100644 --- a/PhotoRank.xcodeproj/project.pbxproj +++ b/PhotoRank.xcodeproj/project.pbxproj @@ -7,21 +7,49 @@ objects = { /* Begin PBXBuildFile section */ + D61299E122F8FC7900BC29AD /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D61299E022F8FC7900BC29AD /* Photos.framework */; }; + D63DF79C23188D9000688258 /* RankingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63DF79A23188D9000688258 /* RankingTableViewCell.swift */; }; + D63DF79D23188D9000688258 /* RankingTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D63DF79B23188D9000688258 /* RankingTableViewCell.xib */; }; D66658CB22F655750053041D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66658CA22F655750053041D /* AppDelegate.swift */; }; - D66658CD22F655750053041D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66658CC22F655750053041D /* ViewController.swift */; }; - D66658D022F655750053041D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66658CE22F655750053041D /* Main.storyboard */; }; D66658D222F655760053041D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D66658D122F655760053041D /* Assets.xcassets */; }; D66658D522F655760053041D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D66658D322F655760053041D /* LaunchScreen.storyboard */; }; + D69E1CD422F6561B00FDB494 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69E1CD222F6561B00FDB494 /* RootViewController.swift */; }; + D69E1CD522F6561B00FDB494 /* RootViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D69E1CD322F6561B00FDB494 /* RootViewController.xib */; }; + D69E1CDB22F656D700FDB494 /* AssetCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D69E1CD822F656D700FDB494 /* AssetCollectionViewCell.xib */; }; + D69E1CDC22F656D700FDB494 /* AssetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69E1CD922F656D700FDB494 /* AssetCollectionViewCell.swift */; }; + D6AD654722F65DF9000148A3 /* AssetPickerViewControler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AD654622F65DF9000148A3 /* AssetPickerViewControler.swift */; }; + D6AD654B22F680A2000148A3 /* AssetComparisonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AD654922F680A2000148A3 /* AssetComparisonViewController.swift */; }; + D6AD654C22F680A2000148A3 /* AssetComparisonViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6AD654A22F680A2000148A3 /* AssetComparisonViewController.xib */; }; + D6AD654F22F681B0000148A3 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AD654D22F681B0000148A3 /* LargeImageViewController.swift */; }; + D6AD655022F681B0000148A3 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6AD654E22F681B0000148A3 /* LargeImageViewController.xib */; }; + D6AD655222F68629000148A3 /* PhotosHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AD655122F68629000148A3 /* PhotosHelper.swift */; }; + D6E5189622F8F44C00B4CCA1 /* RankingsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6E5189422F8F44C00B4CCA1 /* RankingsTableViewController.xib */; }; + D6E5189722F8F44C00B4CCA1 /* RankingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E5189522F8F44C00B4CCA1 /* RankingsTableViewController.swift */; }; + D6EBEF5F22F68F19008EA503 /* Array+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBEF5E22F68F19008EA503 /* Array+Helpers.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + D61299E022F8FC7900BC29AD /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + D63DF79A23188D9000688258 /* RankingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankingTableViewCell.swift; sourceTree = ""; }; + D63DF79B23188D9000688258 /* RankingTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RankingTableViewCell.xib; sourceTree = ""; }; D66658C722F655750053041D /* PhotoRank.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoRank.app; sourceTree = BUILT_PRODUCTS_DIR; }; D66658CA22F655750053041D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D66658CC22F655750053041D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - D66658CF22F655750053041D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; D66658D122F655760053041D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D66658D422F655760053041D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; D66658D622F655760053041D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D69E1CD222F6561B00FDB494 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RootViewController.swift; path = ../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/Root/RootViewController.swift; sourceTree = ""; }; + D69E1CD322F6561B00FDB494 /* RootViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = RootViewController.xib; path = ../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/Root/RootViewController.xib; sourceTree = ""; }; + D69E1CD822F656D700FDB494 /* AssetCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AssetCollectionViewCell.xib; sourceTree = ""; }; + D69E1CD922F656D700FDB494 /* AssetCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetCollectionViewCell.swift; sourceTree = ""; }; + D6AD654622F65DF9000148A3 /* AssetPickerViewControler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetPickerViewControler.swift; sourceTree = ""; }; + D6AD654922F680A2000148A3 /* AssetComparisonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AssetComparisonViewController.swift; path = "../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/Asset Comparison/AssetComparisonViewController.swift"; sourceTree = ""; }; + D6AD654A22F680A2000148A3 /* AssetComparisonViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = AssetComparisonViewController.xib; path = "../../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/Asset Comparison/AssetComparisonViewController.xib"; sourceTree = ""; }; + D6AD654D22F681B0000148A3 /* LargeImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = ""; }; + D6AD654E22F681B0000148A3 /* LargeImageViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; + D6AD655122F68629000148A3 /* PhotosHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PhotosHelper.swift; path = ../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/PhotosHelper.swift; sourceTree = ""; }; + D6E5189422F8F44C00B4CCA1 /* RankingsTableViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RankingsTableViewController.xib; sourceTree = ""; }; + D6E5189522F8F44C00B4CCA1 /* RankingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RankingsTableViewController.swift; sourceTree = ""; }; + D6EBEF5E22F68F19008EA503 /* Array+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Array+Helpers.swift"; path = "../../../../../../System/Volumes/Data/Users/shadowfacts/Dev/iOS/PhotoRank/PhotoRank/Array+Helpers.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -29,17 +57,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D61299E122F8FC7900BC29AD /* Photos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D61299DF22F8FC7900BC29AD /* Frameworks */ = { + isa = PBXGroup; + children = ( + D61299E022F8FC7900BC29AD /* Photos.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; D66658BE22F655750053041D = { isa = PBXGroup; children = ( D66658C922F655750053041D /* PhotoRank */, D66658C822F655750053041D /* Products */, + D61299DF22F8FC7900BC29AD /* Frameworks */, ); sourceTree = ""; }; @@ -55,8 +93,12 @@ isa = PBXGroup; children = ( D66658CA22F655750053041D /* AppDelegate.swift */, - D66658CC22F655750053041D /* ViewController.swift */, - D66658CE22F655750053041D /* Main.storyboard */, + D6AD655122F68629000148A3 /* PhotosHelper.swift */, + D6EBEF5E22F68F19008EA503 /* Array+Helpers.swift */, + D69E1CD122F6560C00FDB494 /* Root */, + D69E1CD622F656C800FDB494 /* Asset Picker */, + D6AD654822F65E18000148A3 /* Asset Comparison */, + D6E5189322F8F44C00B4CCA1 /* Final Rankings */, D66658D122F655760053041D /* Assets.xcassets */, D66658D322F655760053041D /* LaunchScreen.storyboard */, D66658D622F655760053041D /* Info.plist */, @@ -64,6 +106,47 @@ path = PhotoRank; sourceTree = ""; }; + D69E1CD122F6560C00FDB494 /* Root */ = { + isa = PBXGroup; + children = ( + D69E1CD222F6561B00FDB494 /* RootViewController.swift */, + D69E1CD322F6561B00FDB494 /* RootViewController.xib */, + ); + path = Root; + sourceTree = ""; + }; + D69E1CD622F656C800FDB494 /* Asset Picker */ = { + isa = PBXGroup; + children = ( + D6AD654622F65DF9000148A3 /* AssetPickerViewControler.swift */, + D69E1CD922F656D700FDB494 /* AssetCollectionViewCell.swift */, + D69E1CD822F656D700FDB494 /* AssetCollectionViewCell.xib */, + ); + path = "Asset Picker"; + sourceTree = ""; + }; + D6AD654822F65E18000148A3 /* Asset Comparison */ = { + isa = PBXGroup; + children = ( + D6AD654922F680A2000148A3 /* AssetComparisonViewController.swift */, + D6AD654A22F680A2000148A3 /* AssetComparisonViewController.xib */, + D6AD654D22F681B0000148A3 /* LargeImageViewController.swift */, + D6AD654E22F681B0000148A3 /* LargeImageViewController.xib */, + ); + path = "Asset Comparison"; + sourceTree = ""; + }; + D6E5189322F8F44C00B4CCA1 /* Final Rankings */ = { + isa = PBXGroup; + children = ( + D6E5189522F8F44C00B4CCA1 /* RankingsTableViewController.swift */, + D6E5189422F8F44C00B4CCA1 /* RankingsTableViewController.xib */, + D63DF79A23188D9000688258 /* RankingTableViewCell.swift */, + D63DF79B23188D9000688258 /* RankingTableViewCell.xib */, + ); + path = "Final Rankings"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -123,8 +206,13 @@ buildActionMask = 2147483647; files = ( D66658D522F655760053041D /* LaunchScreen.storyboard in Resources */, + D6E5189622F8F44C00B4CCA1 /* RankingsTableViewController.xib in Resources */, D66658D222F655760053041D /* Assets.xcassets in Resources */, - D66658D022F655750053041D /* Main.storyboard in Resources */, + D6AD654C22F680A2000148A3 /* AssetComparisonViewController.xib in Resources */, + D69E1CD522F6561B00FDB494 /* RootViewController.xib in Resources */, + D6AD655022F681B0000148A3 /* LargeImageViewController.xib in Resources */, + D63DF79D23188D9000688258 /* RankingTableViewCell.xib in Resources */, + D69E1CDB22F656D700FDB494 /* AssetCollectionViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -135,22 +223,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D66658CD22F655750053041D /* ViewController.swift in Sources */, + D6AD654B22F680A2000148A3 /* AssetComparisonViewController.swift in Sources */, D66658CB22F655750053041D /* AppDelegate.swift in Sources */, + D6E5189722F8F44C00B4CCA1 /* RankingsTableViewController.swift in Sources */, + D6AD654F22F681B0000148A3 /* LargeImageViewController.swift in Sources */, + D63DF79C23188D9000688258 /* RankingTableViewCell.swift in Sources */, + D6EBEF5F22F68F19008EA503 /* Array+Helpers.swift in Sources */, + D69E1CD422F6561B00FDB494 /* RootViewController.swift in Sources */, + D6AD655222F68629000148A3 /* PhotosHelper.swift in Sources */, + D6AD654722F65DF9000148A3 /* AssetPickerViewControler.swift in Sources */, + D69E1CDC22F656D700FDB494 /* AssetCollectionViewCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - D66658CE22F655750053041D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - D66658CF22F655750053041D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; D66658D322F655760053041D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -285,6 +373,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = HGYVAQA9FW; INFOPLIST_FILE = PhotoRank/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -303,6 +392,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = HGYVAQA9FW; INFOPLIST_FILE = PhotoRank/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/PhotoRank/AppDelegate.swift b/PhotoRank/AppDelegate.swift index 48b7793..6b74a7e 100644 --- a/PhotoRank/AppDelegate.swift +++ b/PhotoRank/AppDelegate.swift @@ -15,7 +15,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + window = UIWindow(frame: UIScreen.main.bounds) + window!.rootViewController = UINavigationController(rootViewController: RootViewController()) + window!.makeKeyAndVisible() + return true } diff --git a/PhotoRank/Array+Helpers.swift b/PhotoRank/Array+Helpers.swift new file mode 100644 index 0000000..8a73a02 --- /dev/null +++ b/PhotoRank/Array+Helpers.swift @@ -0,0 +1,17 @@ +// +// Array+Helpers.swift +// PhotoRank +// +// Created by Shadowfacts on 8/3/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import Foundation + +extension Array { + func allPairs() -> [[Element]] { + return self.enumerated().flatMap { (index, first) in + self[index..= 2 else { fatalError("AssetComparisonViewController requires at least 2 assets") } + self.allAssets = assets + self.assetPairs = assets.allPairs() + self.currentAssetPair = assetPairs.first! + + super.init(nibName: "AssetComparisonViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + largeImage.view.translatesAutoresizingMaskIntoConstraints = false + imageContainerView.addSubview(largeImage.view) + NSLayoutConstraint.activate([ + largeImage.view.leadingAnchor.constraint(equalTo: imageContainerView.leadingAnchor), + largeImage.view.topAnchor.constraint(equalTo: imageContainerView.topAnchor), + imageContainerView.trailingAnchor.constraint(equalTo: largeImage.view.trailingAnchor), + imageContainerView.bottomAnchor.constraint(equalTo: largeImage.view.bottomAnchor), + ]) + addChild(largeImage) + + toolbarItems = [ + UIBarButtonItem(title: "Skip", style: .plain, target: self, action: #selector(moveToNextPair)), + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + UIBarButtonItem(title: "Switch", style: .plain, target: self, action: #selector(switchButtonPressed)), + UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), + UIBarButtonItem(title: "Pick", style: .plain, target: self, action: #selector(pickButtonPressed)) + ] + + loadImages() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController!.isToolbarHidden = false + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + navigationController!.isToolbarHidden = true + } + + func loadImages() { + self.currentlySelected = 0 + self.currentImagePair = [nil, nil] + PhotosHelper.requestFullSizeImage(for: currentAssetPair[0]) { (image) in + DispatchQueue.main.async { + self.currentImagePair = [image, self.currentImagePair[1]] + } + } + PhotosHelper.requestFullSizeImage(for: currentAssetPair[1]) { (image) in + DispatchQueue.main.async { + self.currentImagePair = [self.currentImagePair[0], image] + } + } + } + + @objc func switchButtonPressed() { + currentlySelected = currentlySelected == 0 ? 1 : 0 + } + + @objc func moveToNextPair() { + if currentPairIndex == assetPairs.count - 1 { + print("finished: \(assetScores)") + // sorts assets by score in descending order + let sorted = allAssets + .map { ($0, assetScores[$0.localIdentifier] ?? 0) } + .sorted { (first, second) -> Bool in + return first.1 > second.1 + } + let rankings = RankingsTableViewController(rankings: sorted) + show(rankings, sender: nil) + } else { + currentPairIndex += 1 + } + } + + @objc func pickButtonPressed() { + let identifier = currentAssetPair[currentlySelected].localIdentifier + let current = assetScores[identifier] ?? 0 + assetScores[identifier] = current + 1 + + moveToNextPair() + } + +} diff --git a/PhotoRank/Asset Comparison/AssetComparisonViewController.xib b/PhotoRank/Asset Comparison/AssetComparisonViewController.xib new file mode 100644 index 0000000..27e821e --- /dev/null +++ b/PhotoRank/Asset Comparison/AssetComparisonViewController.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/Asset Comparison/LargeImageViewController.swift b/PhotoRank/Asset Comparison/LargeImageViewController.swift new file mode 100644 index 0000000..fb5c190 --- /dev/null +++ b/PhotoRank/Asset Comparison/LargeImageViewController.swift @@ -0,0 +1,149 @@ +// +// LargeImageViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/31/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class LargeImageViewController: UIViewController, UIScrollViewDelegate { + + @IBOutlet weak var scrollView: UIScrollView! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var imageViewLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var imageViewTopConstraint: NSLayoutConstraint! + + var image: UIImage? { + didSet { + imageView.image = image + if oldValue == nil, let image = image { + imageView.bounds = CGRect(origin: .zero, size: image.size) + view.setNeedsLayout() + } + } + } + + var prevZoomScale: CGFloat? + + init() { + super.init(nibName: "LargeImageViewController", bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + +// imageView.image = image + + scrollView.delegate = self +// imageView.bounds = CGRect(origin: .zero, size: imageView.image!.size) + +// view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(scrollViewPressed(_:)))) + let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:))) + doubleTap.numberOfTapsRequired = 2 + view.addGestureRecognizer(doubleTap) + } + + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom + let heightScale = maxHeight / imageView.bounds.height + let widthScale = view.bounds.width / imageView.bounds.width + let minScale = min(widthScale, heightScale) + scrollView.minimumZoomScale = minScale + scrollView.zoomScale = minScale + scrollView.maximumZoomScale = minScale * 4 + + centerImage() + } + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return imageView + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + centerImage() + +// let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale +// if scrollView.zoomScale <= scrollView.minimumZoomScale { +// controlsVisible = true +// } else if scrollView.zoomScale > prevZoomScale { +// controlsVisible = false +// } +// self.prevZoomScale = scrollView.zoomScale + } + + func centerImage() { + let yOffset = max(0, (view.bounds.size.height - imageView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom) / 2) + imageViewTopConstraint.constant = yOffset + + let xOffset = max(0, (view.bounds.size.width - imageView.frame.width - scrollView.contentInset.left - scrollView.contentInset.right) / 2) + imageViewLeadingConstraint.constant = xOffset + } + + func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect { + var zoomRect = CGRect.zero + zoomRect.size.width = imageView.frame.width / scale + zoomRect.size.height = imageView.frame.height / scale + let newCenter = scrollView.convert(center, to: imageView) + zoomRect.origin.x = newCenter.x - (zoomRect.width / 2) + zoomRect.origin.y = newCenter.y - (zoomRect.height / 2) + return zoomRect + } + + func animateZoomOut() { + UIView.animate(withDuration: 0.3, animations: { + self.scrollView.zoomScale = self.scrollView.minimumZoomScale + self.view.layoutIfNeeded() + }) + } + +// @objc func scrollViewPressed(_ sender: UITapGestureRecognizer) { +// if scrollView.zoomScale > scrollView.minimumZoomScale { +// animateZoomOut() +// } else { +//// setControlsVisible(!controlsVisible, animated: true) +// } +// } + + @objc func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) { + if scrollView.zoomScale <= scrollView.minimumZoomScale { + let point = recognizer.location(in: recognizer.view) + let scale: CGFloat + if scrollView.minimumZoomScale < 1 { + if 1 - scrollView.zoomScale <= 0.5 { + scale = scrollView.zoomScale + 1 + } else { + scale = 1 + } + } else { + scale = scrollView.maximumZoomScale + } + let rect = zoomRectFor(scale: scale, center: point) + UIView.animate(withDuration: 0.3) { + self.scrollView.zoom(to: rect, animated: false) + self.view.layoutIfNeeded() + } + } else { + animateZoomOut() + } + } + +// @IBAction func closeButtonPressed(_ sender: Any) { +// dismiss(animated: true) +// } +// +// @IBAction func sharePressed(_ sender: Any) { +// guard let image = image else { return } +// let activityVC = UIActivityViewController(activityItems: [image], applicationActivities: nil) +// present(activityVC, animated: true) +// } + +} diff --git a/PhotoRank/Asset Comparison/LargeImageViewController.xib b/PhotoRank/Asset Comparison/LargeImageViewController.xib new file mode 100644 index 0000000..09fdc16 --- /dev/null +++ b/PhotoRank/Asset Comparison/LargeImageViewController.xib @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/Asset Picker/AssetCollectionViewCell.swift b/PhotoRank/Asset Picker/AssetCollectionViewCell.swift new file mode 100644 index 0000000..8de1cff --- /dev/null +++ b/PhotoRank/Asset Picker/AssetCollectionViewCell.swift @@ -0,0 +1,45 @@ +// +// AssetCollectionViewCell.swift +// PhotoRank +// +// Created by Shadowfacts on 8/3/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +class AssetCollectionViewCell: UICollectionViewCell { + + var assetIdentifier: String! + var thumbnailImage: UIImage? { + didSet { + imageView.image = thumbnailImage + } + } + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var visualEffectView: UIVisualEffectView! + + override var isSelected: Bool { + didSet { + visualEffectView.isHidden = !isSelected + } + } + + override func awakeFromNib() { + super.awakeFromNib() + + visualEffectView.layer.masksToBounds = true + visualEffectView.layer.cornerRadius = 12 + + visualEffectView.isHidden = true + } + + override func prepareForReuse() { + super.prepareForReuse() + + imageView.image = nil + } + +} diff --git a/PhotoRank/Asset Picker/AssetCollectionViewCell.xib b/PhotoRank/Asset Picker/AssetCollectionViewCell.xib new file mode 100644 index 0000000..cc28aa0 --- /dev/null +++ b/PhotoRank/Asset Picker/AssetCollectionViewCell.xib @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/Asset Picker/AssetPickerViewControler.swift b/PhotoRank/Asset Picker/AssetPickerViewControler.swift new file mode 100644 index 0000000..40ae784 --- /dev/null +++ b/PhotoRank/Asset Picker/AssetPickerViewControler.swift @@ -0,0 +1,152 @@ +// +// AssetCollectionViewController.swift +// PhotoRank +// +// Created by Shadowfacts on 8/3/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +private let reuseIdentifier = "AssetCell" + +class AssetPickerViewController: UICollectionViewController { + + var continueButton: UIBarButtonItem! + + var flowLayout: UICollectionViewFlowLayout { + return collectionViewLayout as! UICollectionViewFlowLayout + } + + var availableWidth: CGFloat! + var thumbnailSize: CGSize! + + var fetchResult: PHFetchResult! + + init() { + super.init(collectionViewLayout: UICollectionViewFlowLayout()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + collectionView.alwaysBounceVertical = true + + // Register cell classes + self.collectionView!.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier) + + let options = PHFetchOptions() + options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] + // get all photos except live photos + options.predicate = NSPredicate(format: "mediaType == %d && !((mediaSubtypes & %d) == %d)", PHAssetMediaType.image.rawValue, PHAssetMediaSubtype.photoLive.rawValue, PHAssetMediaSubtype.photoLive.rawValue) + fetchResult = PHAsset.fetchAssets(with: options) + + collectionView.allowsMultipleSelection = true + setEditing(true, animated: false) + + continueButton = UIBarButtonItem(title: "Continue", style: .done, target: self, action: #selector(continueButtonPressed)) + updateItemsSelected() + navigationItem.rightBarButtonItem = continueButton + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + let availableWidth = view.bounds.inset(by: view.safeAreaInsets).width + + if self.availableWidth != availableWidth { + self.availableWidth = availableWidth + + let size = (availableWidth - 8) / 3 + flowLayout.itemSize = CGSize(width: size, height: size) + flowLayout.minimumInteritemSpacing = 4 + flowLayout.minimumLineSpacing = 4 + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + let scale = UIScreen.main.scale + let cellSize = flowLayout.itemSize + thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale) + } + + func updateItemsSelected() { + let selected = collectionView.indexPathsForSelectedItems?.count ?? 0 + continueButton.isEnabled = selected >= 2 + + navigationItem.title = "\(selected) selected" + } + + @objc func continueButtonPressed() { + let indexPaths = collectionView.indexPathsForSelectedItems! + let assets = fetchResult.objects(at: IndexSet(indexPaths.map { $0.item })) + + let comparison = AssetComparisonViewController(assets: assets) + show(comparison, sender: nil) + } + + // MARK: UICollectionViewDataSource + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return fetchResult.count + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell + + let asset = fetchResult.object(at: indexPath.row) + + cell.assetIdentifier = asset.localIdentifier + PhotosHelper.requestThumbnail(for: asset, targetSize: thumbnailSize) { (image) in + DispatchQueue.main.async { + if cell.assetIdentifier == asset.localIdentifier { + cell.thumbnailImage = image + } + } + } + + return cell + } + + // MARK: UICollectionViewDelegate + + override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool { + return true + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + updateItemsSelected() + } + + override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + updateItemsSelected() + } + + /* + // Uncomment these methods to specify if an action menu should be displayed for the specified item, and react to actions performed on the item + override func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { + return false + } + + override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + return false + } + + override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { + + } + */ + +} diff --git a/PhotoRank/Base.lproj/Main.storyboard b/PhotoRank/Base.lproj/Main.storyboard deleted file mode 100644 index f1bcf38..0000000 --- a/PhotoRank/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PhotoRank/Final Rankings/RankingTableViewCell.swift b/PhotoRank/Final Rankings/RankingTableViewCell.swift new file mode 100644 index 0000000..24bae46 --- /dev/null +++ b/PhotoRank/Final Rankings/RankingTableViewCell.swift @@ -0,0 +1,79 @@ +// +// RankingTableViewCell.swift +// PhotoRank +// +// Created by Shadowfacts on 8/29/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +class RankingTableViewCell: UITableViewCell { + + @IBOutlet weak var assetImageView: UIImageView! + @IBOutlet weak var scoreLabel: UILabel! + @IBOutlet weak var favoriteButton: UIButton! + + var assetIdentifier: String? + + override func awakeFromNib() { + super.awakeFromNib() + + assetImageView.layer.masksToBounds = true + assetImageView.layer.cornerRadius = 8 + } + + func updateUI(asset: PHAsset, score: Int) { + self.assetIdentifier = asset.localIdentifier + + PhotosHelper.requestThumbnail(for: asset, targetSize: assetImageView.bounds.size) { (image) in + guard self.assetIdentifier == asset.localIdentifier else { return } + self.assetImageView.image = image + } + + scoreLabel.text = "Score: \(score)" + + updateFavoriteButton(favorited: asset.isFavorite) + } + + private func updateFavoriteButton(favorited: Bool) { + let name = favorited ? "heart.fill" : "heart" + let image = UIImage(systemName: name) + favoriteButton.setImage(image, for: .normal) + } + + @IBAction func favoriteButtonPressed() { + let generator = UINotificationFeedbackGenerator() + generator.prepare() + + guard let assetIdentifier = assetIdentifier else { + generator.notificationOccurred(.error) + return + } + let result = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentifier], options: nil) + guard result.count == 1 else { + generator.notificationOccurred(.error) + return + } + let asset = result.object(at: 0) + + + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetChangeRequest(for: asset) + request.isFavorite = !asset.isFavorite + }) { (success, error) in + if success { + generator.notificationOccurred(.success) + DispatchQueue.main.async { + guard self.assetIdentifier == assetIdentifier else { return } + self.updateFavoriteButton(favorited: !asset.isFavorite) + } + } else { + generator.notificationOccurred(.error) + print("Error ocurred changing favorite: \(String(describing: error))") + } + } + } + +} diff --git a/PhotoRank/Final Rankings/RankingTableViewCell.xib b/PhotoRank/Final Rankings/RankingTableViewCell.xib new file mode 100644 index 0000000..fb9ffd9 --- /dev/null +++ b/PhotoRank/Final Rankings/RankingTableViewCell.xib @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/Final Rankings/RankingsTableViewController.swift b/PhotoRank/Final Rankings/RankingsTableViewController.swift new file mode 100644 index 0000000..5d3945f --- /dev/null +++ b/PhotoRank/Final Rankings/RankingsTableViewController.swift @@ -0,0 +1,111 @@ +// +// RankingsTableViewController.swift +// PhotoRank +// +// Created by Shadowfacts on 8/5/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +private let reuseIdentifier = "AssetScoreCell" + +class RankingsTableViewController: UITableViewController { + + var rankings: [(PHAsset, Int)] + + init(rankings: [(PHAsset, Int)]) { + self.rankings = rankings + + super.init(nibName: "RankingsTableViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UINib(nibName: "RankingTableViewCell", bundle: .main), forCellReuseIdentifier: reuseIdentifier) + + tableView.rowHeight = 96 + + tableView.alwaysBounceVertical = true + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rankings.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! RankingTableViewCell + + let (asset, score) = rankings[indexPath.row] + cell.updateUI(asset: asset, score: score) + + return cell + } + + // Override to support conditional editing of the table view. + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + // Return false if you do not want the specified item to be editable. + return true + } + + // Override to support editing the table view. + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + guard editingStyle == .delete else { return } + + let (asset, _) = rankings[indexPath.row] + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.deleteAssets(NSArray(object: asset)) + }) { (success, error) in + let generator = UINotificationFeedbackGenerator() + if success { + generator.notificationOccurred(.success) + DispatchQueue.main.async { + self.rankings.remove(at: indexPath.row) + self.tableView.deleteRows(at: [indexPath], with: .fade) + } + } else { + generator.notificationOccurred(.error) + print("Error occurred deleting asset: \(String(describing: error))") + } + } + + } + + /* + // Override to support rearranging the table view. + override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) { + + } + */ + + /* + // Override to support conditional rearranging of the table view. + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { + // Return false if you do not want the item to be re-orderable. + return true + } + */ + + /* + // 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. + } + */ + +} diff --git a/PhotoRank/Final Rankings/RankingsTableViewController.xib b/PhotoRank/Final Rankings/RankingsTableViewController.xib new file mode 100644 index 0000000..28ca644 --- /dev/null +++ b/PhotoRank/Final Rankings/RankingsTableViewController.xib @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/Info.plist b/PhotoRank/Info.plist index 16be3b6..26ae4ee 100644 --- a/PhotoRank/Info.plist +++ b/PhotoRank/Info.plist @@ -2,6 +2,8 @@ + NSPhotoLibraryUsageDescription + This app must access your photo library to rank photos. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,8 +24,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 @@ -31,8 +31,8 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad diff --git a/PhotoRank/PhotosHelper.swift b/PhotoRank/PhotosHelper.swift new file mode 100644 index 0000000..e89a700 --- /dev/null +++ b/PhotoRank/PhotosHelper.swift @@ -0,0 +1,30 @@ +// +// PhotosHelper.swift +// PhotoRank +// +// Created by Shadowfacts on 8/3/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit +import Photos + +struct PhotosHelper { + private static let imageManager = PHCachingImageManager() + + private init() {} + + static func requestFullSizeImage(for asset: PHAsset, completion: @escaping (UIImage) -> Void) { + imageManager.requestImageData(for: asset, options: nil) { (data, _, _, _) in + guard let data = data, let image = UIImage(data: data) else { fatalError() } + completion(image) + } + } + + static func requestThumbnail(for asset: PHAsset, targetSize: CGSize, completion: @escaping (UIImage) -> Void) { + imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: nil) { (image, _) in + guard let image = image else { return } + completion(image) + } + } +} diff --git a/PhotoRank/Root/RootViewController.swift b/PhotoRank/Root/RootViewController.swift new file mode 100644 index 0000000..c5b20c6 --- /dev/null +++ b/PhotoRank/Root/RootViewController.swift @@ -0,0 +1,30 @@ +// +// RootViewController.swift +// PhotoRank +// +// Created by Shadowfacts on 8/3/19. +// Copyright © 2019 Shadowfacts. All rights reserved. +// + +import UIKit + +class RootViewController: UIViewController { + + init() { + super.init(nibName: "RootViewController", bundle: .main) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "PhotoRank" + } + + @IBAction func choosePhotosPressed() { + show(AssetPickerViewController(), sender: nil) + } +} diff --git a/PhotoRank/Root/RootViewController.xib b/PhotoRank/Root/RootViewController.xib new file mode 100644 index 0000000..8c296c7 --- /dev/null +++ b/PhotoRank/Root/RootViewController.xib @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PhotoRank/ViewController.swift b/PhotoRank/ViewController.swift deleted file mode 100644 index 5cedebc..0000000 --- a/PhotoRank/ViewController.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ViewController.swift -// PhotoRank -// -// Created by Shadowfacts on 8/3/19. -// Copyright © 2019 Shadowfacts. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} -