From 0517b4646f9a403e8fc92765f214f8361a6f428d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adria=CC=81n=20Bouza?= Date: Sat, 24 Nov 2018 09:36:45 +0100 Subject: [PATCH] Updated to swift 4.2. Improved current user API client. --- .swift-version | 2 +- Podfile.lock | 13 ++- Unsplasher.xcodeproj/project.pbxproj | 61 ++---------- Unsplasher/Info.plist | 2 +- Unsplasher/Model/OAuth.swift | 9 ++ .../Unsplash/AuthorizationManager.swift | 23 +++-- .../Requests/CollectionRequests.swift | 2 +- .../Requests/CurrentUserRequests.swift | 95 ++++++++++++++++++- .../Unsplash/Requests/PhotoRequests.swift | 2 +- Unsplasher/Unsplash/Requests/Requests.swift | 2 +- .../Unsplash/Requests/SearchRequests.swift | 2 +- .../Unsplash/Requests/UserRequests.swift | 8 +- Unsplasher/Unsplash/Unsplash.swift | 9 ++ UnsplasherSDK.podspec | 4 +- 14 files changed, 158 insertions(+), 76 deletions(-) diff --git a/.swift-version b/.swift-version index 5186d07..bf77d54 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.0 +4.2 diff --git a/Podfile.lock b/Podfile.lock index 7b5a4e8..4852e8e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -18,6 +18,17 @@ DEPENDENCIES: - SwiftKeychainWrapper - VegaScrollFlowLayout +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - Alamofire + - Hero + - Hue + - Kingfisher + - LGButton + - SnapKit + - SwiftKeychainWrapper + - VegaScrollFlowLayout + SPEC CHECKSUMS: Alamofire: f41a599bd63041760b26d393ec1069d9d7b917f4 Hero: cee286821578e170d8c0c9c9cda7897357401112 @@ -30,4 +41,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f6ded6c97ac0c0186c90641c539594a00d275a8f -COCOAPODS: 1.3.1 +COCOAPODS: 1.5.3 diff --git a/Unsplasher.xcodeproj/project.pbxproj b/Unsplasher.xcodeproj/project.pbxproj index c48a5cb..290087e 100644 --- a/Unsplasher.xcodeproj/project.pbxproj +++ b/Unsplasher.xcodeproj/project.pbxproj @@ -147,6 +147,7 @@ 3D1E4507203D4FA600886C64 /* photos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = photos.json; sourceTree = ""; }; 3D1E4509203D557B00886C64 /* Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Statistics.swift; sourceTree = ""; }; 3D1E450B203D570B00886C64 /* statistics.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = statistics.json; sourceTree = ""; }; + 3D38BFA021A9334C00CC75A3 /* UnsplasherSDK.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnsplasherSDK.podspec; sourceTree = ""; }; 3DA03727203D691700E85963 /* DateFormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterExtension.swift; sourceTree = ""; }; 3DA03729203D703C00E85963 /* Searches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Searches.swift; sourceTree = ""; }; 3DA0372B203D74CC00E85963 /* search_photos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = search_photos.json; sourceTree = ""; }; @@ -197,6 +198,7 @@ 1041A9D8203AE19900956719 = { isa = PBXGroup; children = ( + 3D38BFA021A9334C00CC75A3 /* UnsplasherSDK.podspec */, 1041A9E4203AE19900956719 /* Unsplasher */, 1041A9EF203AE19900956719 /* UnsplasherTests */, 10564647203C29E000D7D49E /* Example */, @@ -386,7 +388,6 @@ 1041A9DE203AE19900956719 /* Frameworks */, 1041A9DF203AE19900956719 /* Headers */, 1041A9E0203AE19900956719 /* Resources */, - 61F64230DE6F4DD8F4163C30 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -406,7 +407,6 @@ 1041A9E8203AE19900956719 /* Frameworks */, 1041A9E9203AE19900956719 /* Resources */, 7EC2840C45E6F5A10E283464 /* [CP] Embed Pods Frameworks */, - C57CA495BCBA279907521408 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -428,7 +428,6 @@ 10564644203C29E000D7D49E /* Resources */, 10564662203C2C2B00D7D49E /* Embed Frameworks */, 0CF99E8B8266BFB8A298CFC7 /* [CP] Embed Pods Frameworks */, - 6548BB2FC3BFF98896570CB7 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -452,11 +451,12 @@ TargetAttributes = { 1041A9E1203AE19900956719 = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 0920; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; }; 1041A9EA203AE19900956719 = { CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; }; 1041AA01203AF77400956719 = { @@ -574,36 +574,6 @@ shellScript = "#!/bin/sh\nUNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal\n# make sure the output directory exists\nmkdir -p \"${UNIVERSAL_OUTPUTFOLDER}\"\n# Step 1. Build Device and Simulator versions\nxcodebuild -target \"AlamoWater\" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\nxcodebuild -target \"AlamoWater\" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR=\"${BUILD_DIR}\" BUILD_ROOT=\"${BUILD_ROOT}\" clean build\n# Step 2. Copy the framework structure (from iphoneos build) to the universal folder\ncp -R \"${BUILD_DIR}/${CONFIGURATION}-iphoneos/AlamoWater.framework\" \"${UNIVERSAL_OUTPUTFOLDER}/\"\n# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory\nSIMULATOR_SWIFT_MODULES_DIR=\"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/AlamoWater.framework/Modules/AlamoWater.swiftmodule/.\"\nif [ -d \"${SIMULATOR_SWIFT_MODULES_DIR}\" ]; then\ncp -R \"${SIMULATOR_SWIFT_MODULES_DIR}\" \"${UNIVERSAL_OUTPUTFOLDER}/AlamoWater.framework/Modules/AlamoWater.swiftmodule\"\nfi\n# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory\nlipo -create -output \"${UNIVERSAL_OUTPUTFOLDER}/AlamoWater.framework/AlamoWater\" \"${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/AlamoWater.framework/AlamoWater\" \"${BUILD_DIR}/${CONFIGURATION}-iphoneos/AlamoWater.framework/AlamoWater\"\n# Step 5. Convenience step to copy the framework to the project's directory\ncp -R \"${UNIVERSAL_OUTPUTFOLDER}/AlamoWater.framework\" \"${PROJECT_DIR}\"\n# Step 6. Convenience step to open the project's directory in Finder\nopen \"${PROJECT_DIR}\""; showEnvVarsInLog = 0; }; - 61F64230DE6F4DD8F4163C30 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Unsplasher/Pods-Unsplasher-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 6548BB2FC3BFF98896570CB7 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example/Pods-Example-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 72BFF6C1712929F360A6736E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -660,21 +630,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C57CA495BCBA279907521408 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-UnsplasherTests/Pods-UnsplasherTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; D825C9EF1118B4DE8C837964 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -912,7 +867,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -935,7 +890,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.adboco.Unsplasher; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -950,7 +905,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.adboco.UnsplasherTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -965,7 +920,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.adboco.UnsplasherTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Unsplasher/Info.plist b/Unsplasher/Info.plist index f0993d5..d04a6b4 100644 --- a/Unsplasher/Info.plist +++ b/Unsplasher/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0.4 + 1.0.5 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/Unsplasher/Model/OAuth.swift b/Unsplasher/Model/OAuth.swift index fa3e3ba..477f9bc 100644 --- a/Unsplasher/Model/OAuth.swift +++ b/Unsplasher/Model/OAuth.swift @@ -42,4 +42,13 @@ public struct AccessToken: Codable { return AccessToken(token: token, tokenType: tokenType, scope: scope) } + static func remove(from keychain: KeychainWrapper?) { + guard let keychain = keychain else { + return + } + keychain.removeObject(forKey: "token") + keychain.removeObject(forKey: "token_type") + keychain.removeObject(forKey: "scope") + } + } diff --git a/Unsplasher/Unsplash/AuthorizationManager.swift b/Unsplasher/Unsplash/AuthorizationManager.swift index 8d008fd..8326721 100644 --- a/Unsplasher/Unsplash/AuthorizationManager.swift +++ b/Unsplasher/Unsplash/AuthorizationManager.swift @@ -7,6 +7,7 @@ // import Foundation +import WebKit /// It manages authorization process through a custom view controller final class AuthorizationManager { @@ -43,7 +44,7 @@ final class UnsplashAuthViewController: UIViewController { /// Completion handler var completionHandler: (AuthorizationResult) -> Void - let webView = UIWebView() + let webView = WKWebView() init(url: URL, callbackURLScheme: String, completion: @escaping (AuthorizationResult) -> Void) { self.authURL = url @@ -69,7 +70,7 @@ final class UnsplashAuthViewController: UIViewController { webView.frame = self.view.frame webView.backgroundColor = UIColor.white - webView.delegate = self + webView.navigationDelegate = self self.view.addSubview(webView) @@ -82,7 +83,7 @@ final class UnsplashAuthViewController: UIViewController { ]) let urlRequest = URLRequest(url: authURL) - webView.loadRequest(urlRequest) + webView.load(urlRequest) } @objc func cancel(sender: UIBarButtonItem) { @@ -93,27 +94,29 @@ final class UnsplashAuthViewController: UIViewController { @objc func refresh(sender: UIBarButtonItem) { let urlRequest = URLRequest(url: authURL) - webView.loadRequest(urlRequest) + webView.load(urlRequest) } } -extension UnsplashAuthViewController: UIWebViewDelegate { +extension UnsplashAuthViewController: WKNavigationDelegate { - func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { - guard let urlString = request.url?.absoluteString, urlString.starts(with: callbackURLScheme) else { - return true + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let urlString = navigationAction.request.url?.absoluteString, urlString.starts(with: callbackURLScheme) else { + decisionHandler(.allow) + return } guard let code = URLComponents(string: urlString)?.queryItems?.first(where: { $0.name == "code" })?.value else { self.dismiss(animated: true, completion: { self.completionHandler(.failure(UnsplashError.authorizationError)) }) - return true + decisionHandler(.allow) + return } self.dismiss(animated: true, completion: { self.completionHandler(.success(code)) }) - return false + decisionHandler(.cancel) } } diff --git a/Unsplasher/Unsplash/Requests/CollectionRequests.swift b/Unsplasher/Unsplash/Requests/CollectionRequests.swift index ea816dc..f1f62d2 100644 --- a/Unsplasher/Unsplash/Requests/CollectionRequests.swift +++ b/Unsplasher/Unsplash/Requests/CollectionRequests.swift @@ -17,7 +17,7 @@ final public class CollectionRequests: Paginable { self.manager = manager } - typealias paginableType = [Collection] + public typealias paginableType = [Collection] /// Get a single page from the list of all collections /// diff --git a/Unsplasher/Unsplash/Requests/CurrentUserRequests.swift b/Unsplasher/Unsplash/Requests/CurrentUserRequests.swift index 1f8070b..c342d0b 100644 --- a/Unsplasher/Unsplash/Requests/CurrentUserRequests.swift +++ b/Unsplasher/Unsplash/Requests/CurrentUserRequests.swift @@ -13,8 +13,23 @@ final public class CurrentUserRequests { private var manager: Unsplash + private var user: User? { + didSet { + photos.user = user + collections.user = user + } + } + + /// Photo related requests + public var photos: CurrentUserPhotosRequests! + + /// Collection related requests + public var collections: CurrentUserCollectionsRequests! + init(_ manager: Unsplash) { self.manager = manager + self.photos = CurrentUserPhotosRequests(manager) + self.collections = CurrentUserCollectionsRequests(manager) } /// Get current user's profile @@ -27,7 +42,10 @@ final public class CurrentUserRequests { return } let url = Unsplash.baseURLString + "/me" - self.manager.request(url: url, expectedType: User.self, completion: completion) + self.manager.request(url: url, expectedType: User.self) { result in + self.user = result.value + completion(result) + } } /// Update current user's profile @@ -69,3 +87,78 @@ final public class CurrentUserRequests { } } + +public class CurrentUserPhotosRequests: Paginable { + + internal var user: User? + + private var userPhotos: UserPhotosRequests! + + init(_ manager: Unsplash) { + self.userPhotos = UserPhotosRequests(manager) + } + + public typealias paginableType = [Photo] + + /// Get user photos + /// + /// - Parameters: + /// - page: Page + /// - perPage: Number of photos per page + /// - orderBy: Order + /// - stats: Show the stats for each user’s photo + /// - resolution: The frequency of the stats + /// - quantity: The amount of for each stat + /// - completion: Completion handler + public func get(page: Int? = nil, perPage: Int? = nil, orderBy: Unsplash.OrderBy? = .popular, stats: Bool? = false, resolution: Unsplash.StatisticsResolution? = nil, quantity: UInt32 = 30, completion: @escaping (PhotoListResult) -> Void) { + guard let username = user?.username else { + completion(.failure(UnsplashError.userProfileNeeded)) + return + } + self.userPhotos.by(username, page: page, perPage: perPage, orderBy: orderBy, stats: stats, resolution: resolution, quantity: quantity, completion: completion) + } + + /// Get photos liked by the user + /// + /// - Parameters: + /// - page: Page + /// - perPage: Number of photos per page + /// - orderBy: Order + /// - completion: Completion handler + public func liked(page: Int?, perPage: Int?, orderBy: Unsplash.OrderBy?, completion: @escaping (PhotoListResult) -> Void) { + guard let username = user?.username else { + completion(.failure(UnsplashError.userProfileNeeded)) + return + } + self.userPhotos.liked(by: username, page: page, perPage: perPage, orderBy: orderBy, completion: completion) + } + +} + +public class CurrentUserCollectionsRequests: Paginable { + + internal var user: User? + + private var userCollections: UserCollectionsRequests! + + init(_ manager: Unsplash) { + self.userCollections = UserCollectionsRequests(manager) + } + + public typealias paginableType = [Collection] + + /// Get list of collections created by the user + /// + /// - Parameters: + /// - page: Page + /// - perPage: Number of collections per page + /// - completion: Completion handler + public func get(page: Int?, perPage: Int?, completion: @escaping (CollectionListResult) -> Void) { + guard let username = user?.username else { + completion(.failure(UnsplashError.userProfileNeeded)) + return + } + self.userCollections.by(username, page: page, perPage: perPage, completion: completion) + } + +} diff --git a/Unsplasher/Unsplash/Requests/PhotoRequests.swift b/Unsplasher/Unsplash/Requests/PhotoRequests.swift index 50d0116..7b255d2 100644 --- a/Unsplasher/Unsplash/Requests/PhotoRequests.swift +++ b/Unsplasher/Unsplash/Requests/PhotoRequests.swift @@ -17,7 +17,7 @@ final public class PhotoRequests: Paginable { self.manager = manager } - typealias paginableType = [Photo] + public typealias paginableType = [Photo] /// Get list of photos /// diff --git a/Unsplasher/Unsplash/Requests/Requests.swift b/Unsplasher/Unsplash/Requests/Requests.swift index aed8312..f8d9772 100644 --- a/Unsplasher/Unsplash/Requests/Requests.swift +++ b/Unsplasher/Unsplash/Requests/Requests.swift @@ -328,7 +328,7 @@ public extension Unsplash { // MARK: - Paginable protocol -protocol Paginable { +public protocol Paginable { associatedtype paginableType: Codable diff --git a/Unsplasher/Unsplash/Requests/SearchRequests.swift b/Unsplasher/Unsplash/Requests/SearchRequests.swift index f6d2ffc..1adebe3 100644 --- a/Unsplasher/Unsplash/Requests/SearchRequests.swift +++ b/Unsplasher/Unsplash/Requests/SearchRequests.swift @@ -17,7 +17,7 @@ final public class SearchRequests: Paginable { self.manager = manager } - typealias paginableType = Search + public typealias paginableType = Search // MARK: - Users diff --git a/Unsplasher/Unsplash/Requests/UserRequests.swift b/Unsplasher/Unsplash/Requests/UserRequests.swift index a4100cb..26c7e06 100644 --- a/Unsplasher/Unsplash/Requests/UserRequests.swift +++ b/Unsplasher/Unsplash/Requests/UserRequests.swift @@ -79,7 +79,7 @@ final public class UserRequests { } -final public class UserPhotosRequests: Paginable { +public class UserPhotosRequests: Paginable { private var manager: Unsplash @@ -87,7 +87,7 @@ final public class UserPhotosRequests: Paginable { self.manager = manager } - typealias paginableType = [Photo] + public typealias paginableType = [Photo] /// Get a list of photos uploaded by an user. /// @@ -151,7 +151,7 @@ final public class UserPhotosRequests: Paginable { } -final public class UserCollectionsRequests: Paginable { +public class UserCollectionsRequests: Paginable { private var manager: Unsplash @@ -159,7 +159,7 @@ final public class UserCollectionsRequests: Paginable { self.manager = manager } - typealias paginableType = [Collection] + public typealias paginableType = [Collection] /// Get a list of collections created by a given user /// diff --git a/Unsplasher/Unsplash/Unsplash.swift b/Unsplasher/Unsplash/Unsplash.swift index 691250b..7b4c344 100644 --- a/Unsplasher/Unsplash/Unsplash.swift +++ b/Unsplasher/Unsplash/Unsplash.swift @@ -273,6 +273,7 @@ public enum UnsplashError: Error { case authenticationError case cancelAuthenticationError case credentialsError + case userProfileNeeded } @@ -304,6 +305,8 @@ extension UnsplashError: CustomStringConvertible { return "User has cancelled authentication process." case .credentialsError: return "Unable to make requests. You must configure credentials first." + case .userProfileNeeded: + return "Unable to get user related data before requesting his/her profile first." } } @@ -351,4 +354,10 @@ extension Unsplash { } } + /// Sign out current user + public func signOut() { + AccessToken.remove(from: keychain) + accessToken = nil + } + } diff --git a/UnsplasherSDK.podspec b/UnsplasherSDK.podspec index d556d37..a08c528 100644 --- a/UnsplasherSDK.podspec +++ b/UnsplasherSDK.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = "UnsplasherSDK" - s.version = "1.0.4" + s.version = "1.0.5" s.summary = "Unsplash API client written in Swift." # This description is used to generate tags and improve search results. @@ -97,6 +97,8 @@ Pod::Spec.new do |s| s.requires_arc = true + s.frameworks = "WebKit" + s.dependency "Alamofire", "~> 4.6" s.dependency "SwiftKeychainWrapper", "~> 3.0.1"