From 8d96392e85de098ad38738d2ae5abac10b384b1b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 18 Feb 2019 19:25:12 +0200 Subject: [PATCH 001/165] add isDisabled flag for username tokens --- BlockV/Core/Network/Models/User/FullTokenModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BlockV/Core/Network/Models/User/FullTokenModel.swift b/BlockV/Core/Network/Models/User/FullTokenModel.swift index 525213f3..ced50fa7 100644 --- a/BlockV/Core/Network/Models/User/FullTokenModel.swift +++ b/BlockV/Core/Network/Models/User/FullTokenModel.swift @@ -22,6 +22,7 @@ public struct FullTokenModel: Codable, Equatable { public let appID: String public let isConfirmed: Bool public let isDefault: Bool + public let isDisabled: Bool public let token: String public let tokenType: String public let userID: String @@ -31,6 +32,7 @@ public struct FullTokenModel: Codable, Equatable { case appID = "app_id" case isConfirmed = "confirmed" case isDefault = "is_default" + case isDisabled = "disabled" case token = "token" case tokenType = "token_type" case userID = "user_id" From 4d296716f440458b79543b82e5ca2fff27a163eb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 18 Feb 2019 19:25:36 +0200 Subject: [PATCH 002/165] username as a token type --- BlockV/Core/Network/Models/User/UserToken.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Network/Models/User/UserToken.swift b/BlockV/Core/Network/Models/User/UserToken.swift index 68bad290..eccfa3bc 100644 --- a/BlockV/Core/Network/Models/User/UserToken.swift +++ b/BlockV/Core/Network/Models/User/UserToken.swift @@ -24,9 +24,10 @@ import Foundation /// Models types of user tokens supported on the BLOCKv platform. public enum UserTokenType: String, Codable { - case phone = "phone_number" - case email = "email" - case id = "id" + case phone = "phone_number" + case email = "email" + case id = "id" + case username = "username" } /// User token model. From 2be16acaa8c144ca180a3dac9b04ebe904bc8a1d Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 18 Feb 2019 19:26:13 +0200 Subject: [PATCH 003/165] add username update and disable endpoints --- BlockV/Core/Network/Stack/API.swift | 22 +++++++ .../Core/Requests/BLOCKv+UserRequests.swift | 65 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 396c7da2..cf6ad647 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -181,6 +181,28 @@ extension API { return Endpoint(method: .put, path: currentUserPath + "/tokens/\(id)/default") } + + // MARK: Username + + /// Builds the endpoint to edit a username token. + /// + /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + static func updateUsernameToken(id: String, username: String) -> Endpoint> { + return Endpoint(method: .patch, + path: currentUserPath + "/tokens/\(id)", + parameters: [ + "token": username, + "token_type": "username" + ]) + } + + /// Builds the endpoint to disable a username token. + /// + /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + static func disableUsername(tokenId: String) -> Endpoint> { //FIXME: General model is wrong + return Endpoint(method: .put, + path: currentUserPath + "/tokens/\(tokenId)/disabled") + } // MARK: Avatar diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index 4aff3cb3..60ae5597 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -291,6 +291,10 @@ extension BLOCKv { /// /// Note: Primary tokens may not be deleted. /// + /// - important: Deleting a *username* token will allow the username to be reclaimed immediately by another + /// user. A safer approach is to call `TODO` which will prevent the username from being + /// reclaimed for a defined period of time. + /// /// - Parameters: /// - tokenId: Unique identifier of the token to be deleted. /// - completion: The completion handler to call when the request is completed. @@ -388,6 +392,67 @@ extension BLOCKv { } + /// Updates the current user's username. + /// + /// Updating a username token will automatically disable the current username token. + /// + /// - Parameters: + /// - tokenID: Unique identifier of the current username token. + /// - newUsername: New username. + /// - completion: The completion handler to call when the request is completed. + /// This handler is executed on the main queue. + public static func updateUsernameToken(tokenID: String, newUsername: String, + completion: @escaping (FullTokenModel?, BVError?) -> Void) { + + let endpoint = API.CurrentUser.updateUsernameToken(id: tokenID, username: newUsername) + + self.client.request(endpoint) { (baseModel, error) in + + // extract model, ensure no error + guard let tokenModel = baseModel?.payload, error == nil else { + DispatchQueue.main.async { + completion(nil, error) + } + return + } + + // model is available + DispatchQueue.main.async { + completion(tokenModel, nil) + } + + } + + } + + /// Disables a username token. + /// + /// Once disabled, the username token is frozen for a period of time. After this period has elapsed, the username + /// is recycled, at which time the username becomes available for other users in the system to claim. + public static func disableUsernameToken(tokenID: String, + completion: @escaping (BVError?) -> Void) { + + let endpoint = API.CurrentUser.disableUsername(tokenId: tokenID) + + self.client.request(endpoint) { (baseModel, error) in + + // extract model, ensure no error + guard baseModel?.payload != nil, error == nil else { + DispatchQueue.main.async { + completion(error) + } + return + } + + // model is available + DispatchQueue.main.async { + completion(nil) + } + + } + + } + /// DO NOT EXPOSE. ONLY USE FOR TESTING. /// /// DELETES THE CURRENT USER. From ec01b572a0e191b7993981c8b9896f7fd34faaaf Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 18 Feb 2019 19:26:47 +0200 Subject: [PATCH 004/165] add username as a transfer option --- .../Controllers/Actions/TransferActionViewController.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Example/BlockV/Controllers/Actions/TransferActionViewController.swift b/Example/BlockV/Controllers/Actions/TransferActionViewController.swift index 7228a1d9..f23c0cd2 100644 --- a/Example/BlockV/Controllers/Actions/TransferActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/TransferActionViewController.swift @@ -147,6 +147,9 @@ class TransferActionViewController: UIViewController { case .id: userTokenTextField.keyboardType = .asciiCapable userTokenTextField.placeholder = "User ID" + case .username: + userTokenTextField.keyboardType = .default + userTokenTextField.placeholder = "Username" } } From 46ec7488d6ba5fe356bc5cad89422cb8672c3944 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 12 Mar 2019 08:28:56 +0200 Subject: [PATCH 005/165] Add Result type from Swift --- BlockV/Core/Helpers/Result.swift | 156 +++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 BlockV/Core/Helpers/Result.swift diff --git a/BlockV/Core/Helpers/Result.swift b/BlockV/Core/Helpers/Result.swift new file mode 100644 index 00000000..16c8541b --- /dev/null +++ b/BlockV/Core/Helpers/Result.swift @@ -0,0 +1,156 @@ +/* + TEMPORARY RESULT TYPE. ONCE SWIFT 5 IS RELEASED THIS CAN BE REMOVED. + */ + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A value that represents either a success or a failure, including an +/// associated value in each case. +public enum Result { + /// A success, storing a `Success` value. + case success(Success) + + /// A failure, storing a `Failure` value. + case failure(Failure) + + /// Returns a new result, mapping any success value using the given + /// transformation. + /// + /// Use this method when you need to transform the value of a `Result` + /// instance when it represents a success. The following example transforms + /// the integer success value of a result into a string: + /// + /// func getNextInteger() -> Result { ... } + /// + /// let integerResult = getNextInteger() + /// // integerResult == .success(5) + /// let stringResult = integerResult.map({ String($0) }) + /// // stringResult == .success("5") + /// + /// - Parameter transform: A closure that takes the success value of this + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new success value if this instance represents a success. + public func map( + _ transform: (Success) -> NewSuccess + ) -> Result { + switch self { + case let .success(success): + return .success(transform(success)) + case let .failure(failure): + return .failure(failure) + } + } + + /// Returns a new result, mapping any failure value using the given + /// transformation. + /// + /// Use this method when you need to transform the value of a `Result` + /// instance when it represents a failure. The following example transforms + /// the error value of a result by wrapping it in a custom `Error` type: + /// + /// struct DatedError: Error { + /// var error: Error + /// var date: Date + /// + /// init(_ error: Error) { + /// self.error = error + /// self.date = Date() + /// } + /// } + /// + /// let result: Result = ... + /// // result == .failure() + /// let resultWithDatedError = result.mapError({ e in DatedError(e) }) + /// // result == .failure(DatedError(error: , date: )) + /// + /// - Parameter transform: A closure that takes the failure value of the + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new failure value if this instance represents a failure. + public func mapError( + _ transform: (Failure) -> NewFailure + ) -> Result { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return .failure(transform(failure)) + } + } + + /// Returns a new result, mapping any success value using the given + /// transformation and unwrapping the produced result. + /// + /// - Parameter transform: A closure that takes the success value of the + /// instance. + /// - Returns: A `Result` instance with the result of evaluating `transform` + /// as the new failure value if this instance represents a failure. + public func flatMap( + _ transform: (Success) -> Result + ) -> Result { + switch self { + case let .success(success): + return transform(success) + case let .failure(failure): + return .failure(failure) + } + } + + /// Returns a new result, mapping any failure value using the given + /// transformation and unwrapping the produced result. + /// + /// - Parameter transform: A closure that takes the failure value of the + /// instance. + /// - Returns: A `Result` instance, either from the closure or the previous + /// `.success`. + public func flatMapError( + _ transform: (Failure) -> Result + ) -> Result { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return transform(failure) + } + } + + /// Returns the success value as a throwing expression. + /// + /// Use this method to retrieve the value of this result if it represents a + /// success, or to catch the value if it represents a failure. + /// + /// let integerResult: Result = .success(5) + /// do { + /// let value = try integerResult.get() + /// print("The value is \(value).") + /// } catch error { + /// print("Error retrieving the value: \(error)") + /// } + /// // Prints "The value is 5." + /// + /// - Returns: The success value, if the instance represent a success. + /// - Throws: The failure value, if the instance represents a failure. + public func get() throws -> Success { + switch self { + case let .success(success): + return success + case let .failure(failure): + throw failure + } + } +} + +extension Result: Equatable where Success: Equatable, Failure: Equatable { } + +extension Result: Hashable where Success: Hashable, Failure: Hashable { } From e57219f2ef031cf2209f57a206c040302c0ce732 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 14:10:07 +0200 Subject: [PATCH 006/165] Add didSelectFaceView and didLoadFaceView methods --- .../Face/Vatom View/FaceMessageDelegate.swift | 115 +++++++++++++----- 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/BlockV/Face/Vatom View/FaceMessageDelegate.swift b/BlockV/Face/Vatom View/FaceMessageDelegate.swift index cbca0014..b848a94a 100644 --- a/BlockV/Face/Vatom View/FaceMessageDelegate.swift +++ b/BlockV/Face/Vatom View/FaceMessageDelegate.swift @@ -12,55 +12,106 @@ import Foundation import GenericJSON -/// Face message error (view bridge). -public struct FaceMessageError: Error { - public let message: String - /// Initialise with a message. This message is forwarded to the Web Face SDK. - public init(message: String) { - self.message = message - } -} - -/// The protocol types must conform to in order to comuninicate with vatom view. +/// The protocol types must conform to in order to communinicate with vatom view. /// -/// Faces may send messages to their underlying viewer to request additional functionality. As a viewer it is -/// important that you state which messages you support (by implementing `determinedSupport(forFaceMessages:[Srting])`). -/// This gives the face an opportunity to adapt it's behaviour. +/// Faces may send custom messages to their underlying viewer to request additional functionality. public protocol VatomViewDelegate: class { - /// Completion handler for face messages. + /// Tells the delegate that a face message has been received. /// /// - Parameters: - /// - payload: The JSON payload to be sent back to the Web Face SDK. - /// - error: Error with description if one was encountered. - typealias Completion = (_ payload: JSON?, _ error: FaceMessageError?) -> Void + /// - vatomView: A vatom-view object informing the delegate about the face message. + /// - message: The unique identifier of the message. + /// - object: Companion object which addtional information relevant to the request. + /// - completion: The completion handler to call once the request has been processed. Passed a `Result` type + /// with either the success payload of a face message error. + func vatomView(_ vatomView: VatomView, + didRecevieFaceMessage message: String, + withObject object: [String: JSON], + completion: ((Result) -> Void)?) - /// Called when the vatom view receives a message from the face. + /// Tells the delegate that a face view was selected, or an error was encountered. + /// + /// A face view is selected as part of the Vatom View Life Cycle (VVLC). There are two scenarios where this will + /// happen: + /// 1. A `VatomView` instance was created (triggering the VVLC). + /// 2. After calling `update(usingVatom:procedure:)` (triggering the VVLC), which resulted in a new Face View being + /// selected. /// /// - Parameters: - /// - vatomView: The `VatomView` instance which the face message was received from. - /// - message: The unique identifier of the message. - /// - object: Companion object which addtional information relevant to the request. - /// - completion: The completion handler to call once the request has been processed. + /// - vatomView: A vatom-view object informing the delegate about the selected face. + /// - result: A `Result` type type containing either the selected face view or an error. + func vatomView(_ vatomView: VatomView, didSelectFaceView result: Result) + + /// Tells the delgate that the selected face view has completed loading, or an error was encountered. + /// + /// - Parameters: + /// - vatomView: A vatom-view object informing the delegate about the selected face completing its load. + /// - result: A `Result` instance with the result of the selected face view's load outcome. + func vatomView(_ vatomView: VatomView, didLoadFaceView result: Result) + +} + +/// This extension contians default and empty implementations of `VatomViewDelegate` methods. +/// This is the 'Swifty' way to make the methods optional. +public extension VatomViewDelegate { + func vatomView(_ vatomView: VatomView, didRecevieFaceMessage message: String, withObject object: [String: JSON], - completion: VatomViewDelegate.Completion?) + completion: ((Result) -> Void)?) { + // optional method + } + + func vatomView(_ vatomView: VatomView, didSelectFaceView result: Result) { + // optional method + } + + func vatomView(_ vatomView: VatomView, didLoadFaceView result: Result) { + // optional method + } + +} + +/// Vatom View Life Cycle error. +public enum VVLCError: Error, CustomStringConvertible { + + /// Face selection failed. + case faceViewSelectionFailed + /// A face model was selected but no corresponding face view was registered. + case unregisteredFaceViewSelected(_ displayURL: String) + /// The face view failed to load. + case faceViewLoadFailed + + public var description: String { + switch self { + case .faceViewSelectionFailed: + return "Face Selection Procedure (FSP) did not select a face view." + case .unregisteredFaceViewSelected(let url): + return """ + Face Selection Procedure (FSP) selected a face view '\(url)' without the face view being registered. + """ + case .faceViewLoadFailed: + return "Face View load failed." + } + } } // MARK: - Face View Delegate +/// Face message error (view bridge). +public struct FaceMessageError: Error { + public let message: String + /// Initialise with a message. This message is forwarded to the Web Face SDK. + public init(message: String) { + self.message = message + } +} + /// The protocol face views must conform to in order to communicate protocol FaceViewDelegate: class { - /// Completion handler for face messages. - /// - /// - Parameters: - /// - payload: The JSON payload to be sent back to the Web Face View. - /// - error: Error with description if one was encountered. - typealias Completion = (_ payload: JSON?, _ error: FaceMessageError?) -> Void - /// Called when the vatom view receives a message from the face. /// /// - Parameters: @@ -71,7 +122,7 @@ protocol FaceViewDelegate: class { func faceView(_ faceView: FaceView, didSendMessage message: String, withObject object: [String: JSON], - completion: FaceViewDelegate.Completion?) + completion: ((Result) -> Void)?) } @@ -83,7 +134,7 @@ extension VatomView: FaceViewDelegate { func faceView(_ faceView: FaceView, didSendMessage message: String, withObject object: [String: JSON], - completion: ((JSON?, FaceMessageError?) -> Void)?) { + completion: ((Result) -> Void)?) { // forward the message to the vatom view delegate self.vatomViewDelegate?.vatomView(self, From 01c87865f7a0d2906f0ce9357c19d72e1759e7c8 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 14:11:29 +0200 Subject: [PATCH 007/165] Support didSelectFaceView and didLoadFaceView --- BlockV/Face/Vatom View/VatomView.swift | 67 +++++++++++++------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index a7c06f3a..7fed36f3 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -27,12 +27,6 @@ public protocol VatomViewError where Self: UIView { var vatom: VatomModel? { get set } } -/// Types that manage a `VatomView` should conform to this delegate to know when the face has completed loading. -protocol VatomViewLifecycleDelegate: class { - /// Called when the vatom view's selected face view has loaded successful or with an error. - func faceViewDidCompleteLoad(error: Error?) -} - /* Design Goals: 1. Vatom View will ask for the best face (default routine for each view context). @@ -42,6 +36,8 @@ protocol VatomViewLifecycleDelegate: class { prepareForReuse by calling `unLoad`. 4. Viewers must be able to use embedded FSPs. 5. Viewers must be able supply a custom FSP (defaults to the icon). + 6. Viewers must be informed of the selected face view (or any errors in selected the face view). + 7. Viewers must be informed of the completion of loading the face view (or any error encountered). */ /// Visualizes a vAtom by attempting to display one of the vAtom's face views. @@ -107,7 +103,7 @@ open class VatomView: UIView { /// /// The roster is a consolidated list of the face views registered by both the SDK and Viewer. /// This list represents the face views that are *capable* of being rendered. - public private(set) var roster: Roster + public internal(set) var roster: Roster /// Face model selected by the specifed face selection procedure (FSP). public private(set) var selectedFaceModel: FaceModel? @@ -186,13 +182,15 @@ open class VatomView: UIView { /// - procedure: The Face Selection Procedure (FSP) that determines which face view to display. /// Defaults to `.icon`. public init(vatom: VatomModel, - procedure: @escaping FaceSelectionProcedure = EmbeddedProcedure.icon.procedure) { + procedure: @escaping FaceSelectionProcedure = EmbeddedProcedure.icon.procedure, + delegate: VatomViewDelegate? = nil) { self.procedure = procedure self.roster = FaceViewRoster.shared.roster self.loaderView = VatomView.defaultLoaderView.init() self.errorView = VatomView.defaultErrorView.init() self.vatom = vatom + self.vatomViewDelegate = delegate super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) commonSetup() @@ -221,13 +219,15 @@ open class VatomView: UIView { procedure: @escaping FaceSelectionProcedure = EmbeddedProcedure.icon.procedure, loadingView: VVLoaderView = VatomView.defaultLoaderView.init(), errorView: VVErrorView = VatomView.defaultErrorView.init(), - roster: Roster = FaceViewRoster.shared.roster) { + roster: Roster = FaceViewRoster.shared.roster, + delegate: VatomViewDelegate? = nil) { self.procedure = procedure self.loaderView = loadingView self.errorView = errorView self.roster = roster self.vatom = vatom + self.vatomViewDelegate = delegate super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) commonSetup() @@ -315,25 +315,6 @@ open class VatomView: UIView { self.selectedFaceView?.unload() } - /// Vatom View Life Cycle error - public enum VVLCError: Error, CustomStringConvertible { - - case selectionFailed - case unregisteredFaceViewSelected(_ displayURL: String) - - public var description: String { - switch self { - case .selectionFailed: - return "Face Selection Procedure (FSP) did not to select a face view." - case .unregisteredFaceViewSelected(let url): - return """ - Face selection procedure (FSP) selected a face view '\(url)' without the face view being registered. - """ - } - } - - } - // MARK: - Vatom View Life Cycle /// Exectues the Vatom View Lifecycle (VVLC) on the current vAtom. @@ -347,7 +328,16 @@ open class VatomView: UIView { /// - Parameter oldVatom: The previous vAtom being visualized by this VatomView. internal func runVVLC(oldVatom: VatomModel? = nil) { + /* + Note: + VVLC traps with assertion failure in two cases: + 1. Backing vatom is nil. + 2. The supplied FSP selected a Face Model and no eligilbe Face View was installed. + Both of these cases indicated developer error. + */ + guard let vatom = vatom else { + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) assertionFailure("Developer error: vatom must not be nil.") return } @@ -363,6 +353,7 @@ open class VatomView: UIView { //printBV(error: "Face Selection Procedure (FSP) returned without selecting a face model.") self.state = .error + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) return } @@ -393,6 +384,8 @@ open class VatomView: UIView { self.state = .completed // update currently selected face view (without replacement) self.selectedFaceView?.vatomChanged(vatom) + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(self.selectedFaceView!)) } else { @@ -412,10 +405,11 @@ open class VatomView: UIView { guard let viewType = faceViewType else { // viewer developer MUST have registered the face view with the face registry + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) assertionFailure( """ - Developer error: Face selection procedure (FSP) selected a face without the face view being - installed. Your FSP MUST check if the face view has been registered with the FaceRegistry. + Developer error: Face Selection Procedure (FSP) selected a face model without an eligible face view + being registered. Your FSP MUST check if the face view has been registered with the FaceRegistry. """) return } @@ -429,6 +423,8 @@ open class VatomView: UIView { // replace currently selected face view with newly selected self.replaceFaceView(with: selectedFaceView) + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(selectedFaceView)) } @@ -456,18 +452,23 @@ open class VatomView: UIView { ///printBV(info: "Face view load completion called.") + guard let self = self else { return } + // Error - Case 2 - Display error view if the face view encounters an error during its load operation. // ensure no error guard error == nil else { // face view encountered an error - self?.selectedFaceView?.removeFromSuperview() - self?.state = .error + self.selectedFaceView?.removeFromSuperview() + self.state = .error + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) return } // show face - self?.state = .completed + self.state = .completed + // inform delegate + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(self.selectedFaceView!)) } From 1874738b6829996a4f4b0a4dc9ff0460417f295a Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 14:12:03 +0200 Subject: [PATCH 008/165] Convert to result type completion --- .../Web/WebScriptMessageHandler.swift | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift index 8aff9877..c2b908d8 100644 --- a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift +++ b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift @@ -234,32 +234,26 @@ extension WebFaceView { /// Routes the script message to the viewer and handles the response. private func routeMessageToViewer(_ message: RequestScriptMessage) { - + // notify the host's message delegate of the custom message from the web page self.delegate?.faceView(self, didSendMessage: message.name, withObject: message.payload, - completion: { (payload, error) in - - // transform the bridge error - let bridgeError = BridgeError.viewer(error?.message ?? "Unknown error occured.") - - // V1 Support. Transform the `viewer.qr.scan` reponse paylaod from [String: String] - // to String. - if message.version == "1.0.0" && message.name == "viewer.qr.scan" { - if let newPayload = payload?["data"] { - self.sendResponse(forRequestMessage: message, payload: newPayload, - error: bridgeError) - return - } + completion: { result in + switch result { + case .success(let payload): + self.sendResponse(forRequestMessage: message, payload: payload, error: nil) + return + case .failure(let error): + // transform the bridge error + let bridgeError = BridgeError.viewer(error.message) + self.sendResponse(forRequestMessage: message, + payload: nil, + error: bridgeError) + return } - - self.sendResponse(forRequestMessage: message, - payload: payload, - error: bridgeError) - }) - + } /// Transforms viewer message from protocol V1 to V2. From 5f911aac895867a706ba7f5bb6f4f1b4ca2d339b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 14:13:22 +0200 Subject: [PATCH 009/165] Allow delegate propagation on init --- Example/BlockV/Views/LiveVatomView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/BlockV/Views/LiveVatomView.swift b/Example/BlockV/Views/LiveVatomView.swift index 934831f6..11d5aa0d 100644 --- a/Example/BlockV/Views/LiveVatomView.swift +++ b/Example/BlockV/Views/LiveVatomView.swift @@ -36,8 +36,8 @@ class LiveVatomView: VatomView { commonInit() } - override init(vatom: VatomModel, procedure: @escaping FaceSelectionProcedure) { - super.init(vatom: vatom, procedure: procedure) + override init(vatom: VatomModel, procedure: @escaping FaceSelectionProcedure, delegate: VatomViewDelegate? = nil) { + super.init(vatom: vatom, procedure: procedure, delegate: delegate) commonInit() } From 74b956d0b08cde610c90de419f6aa44f4a312dab Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 14:49:34 +0200 Subject: [PATCH 010/165] Fix typos --- BlockV/Face/Vatom View/FaceSelectionProcedure.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BlockV/Face/Vatom View/FaceSelectionProcedure.swift b/BlockV/Face/Vatom View/FaceSelectionProcedure.swift index 963b1b27..0c0ed476 100644 --- a/BlockV/Face/Vatom View/FaceSelectionProcedure.swift +++ b/BlockV/Face/Vatom View/FaceSelectionProcedure.swift @@ -18,7 +18,7 @@ import Foundation /// face for a specific visual context. /// /// Closure inputs: -/// - Packed vAtom to be displayed. +/// - vAtom to be displayed. /// - Installed display URLs. This is the set of native face display URLs (i.e. unique identifiers of the installed /// native faces). /// @@ -214,7 +214,7 @@ private struct EmbeddedProcedureBuilder { if face.isNative { - // enusrue the native face is supported (i.e. the face code is installed) + // ensure the native face is supported (i.e. the face code is installed) if installedURLs.contains(where: { ($0.caseInsensitiveCompare(face.properties.displayURL) == .orderedSame) }) { // prefer 'native' over 'web' @@ -228,7 +228,7 @@ private struct EmbeddedProcedureBuilder { if face.isWeb { - // enusrue the native face is supported (i.e. the face code is installed) + // ensure the native face is supported (i.e. the face code is installed) if installedURLs.contains(where: { ($0.caseInsensitiveCompare("https://*") == .orderedSame) }) { } else { From cf9df9f117eb408e01d50bb670bf2740e436fe1b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 15:12:14 +0200 Subject: [PATCH 011/165] Update to result type delegate --- .../Inventory/TappedVatomViewController.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift index c5b484f0..6fa33bd6 100644 --- a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift +++ b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift @@ -150,12 +150,14 @@ class TappedVatomViewController: UIViewController { // MARK: - Vatom View Delegate extension TappedVatomViewController: VatomViewDelegate { - + func vatomView(_ vatomView: VatomView, didRecevieFaceMessage message: String, - withObject object: [String : JSON], - completion: ((JSON?, FaceMessageError?) -> Void)?) { - print("Message: \(message)") + withObject object: [String: JSON], + completion: ((Result) -> Void)?) { + + print("Handle face message: \(message)") + } - + } From 0811807c504ee9998a49427eeb316b4669040fc3 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 20 Mar 2019 15:18:56 +0200 Subject: [PATCH 012/165] lint --- BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift index c2b908d8..8dc93c5a 100644 --- a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift +++ b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift @@ -234,7 +234,7 @@ extension WebFaceView { /// Routes the script message to the viewer and handles the response. private func routeMessageToViewer(_ message: RequestScriptMessage) { - + // notify the host's message delegate of the custom message from the web page self.delegate?.faceView(self, didSendMessage: message.name, @@ -253,7 +253,7 @@ extension WebFaceView { return } }) - + } /// Transforms viewer message from protocol V1 to V2. From 5c09a4d4cc35f00494dd7a324d0bf1c9a7d7853f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 26 Mar 2019 13:39:51 +0200 Subject: [PATCH 013/165] Constrain Starscream to 3.0.6 --- BLOCKv.podspec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BLOCKv.podspec b/BLOCKv.podspec index f1969be6..e1899d40 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -18,12 +18,12 @@ Pod::Spec.new do |s| s.subspec 'Core' do |s| s.source_files = 'BlockV/Core/**/*.{swift}' - s.dependency 'Alamofire', '~> 4.7' # Networking - s.dependency 'Starscream', '~> 3.0' # Web socket - s.dependency 'JWTDecode', '~> 2.1' # JWT decoding - s.dependency 'Signals', '~> 6.0' # Elegant eventing - s.dependency 'SwiftLint', '~> 0.26' # Linter - s.dependency 'GenericJSON', '~> 1.2' # JSON + s.dependency 'Alamofire', '~> 4.7' # Networking + s.dependency 'Starscream', '~> 3.0.6' # Web socket + s.dependency 'JWTDecode', '~> 2.1' # JWT decoding + s.dependency 'Signals', '~> 6.0' # Elegant eventing + s.dependency 'SwiftLint', '~> 0.26' # Linter + s.dependency 'GenericJSON', '~> 1.2' # JSON #s.exclude_files = '**/Info*.plist' end From 78a915b5dc1994c5e1b7003995c8f520608d3b0f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 26 Mar 2019 13:48:08 +0200 Subject: [PATCH 014/165] Convert hasValue to hash(into:) --- BlockV/Core/Network/Models/Package/ActionModel.swift | 4 ++-- BlockV/Core/Network/Models/Package/FaceModel.swift | 4 ++-- BlockV/Core/Network/Models/Package/VatomModel.swift | 7 ++++--- .../Core/Network/Models/Package/VatomResourceModel.swift | 7 +++++-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/BlockV/Core/Network/Models/Package/ActionModel.swift b/BlockV/Core/Network/Models/Package/ActionModel.swift index 52b02c39..3df60763 100644 --- a/BlockV/Core/Network/Models/Package/ActionModel.swift +++ b/BlockV/Core/Network/Models/Package/ActionModel.swift @@ -110,8 +110,8 @@ extension ActionModel { extension ActionModel: Hashable { - public var hashValue: Int { - return compoundName.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(compoundName) } } diff --git a/BlockV/Core/Network/Models/Package/FaceModel.swift b/BlockV/Core/Network/Models/Package/FaceModel.swift index c0a04ade..99a9e338 100644 --- a/BlockV/Core/Network/Models/Package/FaceModel.swift +++ b/BlockV/Core/Network/Models/Package/FaceModel.swift @@ -100,8 +100,8 @@ extension FaceModel: Codable { extension FaceModel: Hashable { /// Faces are uniquely identified by their platform identifier. - public var hashValue: Int { - return id.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(id) } } diff --git a/BlockV/Core/Network/Models/Package/VatomModel.swift b/BlockV/Core/Network/Models/Package/VatomModel.swift index 499f471e..a2026a63 100644 --- a/BlockV/Core/Network/Models/Package/VatomModel.swift +++ b/BlockV/Core/Network/Models/Package/VatomModel.swift @@ -72,7 +72,7 @@ extension VatomModel: Codable { whenCreated = try items.decode(Date.self, forKey: .whenCreated) whenModified = try items.decode(Date.self, forKey: .whenModified) props = try items.decode(RootProperties.self, forKey: .props) - + isUnpublished = try items.decodeIfPresent(Bool.self, forKey: .isUnpublished) ?? false `private` = try items.decodeIfPresent(JSON.self, forKey: .private) eos = try items.decodeIfPresent(JSON.self, forKey: .eos) @@ -102,9 +102,10 @@ extension VatomModel: Codable { extension VatomModel: Hashable { /// vAtoms are uniquely identified by their platform identifier. - public var hashValue: Int { - return id.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(id) } + } // MARK: - Vatom Root Properties diff --git a/BlockV/Core/Network/Models/Package/VatomResourceModel.swift b/BlockV/Core/Network/Models/Package/VatomResourceModel.swift index 2935f591..6ca90ccb 100644 --- a/BlockV/Core/Network/Models/Package/VatomResourceModel.swift +++ b/BlockV/Core/Network/Models/Package/VatomResourceModel.swift @@ -61,7 +61,10 @@ extension VatomResourceModel: Codable { extension VatomResourceModel: Hashable { - public var hashValue: Int { - return name.hashValue ^ type.hashValue ^ url.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(type) + hasher.combine(url) } + } From 1a4f18a27d9637e6b152d70492666a0ffeb66af3 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 26 Mar 2019 13:48:21 +0200 Subject: [PATCH 015/165] Lint --- BlockV/Core/Helpers/Result.swift | 12 ++++++------ .../Image Progress/ImageProgressFaceView.swift | 2 +- BlockV/Face/Face Views/Web/WebFaceView.swift | 11 ++++++++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/BlockV/Core/Helpers/Result.swift b/BlockV/Core/Helpers/Result.swift index 16c8541b..e0f824fe 100644 --- a/BlockV/Core/Helpers/Result.swift +++ b/BlockV/Core/Helpers/Result.swift @@ -19,10 +19,10 @@ public enum Result { /// A success, storing a `Success` value. case success(Success) - + /// A failure, storing a `Failure` value. case failure(Failure) - + /// Returns a new result, mapping any success value using the given /// transformation. /// @@ -51,7 +51,7 @@ public enum Result { return .failure(failure) } } - + /// Returns a new result, mapping any failure value using the given /// transformation. /// @@ -88,7 +88,7 @@ public enum Result { return .failure(transform(failure)) } } - + /// Returns a new result, mapping any success value using the given /// transformation and unwrapping the produced result. /// @@ -106,7 +106,7 @@ public enum Result { return .failure(failure) } } - + /// Returns a new result, mapping any failure value using the given /// transformation and unwrapping the produced result. /// @@ -124,7 +124,7 @@ public enum Result { return transform(failure) } } - + /// Returns the success value as a throwing expression. /// /// Use this method to retrieve the value of this result if it represents a diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 828d2eff..0fc88dff 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -187,7 +187,7 @@ class ImageProgressFaceView: FaceView { let paddingEnd = contentClippingRect.width * CGFloat(self.config.paddingEnd) / imagePixelSize.width let innerY = contentClippingRect.height - paddingStart - paddingEnd - let innerX = contentClippingRect.width - paddingStart - paddingEnd +// let innerX = contentClippingRect.width - paddingStart - paddingEnd let innerProgressY = (contentClippingRect.height - paddingStart - paddingEnd) * progress let innerProgressX = (contentClippingRect.width - paddingStart - paddingEnd) * progress diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index 49c5b9b2..599d26f2 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -136,7 +136,9 @@ extension WebFaceView: WKNavigationDelegate { if navigationAction.navigationType == .linkActivated { if let url = navigationAction.request.url { // open in Safari.app - UIApplication.shared.open(url, options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), completionHandler: nil) + UIApplication.shared.open(url, + options: convertToUIApplicationOpenExternalURLOptionsKeyDictionary([:]), + completionHandler: nil) return decisionHandler(.cancel) } } @@ -148,6 +150,9 @@ extension WebFaceView: WKNavigationDelegate { } // Helper function inserted by Swift 4.2 migrator. -fileprivate func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) -> [UIApplication.OpenExternalURLOptionsKey: Any] { - return Dictionary(uniqueKeysWithValues: input.map { key, value in (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value)}) +private func convertToUIApplicationOpenExternalURLOptionsKeyDictionary(_ input: [String: Any]) + -> [UIApplication.OpenExternalURLOptionsKey: Any] { + return Dictionary(uniqueKeysWithValues: input.map { key, value in + (UIApplication.OpenExternalURLOptionsKey(rawValue: key), value) + }) } From f5e7005d67c3658d1ff85418cc098414d2eb9322 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 28 Mar 2019 09:27:10 +0200 Subject: [PATCH 016/165] Add basic action support --- Example/BlockV/Actions.storyboard | 10 ++- Example/BlockV/Base.lproj/Main.storyboard | 17 ++++- .../ActionListTableViewController.swift | 54 +++++++++++--- ...ift => OutboundActionViewController.swift} | 71 +++++++------------ .../Inventory/TappedVatomViewController.swift | 5 ++ Example/Podfile.lock | 16 ++--- 6 files changed, 103 insertions(+), 70 deletions(-) rename Example/BlockV/Controllers/Actions/{TransferActionViewController.swift => OutboundActionViewController.swift} (69%) diff --git a/Example/BlockV/Actions.storyboard b/Example/BlockV/Actions.storyboard index 9c2f0644..073edd32 100644 --- a/Example/BlockV/Actions.storyboard +++ b/Example/BlockV/Actions.storyboard @@ -1,11 +1,11 @@ - + - + @@ -36,9 +36,7 @@ - - - + @@ -63,7 +61,7 @@ - + diff --git a/Example/BlockV/Base.lproj/Main.storyboard b/Example/BlockV/Base.lproj/Main.storyboard index 50f869bf..22123670 100644 --- a/Example/BlockV/Base.lproj/Main.storyboard +++ b/Example/BlockV/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -745,6 +745,14 @@ + + + + + + + + @@ -835,6 +843,11 @@ + + + + + diff --git a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift index 99010b34..cd6a6ef2 100644 --- a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift +++ b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift @@ -39,11 +39,18 @@ class ActionListTableViewController: UITableViewController { var vatom: VatomModel! - // For now, we hardcode to display a single action. - // - // In a future release, the actions configure for the vAtom's template - // will be returned. - fileprivate var actions: [String] = ["Transfer"] + // table view data model + struct AvailableAction { + let action: ActionModel + /// Flag indicating whether this action is supported by this viewer. + let isSupported: Bool + } + + /// List of available actions. + fileprivate var availableActions: [AvailableAction] = [] + + /// List of actions this viewer supports (i.e. knows how to handle). + private var supportedActions = ["Transfer", "Clone", "Redeem"] // MARK: - Actions @@ -66,10 +73,14 @@ class ActionListTableViewController: UITableViewController { /// Fetches all the actions configured / associated with our vAtom's template. fileprivate func fetchActions() { + self.showNavBarActivityRight() + let templateID = self.vatom.props.templateID BLOCKv.getActions(forTemplateID: templateID) { (actions, error) in + self.hideNavBarActivityRight() + // unwrap actions, handle error guard let actions = actions, error == nil else { print(error!.localizedDescription) @@ -78,18 +89,33 @@ class ActionListTableViewController: UITableViewController { // success print("Actions: \(actions.debugDescription)") + // update data source + self.availableActions = actions.map { action -> AvailableAction in + // record + let supported = self.supportedActions.contains(action.name) + return AvailableAction(action: action, isSupported: supported) + } + self.tableView.reloadData() } } // MARK: - Navigation + + override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { + return false + } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - let destination = segue.destination as! TransferActionViewController + // dependecy injection + let destination = segue.destination as! OutboundActionViewController destination.vatom = self.vatom + destination.actionName = self.selectedAction!.action.name } - + + var selectedAction: AvailableAction? + } // MARK: - Table view data source @@ -101,14 +127,24 @@ extension ActionListTableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 + return availableActions.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell.action.id", for: indexPath) - cell.textLabel?.text = actions[indexPath.row] + let availableAction = availableActions[indexPath.row] + cell.textLabel?.text = availableAction.action.name + cell.textLabel?.alpha = availableAction.isSupported ? 1 : 0.5 + cell.accessoryView = nil return cell } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + selectedAction = availableActions[indexPath.row] + if selectedAction!.isSupported { + self.performSegue(withIdentifier: "seg.action.selection", sender: self) + } + } + } diff --git a/Example/BlockV/Controllers/Actions/TransferActionViewController.swift b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift similarity index 69% rename from Example/BlockV/Controllers/Actions/TransferActionViewController.swift rename to Example/BlockV/Controllers/Actions/OutboundActionViewController.swift index 7228a1d9..655f38e1 100644 --- a/Example/BlockV/Controllers/Actions/TransferActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift @@ -22,18 +22,19 @@ // // -// TransferActionViewController.swift +// OutboundActionViewController.swift // BlockV_Example // import UIKit import BLOCKv -class TransferActionViewController: UIViewController { +class OutboundActionViewController: UIViewController { // MARK: - Properties var vatom: VatomModel! + var actionName: String! /// Type of token selected. var tokenType: UserTokenType { @@ -56,9 +57,10 @@ class TransferActionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - precondition(vatom != nil, "A vAtom must be passed into this view controller.") + precondition(actionName != nil, "An action name must be passed into this view controller.") + self.title = actionName } // MARK: - Actions @@ -78,61 +80,37 @@ class TransferActionViewController: UIViewController { // create the token guard let value = userTokenTextField.text else { return } let token = UserToken(value: value, type: tokenType) - - performTransferManual(token: token) - // OR - //performTransferConvenience(token: token) + // execute the action + performAction(token: token) } /// Option 1 - This show the convenience `transfer` method on VatomModel to transfer the vAtom to /// a another user via a phone, email, or user id token. - func performTransferConvenience(token: UserToken) { + func performAction(token: UserToken) { - self.vatom.transfer(toToken: token) { [weak self] (json, error) in - - // unwrap data, handle error - guard let json = json, error == nil else { - print(error!.localizedDescription) + let resultHandler: ((Result<[String: Any], BVError>) -> Void) = { [weak self] result in + switch result { + case .success(let json): + // success + print("Action response: \(json.debugDescription)") + self?.hide() + case .failure(let error): + print(error.localizedDescription) return } - - // success - print("Action response: \(json.debugDescription)") - self?.hide() } - } - - /// Option 2 - This show the manual, method of performing an action by constructing the - /// action body payload. - func performTransferManual(token: UserToken) { - - /* - Each action has a defined payload structure. - */ - let body = [ - "this.id": self.vatom.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - BLOCKv.performAction(name: "Transfer", payload: body) { [weak self] (json, error) in - - // unwrap data, handle error - guard let json = json, error == nil else { - print(error!.localizedDescription) - return - } - - // success - print("Action response: \(json.debugDescription)") - self?.hide() - + switch actionName { + case "Transfer": self.vatom.transfer(toToken: token, completion: resultHandler) + case "Clone": self.vatom.clone(toToken: token, completion: resultHandler) + case "Redeem": self.vatom.redeem(toToken: token, completion: resultHandler) + default: + return } } - // MARK: - Helpers fileprivate func configureTokenTextField(for type: UserTokenType) { @@ -141,17 +119,20 @@ class TransferActionViewController: UIViewController { case .phone: userTokenTextField.keyboardType = .phonePad userTokenTextField.placeholder = "Phone number" + userTokenTextField.autocorrectionType = .no case .email: userTokenTextField.keyboardType = .emailAddress userTokenTextField.placeholder = "Email address" + userTokenTextField.autocorrectionType = .no case .id: userTokenTextField.keyboardType = .asciiCapable userTokenTextField.placeholder = "User ID" + userTokenTextField.autocorrectionType = .no } } fileprivate func hide() { - self.navigationController?.dismiss(animated: true, completion: nil) + self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil) } } diff --git a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift index 6fa33bd6..9c4cfa75 100644 --- a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift +++ b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift @@ -142,6 +142,11 @@ class TappedVatomViewController: UIViewController { if segue.identifier == "seg.vatom.detail" { let destination = segue.destination as! VatomDetailTableViewController destination.vatom = vatom + } else if segue.identifier == "seg.action.storyboard" { + let destination = segue.destination as! UINavigationController + let vc = destination.viewControllers[0] as! ActionListTableViewController + // pass vatom along + vc.vatom = self.vatom } } diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 85020b48..9e3f9ede 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,15 +1,15 @@ PODS: - Alamofire (4.8.1) - - BLOCKv (3.0.0): - - BLOCKv/Face (= 3.0.0) - - BLOCKv/Core (3.0.0): + - BLOCKv (3.1.0): + - BLOCKv/Face (= 3.1.0) + - BLOCKv/Core (3.1.0): - Alamofire (~> 4.7) - GenericJSON (~> 1.2) - JWTDecode (~> 2.1) - Signals (~> 6.0) - - Starscream (~> 3.0) + - Starscream (~> 3.0.6) - SwiftLint (~> 0.26) - - BLOCKv/Face (3.0.0): + - BLOCKv/Face (3.1.0): - BLOCKv/Core - FLAnimatedImage (~> 1.0) - Nuke (~> 7.0) @@ -53,7 +53,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: 16ce2c353fb72865124ddae8a57c5942388f4f11 - BLOCKv: 60d3a98639188c430c00d126669e452c4aac1d73 + BLOCKv: 214559aa90809f52e7bd2ffbf733d895b3c5ada1 FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 GenericJSON: 05eb212cd12bf0562b816075090bea3eda2ab2ed JWTDecode: 85a405ab16d5473e99bd89ded1d535090ebd6a0e @@ -64,6 +64,6 @@ SPEC CHECKSUMS: SwiftLint: a54bf1fe12b55c68560eb2a7689dfc81458508f7 VatomFace3D: 78d08c503819d5e2d8e1978d953885d596e379cf -PODFILE CHECKSUM: d63113581b30008d083f82a8a354c17db0a9e801 +PODFILE CHECKSUM: 572540b34367caffea356b90ca04601e730b047d -COCOAPODS: 1.6.0 +COCOAPODS: 1.6.1 From 8544cdf7b42c80de75037f41d9fa6877feda6e10 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 28 Mar 2019 09:29:01 +0200 Subject: [PATCH 017/165] Rename file --- Example/BlockV.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 4f33dcc9..ffcb6fe9 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ AD0BCC98217A0106001836DE /* LiveVatomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0BCC97217A0106001836DE /* LiveVatomView.swift */; }; AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */; }; AD36A5B0215E3578009EFD55 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = AD36A5AF215E3578009EFD55 /* CHANGELOG.md */; }; + AD3B591D224CAF0600257EF3 /* OutboundActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */; }; AD3DB5302157FD7200E00B67 /* TappedVatomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */; }; AD61462E213DC88700204E5B /* FaceSelectionProcedure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */; }; AD61462F213DC88B00204E5B /* MockModelFaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462C213DC87100204E5B /* MockModelFaces.swift */; }; @@ -44,7 +45,6 @@ D55B21F52056745700B6D5C2 /* UIViewController+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B21F42056745700B6D5C2 /* UIViewController+Ext.swift */; }; D55B21F92056DE6D00B6D5C2 /* UIColor+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B21F82056DE6D00B6D5C2 /* UIColor+Ext.swift */; }; D55B21FB2056E04B00B6D5C2 /* InventoryCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55B21FA2056E04B00B6D5C2 /* InventoryCollectionViewController.swift */; }; - D5728D5C206131980041F4F7 /* TransferActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728D5B206131980041F4F7 /* TransferActionViewController.swift */; }; D5728D5E206155600041F4F7 /* ActionListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5728D5D206155600041F4F7 /* ActionListTableViewController.swift */; }; D597FF272051305A009B2910 /* UIAlertController+Etx.swift in Sources */ = {isa = PBXBuildFile; fileRef = D597FF262051305A009B2910 /* UIAlertController+Etx.swift */; }; D59C7DDD206CE1D000FCE1DD /* Actions.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D59C7DDC206CE1D000FCE1DD /* Actions.storyboard */; }; @@ -90,6 +90,7 @@ AD0BCC97217A0106001836DE /* LiveVatomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveVatomView.swift; sourceTree = ""; }; AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLoaderView.swift; sourceTree = ""; }; AD36A5AF215E3578009EFD55 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; + AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundActionViewController.swift; sourceTree = ""; }; AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TappedVatomViewController.swift; sourceTree = ""; }; AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceSelectionProcedure_Tests.swift; sourceTree = ""; }; AD61462C213DC87100204E5B /* MockModelFaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModelFaces.swift; sourceTree = ""; }; @@ -123,7 +124,6 @@ D55B21F42056745700B6D5C2 /* UIViewController+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ext.swift"; sourceTree = ""; }; D55B21F82056DE6D00B6D5C2 /* UIColor+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Ext.swift"; sourceTree = ""; }; D55B21FA2056E04B00B6D5C2 /* InventoryCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InventoryCollectionViewController.swift; sourceTree = ""; }; - D5728D5B206131980041F4F7 /* TransferActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferActionViewController.swift; sourceTree = ""; }; D5728D5D206155600041F4F7 /* ActionListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionListTableViewController.swift; sourceTree = ""; }; D597FF262051305A009B2910 /* UIAlertController+Etx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Etx.swift"; sourceTree = ""; }; D59C7DDC206CE1D000FCE1DD /* Actions.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Actions.storyboard; sourceTree = ""; }; @@ -341,8 +341,8 @@ D5728D5A206130E40041F4F7 /* Actions */ = { isa = PBXGroup; children = ( + AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */, D5728D5D206155600041F4F7 /* ActionListTableViewController.swift */, - D5728D5B206131980041F4F7 /* TransferActionViewController.swift */, ); path = Actions; sourceTree = ""; @@ -600,6 +600,7 @@ files = ( AD0BCC98217A0106001836DE /* LiveVatomView.swift in Sources */, D5475764205D124100E6FE90 /* UIView+Ext.swift in Sources */, + AD3B591D224CAF0600257EF3 /* OutboundActionViewController.swift in Sources */, AD920A5C215FB47200CEFC1D /* PasswordTableViewController.swift in Sources */, D547576B205E67AD00E6FE90 /* VatomCell.swift in Sources */, D519A84B205E83AB006B0D19 /* VatomDetailTableViewController.swift in Sources */, @@ -620,7 +621,6 @@ D55B21EA2052E38000B6D5C2 /* RoundedImageView.swift in Sources */, D5728D5E206155600041F4F7 /* ActionListTableViewController.swift in Sources */, AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */, - D5728D5C206131980041F4F7 /* TransferActionViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From c516575d9a36c93d2f49172be03bd1f6015e4ea9 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 28 Mar 2019 10:16:07 +0200 Subject: [PATCH 018/165] Add convenience clone and redeem funcitons --- .../OutboundActionViewController.swift | 79 ++++++++++++++++--- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift index 655f38e1..c21db830 100644 --- a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift @@ -85,26 +85,24 @@ class OutboundActionViewController: UIViewController { } - /// Option 1 - This show the convenience `transfer` method on VatomModel to transfer the vAtom to - /// a another user via a phone, email, or user id token. + /// Performs the appropriate action using the func performAction(token: UserToken) { - let resultHandler: ((Result<[String: Any], BVError>) -> Void) = { [weak self] result in - switch result { - case .success(let json): - // success - print("Action response: \(json.debugDescription)") - self?.hide() - case .failure(let error): - print(error.localizedDescription) + let completionHandler: (([String: Any]?, BVError?) -> Void) = { [weak self] (data, error) in + // handle error + guard let data = data, error != nil else { + print(error!.localizedDescription) return } + // handle success + print("Action response: \(data.debugDescription)") + self?.hide() } switch actionName { - case "Transfer": self.vatom.transfer(toToken: token, completion: resultHandler) - case "Clone": self.vatom.clone(toToken: token, completion: resultHandler) - case "Redeem": self.vatom.redeem(toToken: token, completion: resultHandler) + case "Transfer": self.vatom.transfer(toToken: token, completion: completionHandler) + case "Clone": self.vatom.clone(toToken: token, completion: completionHandler) + case "Redeem": self.vatom.redeem(toToken: token, completion: completionHandler) default: return } @@ -136,3 +134,58 @@ class OutboundActionViewController: UIViewController { } } + +/// Action Extensions +extension VatomModel { + + /// Clones this vAtom to the specified token. + /// + /// Note: Calling this action will trigger the action associated with this vAtom's + /// template. If an action has not been configured, an error will be generated. + /// + /// - Parameters: + /// - token: Standard UserToken (Phone, Email, or User ID) + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func clone(toToken token: UserToken, + completion: @escaping ([String: Any]?, BVError?) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // perform the action + BLOCKv.performAction(name: "Clone", payload: body) { (json, error) in + //TODO: should it be weak self? + completion(json, error) + } + + } + + /// Redeems this vAtom to the specified token. + /// + /// Note: Calling this action will trigger the action associated with this vAtom's + /// template. If an action has not been configured, an error will be generated. + /// + /// - Parameters: + /// - token: Standard UserToken (Phone, Email, or User ID) + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func redeem(toToken token: UserToken, + completion: @escaping ([String: Any]?, BVError?) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // perform the action + BLOCKv.performAction(name: "Redeem", payload: body) { (json, error) in + //TODO: should it be weak self? + completion(json, error) + } + + } + +} From d26aab07ac52eb07f5978d63a14943fca22a6770 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 28 Mar 2019 10:22:54 +0200 Subject: [PATCH 019/165] Shift extensions to new file --- Example/BlockV.xcodeproj/project.pbxproj | 4 + .../OutboundActionViewController.swift | 55 ------------- .../Extensions/VatomModel+Actions.swift | 80 +++++++++++++++++++ 3 files changed, 84 insertions(+), 55 deletions(-) create mode 100644 Example/BlockV/Extensions/VatomModel+Actions.swift diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index ffcb6fe9..a46953a6 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */; }; AD36A5B0215E3578009EFD55 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = AD36A5AF215E3578009EFD55 /* CHANGELOG.md */; }; AD3B591D224CAF0600257EF3 /* OutboundActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */; }; + AD3B591F224CBAA000257EF3 /* VatomModel+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */; }; AD3DB5302157FD7200E00B67 /* TappedVatomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */; }; AD61462E213DC88700204E5B /* FaceSelectionProcedure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */; }; AD61462F213DC88B00204E5B /* MockModelFaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462C213DC87100204E5B /* MockModelFaces.swift */; }; @@ -91,6 +92,7 @@ AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLoaderView.swift; sourceTree = ""; }; AD36A5AF215E3578009EFD55 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundActionViewController.swift; sourceTree = ""; }; + AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VatomModel+Actions.swift"; sourceTree = ""; }; AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TappedVatomViewController.swift; sourceTree = ""; }; AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceSelectionProcedure_Tests.swift; sourceTree = ""; }; AD61462C213DC87100204E5B /* MockModelFaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModelFaces.swift; sourceTree = ""; }; @@ -394,6 +396,7 @@ D55B21F42056745700B6D5C2 /* UIViewController+Ext.swift */, D55B21F82056DE6D00B6D5C2 /* UIColor+Ext.swift */, D5475763205D124100E6FE90 /* UIView+Ext.swift */, + AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */, ); path = Extensions; sourceTree = ""; @@ -621,6 +624,7 @@ D55B21EA2052E38000B6D5C2 /* RoundedImageView.swift in Sources */, D5728D5E206155600041F4F7 /* ActionListTableViewController.swift in Sources */, AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */, + AD3B591F224CBAA000257EF3 /* VatomModel+Actions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift index c21db830..87169f15 100644 --- a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift @@ -134,58 +134,3 @@ class OutboundActionViewController: UIViewController { } } - -/// Action Extensions -extension VatomModel { - - /// Clones this vAtom to the specified token. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - token: Standard UserToken (Phone, Email, or User ID) - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func clone(toToken token: UserToken, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - // perform the action - BLOCKv.performAction(name: "Clone", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - - /// Redeems this vAtom to the specified token. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - token: Standard UserToken (Phone, Email, or User ID) - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func redeem(toToken token: UserToken, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - // perform the action - BLOCKv.performAction(name: "Redeem", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - -} diff --git a/Example/BlockV/Extensions/VatomModel+Actions.swift b/Example/BlockV/Extensions/VatomModel+Actions.swift new file mode 100644 index 00000000..216bb5f1 --- /dev/null +++ b/Example/BlockV/Extensions/VatomModel+Actions.swift @@ -0,0 +1,80 @@ +// MIT License +// +// Copyright (c) 2018 BlockV AG +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import BLOCKv + +/// Action Extensions +extension VatomModel { + + /// Clones this vAtom to the specified token. + /// + /// Note: Calling this action will trigger the action associated with this vAtom's + /// template. If an action has not been configured, an error will be generated. + /// + /// - Parameters: + /// - token: Standard UserToken (Phone, Email, or User ID) + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func clone(toToken token: UserToken, + completion: @escaping ([String: Any]?, BVError?) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // perform the action + BLOCKv.performAction(name: "Clone", payload: body) { (json, error) in + //TODO: should it be weak self? + completion(json, error) + } + + } + + /// Redeems this vAtom to the specified token. + /// + /// Note: Calling this action will trigger the action associated with this vAtom's + /// template. If an action has not been configured, an error will be generated. + /// + /// - Parameters: + /// - token: Standard UserToken (Phone, Email, or User ID) + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func redeem(toToken token: UserToken, + completion: @escaping ([String: Any]?, BVError?) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // perform the action + BLOCKv.performAction(name: "Redeem", payload: body) { (json, error) in + //TODO: should it be weak self? + completion(json, error) + } + + } + +} From eb595b7dc2ed2c9ace0dbb9908f8b6facfcd59c1 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 28 Mar 2019 12:58:30 +0200 Subject: [PATCH 020/165] Fix error handling --- .../Controllers/Actions/OutboundActionViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift index 87169f15..1c19930c 100644 --- a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift @@ -90,8 +90,8 @@ class OutboundActionViewController: UIViewController { let completionHandler: (([String: Any]?, BVError?) -> Void) = { [weak self] (data, error) in // handle error - guard let data = data, error != nil else { - print(error!.localizedDescription) + guard let data = data, error == nil else { + print(error?.localizedDescription) return } // handle success From 68de9764440ddd00f8a6d1903173c2c9205b3adb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 2 Apr 2019 15:42:12 +0200 Subject: [PATCH 021/165] Core: Feature - Data pool (#185) * Data pool --- BLOCKv.podspec | 15 +- BlockV/Core/Data Pool/Core+Overlays.swift | 66 ++ BlockV/Core/Data Pool/DataObject.swift | 32 + .../Core/Data Pool/DataObjectAnimator.swift | 176 +++++ .../Data Pool/DataObjectUpdateRecord.swift | 23 + BlockV/Core/Data Pool/DataPool.swift | 131 ++++ .../Core/Data Pool/Dictionary+DeepMerge.swift | 31 + .../Core/Data Pool/Regions/BLOCKvRegion.swift | 368 +++++++++++ .../Data Pool/Regions/InventoryRegion.swift | 289 ++++++++ .../Regions/Region+Notifications.swift | 96 +++ BlockV/Core/Data Pool/Regions/Region.swift | 622 ++++++++++++++++++ .../Core/Extensions/NSError+Convenience.swift | 21 + BlockV/Core/Helpers/KeyPath.swift | 64 +- BlockV/Core/Helpers/Performance.swift | 25 + Example/BlockV.xcodeproj/project.pbxproj | 4 + Example/Podfile.lock | 30 +- 16 files changed, 1975 insertions(+), 18 deletions(-) create mode 100644 BlockV/Core/Data Pool/Core+Overlays.swift create mode 100644 BlockV/Core/Data Pool/DataObject.swift create mode 100644 BlockV/Core/Data Pool/DataObjectAnimator.swift create mode 100644 BlockV/Core/Data Pool/DataObjectUpdateRecord.swift create mode 100644 BlockV/Core/Data Pool/DataPool.swift create mode 100644 BlockV/Core/Data Pool/Dictionary+DeepMerge.swift create mode 100644 BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift create mode 100644 BlockV/Core/Data Pool/Regions/InventoryRegion.swift create mode 100644 BlockV/Core/Data Pool/Regions/Region+Notifications.swift create mode 100644 BlockV/Core/Data Pool/Regions/Region.swift create mode 100644 BlockV/Core/Extensions/NSError+Convenience.swift create mode 100644 BlockV/Core/Helpers/Performance.swift diff --git a/BLOCKv.podspec b/BLOCKv.podspec index e1899d40..565ddb4a 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -18,12 +18,15 @@ Pod::Spec.new do |s| s.subspec 'Core' do |s| s.source_files = 'BlockV/Core/**/*.{swift}' - s.dependency 'Alamofire', '~> 4.7' # Networking - s.dependency 'Starscream', '~> 3.0.6' # Web socket - s.dependency 'JWTDecode', '~> 2.1' # JWT decoding - s.dependency 'Signals', '~> 6.0' # Elegant eventing - s.dependency 'SwiftLint', '~> 0.26' # Linter - s.dependency 'GenericJSON', '~> 1.2' # JSON + s.dependency 'Alamofire', '~> 4.7' # Networking + s.dependency 'Starscream', '~> 3.0.6' # Web socket + s.dependency 'JWTDecode', '~> 2.1' # JWT decoding + s.dependency 'Signals', '~> 6.0' # Elegant eventing + s.dependency 'SwiftLint', '~> 0.26' # Linter + s.dependency 'GenericJSON', '~> 1.2' # JSON + s.dependency 'PromiseKit', '~> 6.8' # Promises +# s.dependency 'MoreCodable' + s.dependency 'DictionaryCoding' #s.exclude_files = '**/Info*.plist' end diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift new file mode 100644 index 00000000..ccd7badc --- /dev/null +++ b/BlockV/Core/Data Pool/Core+Overlays.swift @@ -0,0 +1,66 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +// Core overlays to support Data Pool. + +// PromiseKit Overlays +internal extension Client { + + func request(_ endpoint: Endpoint) -> Promise { + return Promise { self.request(endpoint, completion: $0.resolve) } + } + +} + +internal extension API { + + /// Raw endpoints are generic over `Void`. This informs the networking client to return the raw data (instead of + /// parsing out a model). + /// + /// NB: `Void` endpoints don't partake in the auth cycle. + enum Raw { + + /// Builds the endpoint to search for vAtoms. + static func discover(_ payload: [String: Any]) -> Endpoint { + + return Endpoint(method: .post, + path: "/v1/vatom/discover", + parameters: payload) + } + + /// Build the endpoint to fetch the inventory. + static func getInventory(parentID: String, + page: Int = 0, + limit: Int = 0) -> Endpoint { + return Endpoint(method: .post, + path: "/v1/user/vatom/inventory", + parameters: [ + "parent_id": parentID, + "page": page, + "limit": limit + ] + ) + } + + /// Builds the endpoint to get a vAtom by its unique identifier. + static func getVatoms(withIDs ids: [String]) -> Endpoint { + return Endpoint(method: .post, + path: "/v1/user/vatom/get", + parameters: ["ids": ids] + ) + } + + } + +} diff --git a/BlockV/Core/Data Pool/DataObject.swift b/BlockV/Core/Data Pool/DataObject.swift new file mode 100644 index 00000000..f928e98e --- /dev/null +++ b/BlockV/Core/Data Pool/DataObject.swift @@ -0,0 +1,32 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/// Represents a raw data object, potentially without any data, which is monitored by a region. +class DataObject { + + /// Type unique identifier. + var type: String = "" + + /// Identifier. + var id: String = "" + + /// Freeform object. + var data: [String: Any]? + + /// Cached concrete type. + /// + /// Plugins use the `map` function to transform raw `data` into a concrete type. + /// This property is used to cache the transformed type. This avoids the overhead of performing the transformation. + var cached: Any? + +} diff --git a/BlockV/Core/Data Pool/DataObjectAnimator.swift b/BlockV/Core/Data Pool/DataObjectAnimator.swift new file mode 100644 index 00000000..05e4a857 --- /dev/null +++ b/BlockV/Core/Data Pool/DataObjectAnimator.swift @@ -0,0 +1,176 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/// Singleton. Responsible for receiving, storing, and exeucting changes to objects over time. +internal class DataObjectAnimator { + + /// Singleton + static let shared = DataObjectAnimator() + + /// List of regions that want animation updates + fileprivate var regions: [Weak] = [] + + /// List of changes to execute + fileprivate var changes: [PendingUpdate] = [] + + /// Animation timer + fileprivate var timer: Timer? + + /// Constructor is private + fileprivate init() { + + // subscribe to raw socket messages + BLOCKv.socket.onMessageReceivedRaw.subscribe(with: self) { descriptor in + self.onWebSocketMessage(descriptor) + } + + } + + /// Add a region to receive updates. + func add(region: Region) { + self.regions.append(Weak(value: region)) + } + + /// Stop receiving updates for the specified region. + func remove(region: Region) { + self.regions = self.regions.filter { !($0.value == nil || $0.value === region) } + } + + /// Called when there's a new event message via the WebSocket. + @objc private func onWebSocketMessage(_ descriptor: [String: Any]) { + + // we only handle state update messages here. + guard descriptor["msg_type"] as? String == "state_update" else { + return + } + + // only handle brain updates + guard let payload = descriptor["payload"] as? [String: Any], + payload["action_name"] as? String == "brain-update" else { + return + } + + // get list of next positions from the brain + guard + let vatomID = payload["id"] as? String, + let newObject = payload["new_object"] as? [String: Any], + let nextPositions = newObject["next_positions"] as? [[String: Any]] else { + return + } + + // TODO: Ensure we care about this vatom. Check if any of our regions have this vatomID + + // map coordinates to sparse object updates + let updates = nextPositions.map { PendingUpdate( + time: ($0["time"] as? Double ?? 0) / 1000, + update: DataObjectUpdateRecord( + id: vatomID, + changes: [ + "vAtom::vAtomType": [ + "geo_pos": [ + "coordinates": $0["geo_pos"] + ] + ] + ] + ) + ) } + + // ensure we have any updates + guard updates.count > 0 else { + return + } + + // fetch earliest time + var earliestTime = updates[0].time + for update in updates { + if earliestTime > update.time { + earliestTime = update.time + } + } + + // remove all pending changes for this object that are before our earliest time + self.changes = self.changes.filter { !($0.update.id == vatomID && $0.time <= earliestTime) } + + // add each item to the array + let now = Date.timeIntervalSinceReferenceDate + Date.timeIntervalBetween1970AndReferenceDate + for update in updates { + + // stop if time has passed already + if update.time < now { + continue + } + + // add it + self.changes.append(update) + + } + + // sort changes oldest to newest + self.changes.sort { $0.time - $1.time < 0 } + + // start update timer if needed + if self.timer == nil { + self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, + selector: #selector(doNextUpdate), userInfo: nil, repeats: true) + } + + } + + /// Called when the timer is executed, to do the next scheduled update. + @objc fileprivate func doNextUpdate() { + + // if there's no more updates, remove the timer and stop + if self.changes.count == 0 { + self.timer?.invalidate() + self.timer = nil + return + } + + // check if the first entry has passed yet + let now = Date.timeIntervalSinceReferenceDate + Date.timeIntervalBetween1970AndReferenceDate + if self.changes[0].time > now { + return + } + + // make list of all changes + var changes: [DataObjectUpdateRecord] = [] + while self.changes.count > 0 && self.changes[0].time <= now { + + // get the next change to execute + let change = self.changes.removeFirst() + + // add to change list + changes.append(change.update) + + } + + // execute the changes on all regions + for region in self.regions { + region.value?.update(objects: changes) + } + + } + +} + +private struct PendingUpdate { + let time: TimeInterval + let update: DataObjectUpdateRecord +} + +private class Weak { + weak var value: T? + init (value: T) { + self.value = value + } +} diff --git a/BlockV/Core/Data Pool/DataObjectUpdateRecord.swift b/BlockV/Core/Data Pool/DataObjectUpdateRecord.swift new file mode 100644 index 00000000..129ca9a7 --- /dev/null +++ b/BlockV/Core/Data Pool/DataObjectUpdateRecord.swift @@ -0,0 +1,23 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/// This contains a *sparse* data object, which represents fields which will be changed on a Data Object. +struct DataObjectUpdateRecord { + + /// The data object ID to modify. + var id: String + + /// The sparse object containing the changed fields. + var changes: [String: Any] + +} diff --git a/BlockV/Core/Data Pool/DataPool.swift b/BlockV/Core/Data Pool/DataPool.swift new file mode 100644 index 00000000..216bc17a --- /dev/null +++ b/BlockV/Core/Data Pool/DataPool.swift @@ -0,0 +1,131 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/* + # Notes: + + 1. Static vs singleton + Static is problematic for testing. It may be better to have a singleton object. + 2. DataPool should have an array of BLOCKvRegion - no need to have Region + 3. Make sure data pool works for changing user sesssion (non-parallel). + 4. Put some thought into creating isolated instances of the SDK - to allow for unit testing. + 5. onObjectAdded(), onObjectUpdated() is fired for every vatom and seems to be causing an inventory reload. + */ + +/// Data Pool plugin (region classes) must be pre-registered. +/// Region instances are created on-demand. +/// Regions are loaded from disk cache. +public final class DataPool { + + /// List of available plugins, i.e. region classes. + internal static let plugins: [Region.Type] = [ + InventoryRegion.self + ] + + /// List of active regions. + internal static var regions: [Region] = [] + + /// Session data. Stores the current user ID, or anything like that that the host uses to identify a session. + internal static var sessionInfo: [String: Any] = [:] { + didSet { + // notify regions + for reg in regions { + reg.onSessionInfoChanged(info: sessionInfo) + } + } + } + + /// Fetches or creates a named data region. + /// + /// - Parameters: + /// - id: The region ID. This is the ID of the region plugin. + /// - descriptor: Any data required by the region plugin. + /// - Returns: A Region. + internal static func region(id: String, descriptor: Any) -> Region { + + // find existing region + if let region = regions.first(where: { $0.matches(id: id, descriptor: descriptor) }) { + return region + } + + // not found, create a new region. find region plugin + guard let regionPlugin = plugins.first(where: { $0.id == id }) else { + fatalError("[DataPool] No region plugin matches ID: \(id)") + } + + // create and store region instance. + guard let region = try? regionPlugin.init(descriptor: descriptor) else { + fatalError("[DataPool] Region can't be created in this context") + // TODO: Better error handling? This shouldn't normally happen though. + } + regions.append(region) + + // load region from disk + region.loadFromCache().recover { err -> Void in + + // unable to load from disk + printBV(error: "[DataPool] Unable to load region state from disk. " + err.localizedDescription) + + }.then { _ -> Guarantee in + + // start sync'ing region data with the server + return region.synchronize() + + }.catch { err in + + // unable to load from network either! + printBV(error: "[DataPool] Unable to load region state from network. " + err.localizedDescription) + + } + + // return new region + return region + + } + + /// Removes the specified region. This is called by Region.close(), it must not be called by anything else. + /// + /// - Parameter region: The region to remove + static func removeRegion(region: Region) { + + // remove region + regions = regions.filter { $0 !== region } + + } + + /// Clear out the session info. + static func clear() { + self.sessionInfo = [:] + } + +} + +extension DataPool { + + /// Returns the global inventory region. + public static func inventory() -> Region { + return DataPool.region(id: "inventory", descriptor: "") + } + + /// Returns the global vatom regions for the specified identifier. + public static func vatom(id: String) -> Region { + return DataPool.region(id: "ids", descriptor: [id]) + } + + /// Returns the global children region for the specifed parent identifier. + public static func children(parentID: String) -> Region { + return DataPool.region(id: "children", descriptor: parentID) + } + +} diff --git a/BlockV/Core/Data Pool/Dictionary+DeepMerge.swift b/BlockV/Core/Data Pool/Dictionary+DeepMerge.swift new file mode 100644 index 00000000..12181a52 --- /dev/null +++ b/BlockV/Core/Data Pool/Dictionary+DeepMerge.swift @@ -0,0 +1,31 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +// For reference: https://stackoverflow.com/a/45221496 +extension Dictionary { + + /// Merges one dictionary with another. + public func deepMerged(with other: [Key: Value]) -> [Key: Value] { + var result: [Key: Value] = self + for (key, value) in other { + if let value = value as? [Key: Value], + let existing = result[key] as? [Key: Value], + let merged = existing.deepMerged(with: value) as? Value { + result[key] = merged + } else { + result[key] = value + } + } + return result + } +} diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift new file mode 100644 index 00000000..ea84f889 --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -0,0 +1,368 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/* + Issues: + 1. Dependecy injection of socket manager. + */ + +/// Abstract subclass of `Region`. This intermediate class handles updates from the BLOCKv Web socket. Regions should +/// subclass to automatically handle Web socket updates. +/// +/// The BLOCKv subclass is hardcoded to treat the 'objects' as vatoms. That is, the freeform object must be a vatom. +/// This is a product of the design, but it could be generalised in the future if say faces were to become a monitored +/// region. +/// +/// Roles: +/// - Handles some Web socket events (including queuing, pausing, and processing). +/// - Only 'state_update' events are intercepted. +/// > State-update messages are transformed into DataObjectUpdateRecord (Sparse Object) and use to update the region. +/// - Data transformations (map) to VatomModel +/// - Notifications +/// > Add, update, remove notification are broadcast for the changed vatom. An update is always emitted for the parent +/// of the changed vatom. This is useful since the parent can then update its state, e.g. on child removal. +class BLOCKvRegion: Region { + + /// Constructor + required init(descriptor: Any) throws { + try super.init(descriptor: descriptor) + + // subscribe to socket connections + BLOCKv.socket.onConnected.subscribe(with: self) { _ in + self.onWebSocketConnect() + } + + // subscribe to raw socket messages + BLOCKv.socket.onMessageReceivedRaw.subscribe(with: self) { descriptor in + self.onWebSocketMessage(descriptor) + } + + // monitor for timed updates + DataObjectAnimator.shared.add(region: self) + + } + + /* + The initialiser below allows for dependency injection of the socket manager. + */ + + /// Initialize with a descriptor and a socket. +// init(descriptor: Any, socket: WebSocketManager) throws { +// try super.init(descriptor: descriptor) +// +// // subscribe to socket connections +// socket.onConnected.subscribe(with: self) { _ in +// self.onWebSocketConnect() +// } +// +// // subscribe to raw socket messages +// socket.onMessageReceivedRaw.subscribe(with: self) { descriptor in +// self.onWebSocketMessage(descriptor) +// } +// +// // Monitor for timed updates +// DataObjectAnimator.shared.add(region: self) +// } + + deinit { + + // stop listening for animation updates + DataObjectAnimator.shared.remove(region: self) + + } + + /// Queue of pending messages. + private var queuedMessages: [[String: Any]] = [] + /// Boolean value that is `true` if message processing is paused. + private var socketPaused = false + /// Boolean valie that is `true` if a message is currently being processed. + private var socketProcessing = false + + /// Called when this region is going to be shut down. + override func close() { + super.close() + + //FIXME: Double check that signals do not need to be unsubscribed from. + + // remove listeners + DataObjectAnimator.shared.remove(region: self) + + } + + /// Called to pause processing of socket messages. + func pauseMessages() { + self.socketPaused = true + } + + /// Called to resume processing of socket messages. + func resumeMessages() { + + // unpause + self.socketPaused = false + + // process next message if needed + if !self.socketProcessing { + self.processNextMessage() + } + + } + + /// Called when the Web socket re-connects. + @objc func onWebSocketConnect() { + + // mark as unstable + self.synchronized = false + + // re-sync the entire thing. Don't worry about synchronize() getting called while it's running already, + // it handles that case. + self.synchronize() + + } + + /// Called when there's a new event message via the Web socket. + @objc func onWebSocketMessage(_ descriptor: [String: Any]) { + + // add to queue + self.queuedMessages.append(descriptor) + + // process it if necessary + if !self.socketPaused && !self.socketProcessing { + self.processNextMessage() + } + + } + + /// Called to process the next WebSocket message. + func processNextMessage() { + + // stop if socket is paused + if socketPaused { + return + } + + // stop if already processing + if socketProcessing { return } + socketProcessing = true + + // get next msg to process + if queuedMessages.count == 0 { + + // no more messages + self.socketProcessing = false + return + + } + + // process message + let msg = queuedMessages.removeFirst() + self.processMessage(msg) + + // done, process next message + self.socketProcessing = false + self.processNextMessage() + + } + + /// Processes a raw Web socket message. + /// + /// Only 'state_update' events intercepted and used to perform parital updates on the region's objects. + /// Message processing is not paused for 'state_update' events. + func processMessage(_ msg: [String: Any]) { + + // get info + guard let msgType = msg["msg_type"] as? String else { return } + guard let payload = msg["payload"] as? [String: Any] else { return } + guard let newData = payload["new_object"] as? [String: Any] else { return } + guard let vatomID = payload["id"] as? String else { return } + if msgType != "state_update" { + return + } + + // update existing objects + let changes = DataObjectUpdateRecord(id: vatomID, changes: newData) + self.update(objects: [changes]) + + } + + // MARK: - Transformations + + /// Map data objects to Vatom objects. + /// + /// This is the primary transformation function which converts freeform data pool objects into concrete types. + override func map(_ object: DataObject) -> Any? { + + //FIXME: This method is synchronous which may affect performance. + + /* + How to transfrom data objects into types? + + Data > Decoder > Type (decode from external representation) + Type > Encoder > Data (encode for extrernal representation) + + Facts: + - Data pool store heterogeneous object of type [String: Any] - it is type independent. + - `map(:DataObject)` needs to transformt this into a concrete type. + - The codable machinary is good for data <> native type transformations. + + + Options: + 1. Convert [String: Any] into Data, then Data into Type (very inefficient). + 2. Write an init(descriptor: [String: Any])` - this allows VatomModel to be initialized with a dictionary. + > This sucks because a) it's a lot of work, b) does not leverage the CodingKeys of Codable conformance. + 3. Write a Decoder with transforms [String: Any] into Type AND leverages the CodingKeys + */ + + // only handle vatoms + guard object.type == "vatom" else { + return nil + } + + // stop if no data available + guard var objectData = object.data else { + return nil + } + + // get vatom info + guard let template = object.data![keyPath: "vAtom::vAtomType.template"] as? String else { return nil } + + // fetch all faces linked to this vatom + let faces = objects.values.filter { $0.type == "face" && $0.data?["template"] as? String == template } + objectData["faces"] = faces.map { $0.data } + + // fetch all actions linked to this vatom + let actionNamePrefix = template + "::Action::" + let actions = objects.values.filter { $0.type == "action" && ($0.data?["name"] as? String)? + .starts(with: actionNamePrefix) == true } + objectData["actions"] = actions.map { $0.data } + + do { + if JSONSerialization.isValidJSONObject(objectData) { + let rawData = try JSONSerialization.data(withJSONObject: objectData) + let vatoms = try JSONDecoder.blockv.decode(VatomModel.self, from: rawData) + return vatoms + } else { + throw NSError.init("Invalid JSON for Vatom: \(object.id)") + } + } catch { + printBV(error: error.localizedDescription) + return nil + } + + } + + // MARK: - Notifications + + // - Add + + /// Called when an object is about to be added. +// override func will(add object: DataObject) { +// +// // Notify parent as well +// guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { +// return +// } +// DispatchQueue.main.async { +// // broadcast update the vatom's parent +// self.emit(.objectUpdated, userInfo: ["id": parentID]) //FIXME: Does this make sense? If the parent calls list children at this point it will not have updated yet +//// // broadbast the add +// self.emit(.objectAdded, userInfo: ["id": object.id]) +// } +// +// } + + override func did(add object: DataObject) { + // Notify parent as well + guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + DispatchQueue.main.async { + // broadcast update the vatom's parent + self.emit(.objectUpdated, userInfo: ["id": parentID]) + // broadbast the add + self.emit(.objectAdded, userInfo: ["id": object.id]) + } + } + + // - Update + + /// Called when an object is about to be updated. + override func will(update object: DataObject, withFields: [String: Any]) { + + // notify parent as well + guard let oldParentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + guard let newParentID = (withFields["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + DispatchQueue.main.async { + self.emit(.objectUpdated, userInfo: ["id": oldParentID]) + self.emit(.objectUpdated, userInfo: ["id": newParentID]) + } + + } + + /// Called when an object is about to be updated. + override func did(update object: DataObject, withFields: [String: Any]) { + + // notify parent as well + guard let oldParentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + guard let newParentID = (withFields["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + DispatchQueue.main.async { + self.emit(.objectUpdated, userInfo: ["id": oldParentID]) + self.emit(.objectUpdated, userInfo: ["id": newParentID]) + } + + } + + /// Called when an object is about to be updated. + override func will(update: DataObject, keyPath: String, oldValue: Any?, newValue: Any?) { + + // check if parent ID is changing + if keyPath != "vAtom::vAtomType.parent_id" { + return + } + + // notify parent as well + guard let oldParentID = oldValue as? String else { return } + guard let newParentID = newValue as? String else { return } + DispatchQueue.main.async { + self.emit(.objectUpdated, userInfo: ["id": oldParentID]) + self.emit(.objectUpdated, userInfo: ["id": newParentID]) + } + + } + + // - Remove + + /// Called when an object is about to be removed. + override func will(remove object: DataObject) { + + // notify parent as well + guard let parentID = (object.data?["vAtom::vAtomType"] as? [String: Any])?["parent_id"] as? String else { + return + } + DispatchQueue.main.async { + if parentID != "." { + self.emit(.objectUpdated, userInfo: ["id": parentID]) + } + self.emit(.objectRemoved, userInfo: ["id": object.id]) + } + + } + +} diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift new file mode 100644 index 00000000..1ce08e51 --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -0,0 +1,289 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/* + Issues + 2. Dependecy inject session info (don't rely on static DataPool.sessionInfo) + 3. Convert discover to inventory call (server dependent). + 4. Convert request to auth-lifecylce participating requests. + */ + +/// This region plugin provides access to the current user's inventory. +/// +/// Two primary functions: +/// 1. Overrides load (to fetch all object from the server). +/// 2. Processing 'inventory' messages. +/// > Importanly, for vatom additions, this pauses the message processing, fetches the vatom, and only resumes the +/// message processing. +class InventoryRegion: BLOCKvRegion { + + /// Plugin identifier. + override class var id: String { return "inventory" } + + /// Constructor. + required init(descriptor: Any) throws { + try super.init(descriptor: descriptor) + + print(DataPool.sessionInfo["userID"]) + + // make sure we have a valid current user + guard let userID = DataPool.sessionInfo["userID"] as? String, !userID.isEmpty else { + throw NSError("You cannot query the inventory region without being logged in.") + } + + } + + /// Current user ID. + let currentUserID = DataPool.sessionInfo["userID"] as? String ?? "" + + /// Our state key is the current user's Id. + override var stateKey: String { + return "inventory:" + currentUserID + } + + /// Returns `true` if this region matches the `id` and `descriptor`. + /// + /// There should only be one inventory region. + override func matches(id: String, descriptor: Any) -> Bool { + return id == "inventory" + } + + /// Called when session info changes. This should trigger a clean up process since the region may no longer be + /// valid. + override func onSessionInfoChanged(info: Any?) { + // shut down this region if the current user changes. + self.close() + } + + /// Load current state from the server. + override func load() -> Promise<[String]?> { + + // pause websocket events + self.pauseMessages() + + // fetch all pages recursively + return self.fetch().ensure { + + // resume websocket events + self.resumeMessages() + + } + + } + + /// Fetches all objects from the server. + /// + /// Recursivly pages through the server's pool until all object have been found. + fileprivate func fetch(page: Int = 1, previousItems: [String] = []) -> Promise<[String]?> { + + // stop if closed + if closed { + return Promise.value(previousItems) + } + + // execute it + printBV(info: "[DataPool > InventoryRegion] Loading page \(page), got \(previousItems.count) items so far...") + + // build raw request + let endpoint = API.Raw.getInventory(parentID: "*", page: page, limit: 100) + + return BLOCKv.client.request(endpoint).then { data -> Promise<[String]?> in + + //TODO: Use a json returning request instead of a raw data request. + + // convert + guard let object = try? JSONSerialization.jsonObject(with: data), let json = object as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + guard let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + //TODO: This should be factored out. + + // create list of items + var items: [DataObject] = [] + var ids = previousItems + + // add faces to the list + guard let faces = payload["faces"] as? [[String: Any]] else { return Promise.value(ids) } + for face in faces { + + // add data object + let obj = DataObject() + obj.type = "face" + obj.id = face["id"] as? String ?? "" + obj.data = face + items.append(obj) + ids.append(obj.id) + + } + + // add actions to the list + guard let actions = payload["actions"] as? [[String: Any]] else { return Promise.value(ids) } + for action in actions { + + // add data object + let obj = DataObject() + obj.type = "action" + obj.id = action["name"] as? String ?? "" + obj.data = action + items.append(obj) + ids.append(obj.id) + + } + + // add vatoms to the list + guard let vatomInfos = payload["vatoms"] as? [[String: Any]] else { return Promise.value(ids) } + for vatomInfo in vatomInfos { + + // add data object + let obj = DataObject() + obj.type = "vatom" + obj.id = vatomInfo["id"] as? String ?? "" + obj.data = vatomInfo + items.append(obj) + ids.append(obj.id) + + } + + // add data objects + self.add(objects: items) + + // if no more data, stop + if vatomInfos.count == 0 { + return Promise.value(ids) + } + + // done, get next page + return self.fetch(page: page + 1, previousItems: ids) + + } + + } + + /// Called on Web socket message. + /// + /// Allows super to handle 'state_update', then goes on to process 'inventory' events. + /// Message process is paused for 'inventory' events which indicate a vatom was added. Since the vatom must + /// be fetched from the server. + override func processMessage(_ msg: [String: Any]) { + super.processMessage(msg) + + // get info + guard let msgType = msg["msg_type"] as? String else { return } + guard let payload = msg["payload"] as? [String: Any] else { return } + guard let oldOwner = payload["old_owner"] as? String else { return } + guard let newOwner = payload["new_owner"] as? String else { return } + guard let vatomID = payload["id"] as? String else { return } + if msgType != "inventory" { + return + } + + // check if this is an incoming or outgoing vatom + if oldOwner == self.currentUserID && newOwner != self.currentUserID { + + // vatom is no longer owned by us + self.remove(ids: [vatomID]) + + } else if oldOwner != self.currentUserID && newOwner == self.currentUserID { + + // vatom is now our inventory + // pause this instance's message processing and fetch vatom payload + self.pauseMessages() + + let endpoint = API.Raw.getVatoms(withIDs: [vatomID]) + BLOCKv.client.request(endpoint).done { data in + + // convert + guard let object = try? JSONSerialization.jsonObject(with: data), + let json = object as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + guard let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + // add vatom to new objects list + var items: [DataObject] = [] + + // add faces to the list + guard let faces = payload["faces"] as? [[String: Any]] else { return } + for face in faces { + + // add data object + let obj = DataObject() + obj.type = "face" + obj.id = face["id"] as? String ?? "" + obj.data = face + items.append(obj) + + } + + // add actions to the list + guard let actions = payload["actions"] as? [[String: Any]] else { return } + for action in actions { + + // add data object + let obj = DataObject() + obj.type = "action" + obj.id = action["name"] as? String ?? "" + obj.data = action + items.append(obj) + + } + + // add vatoms to the list + guard let vatomInfos = payload["vatoms"] as? [[String: Any]] else { return } + for vatomInfo in vatomInfos { + + // add data object + let obj = DataObject() + obj.type = "vatom" + obj.id = vatomInfo["id"] as? String ?? "" + obj.data = vatomInfo + items.append(obj) + + } + + // add new objects + self.add(objects: items) + + // notify vatom received + guard let vatom = self.get(id: vatomInfos[0]["id"] as? String ?? "") as? VatomModel else { + printBV(error: "[DataPool > InventoryRegion] Couldn't process incoming vatom") + return + } + + //FIXME: Consider where to add onReceived + + }.catch { error in + printBV(error: "[InventoryRegion] Unable to fetch inventory. \(error.localizedDescription)") + }.finally { + // resume WebSocket processing + self.resumeMessages() + } + + } else { + + // logic error, old owner and new owner cannot be the same + printBV(error: "[InventoryRegion] Logic error in WebSocket message, old_owner and new_owner shouldn't be the same: \(vatomID)") + + } + + } + +} diff --git a/BlockV/Core/Data Pool/Regions/Region+Notifications.swift b/BlockV/Core/Data Pool/Regions/Region+Notifications.swift new file mode 100644 index 00000000..6046f0a3 --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/Region+Notifications.swift @@ -0,0 +1,96 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/// Possible events +public enum RegionEvent: String { + + /// Triggered when any data in the region changes. This also indicates that there is no longer an error. + case updated = "region.updated" + + /// Triggered when an object is added. + case objectAdded = "region.object.addded" + + /// Triggered when an object is removed. + case objectRemoved = "region.object.removed" + + /// When a data object changes. userInfo["id"] is the ID of the changed object. + case objectUpdated = "region.object.updated" + + /// When an error occurs. userInfo["error"] is the error. You can also access `region.error` to get the error. + case error = "region.error" + + /// Lifecycle events + + /// Triggered when the region stabalizes. + case stabalized = "region.stabalized" + /// Triggered when the region destabalizes. + case destabalized = "region.destablaized" + /// Triggered when the region begins synchronization. + case synchronizing = "region.synchronizing" + +} + +extension RegionEvent { + + /// Convert the notification to a Notification.Name + public var asNotification: Notification.Name { + return Notification.Name(rawValue: self.rawValue) + } + +} + +/// Helpers to deal with events +extension Region { + + /// Add a listener. + public func addObserver(_ observer: Any, selector: Selector, name: RegionEvent) { + NotificationCenter.default.addObserver(observer, + selector: selector, + name: Notification.Name(name.rawValue), + object: self) + } + + /// Remove a listener. + public func removeObserver(_ observer: Any, name: RegionEvent) { + NotificationCenter.default.removeObserver(observer, + name: Notification.Name(name.rawValue), + object: self) + } + + /// Removes a set listener. + /// TODO: Find a way to auto remove block listeners when their container object is dealloc'd? + public typealias RemoveObserverFunction = () -> Void + + /// Add a listener + public func listen(for name: RegionEvent, handler: @escaping (Notification) -> Void) -> RemoveObserverFunction { + + // register observer + let observer = NotificationCenter.default.addObserver(forName: Notification.Name(name.rawValue), + object: self, queue: OperationQueue.main, using: handler) + + // return a function which can be called to remove the observer + return { + NotificationCenter.default.removeObserver(observer) + } + + } + + /// Emits an event. This is used by Region and it's subclasses only. + func emit(_ name: RegionEvent, userInfo: [String: Any] = [:]) { + + // send notification + NotificationCenter.default.post(name: Notification.Name(name.rawValue), object: self, userInfo: userInfo) + + } + +} diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift new file mode 100644 index 00000000..555c9a21 --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -0,0 +1,622 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/* + # Notes: + + ## Filter & Sort + + Both of these should become classes (which do not inherit from Region). They provide a 'view into' a region. + + Caller: + + DataPool.region(id: "inventory", descriptor: [:]).filter { $0.dateModified > a } + DataPool.region(id: "inventory", descriptor: [:]).sort { $0.title } + + Adding sort directly to Region is hard beacuse multiple callers may use use the same regions - all with different + sorting predicates. So sorting must be cofigurable (isolated) per caller. + + */ + +/// An abstract class that manages a complete collection of objects (a.k.a Regions). +/// +/// Regions are generally "id-complete". That is, the local region should have a complete copy of all remote objects. +/// +/// Roles: +/// - In memory store of objects. +/// - Keep track of synchornization state. +/// - Loads new objects. +/// - CRUD (including update with spare objects). +/// - Change Notifications. +/// - Persistance. +public class Region { + + /// Constructor + required init(descriptor: Any) throws { } + + /// This region plugin's ID. + class var id: String { + assertionFailure("subclass-should-override") + return "" + } + + /// `true` if this region contains temporary objects which should not be cached to disk, `false` otherwise. + let noCache = false + + /// All objects currently in our cache. + var objects: [String: DataObject] = [:] + + /// `true` if data in this region is in sync with the backend. + public internal(set) var synchronized = false { + didSet { + if synchronized { + self.emit(.stabalized, userInfo: [:]) + } else { + self.emit(.destabalized, userInfo: [:]) + } + } + } + + /// Contains the current error. + public internal(set) var error: Error? + + /// An ID which uniquely identifies this region. Used for caching purposes. + var stateKey: String { + assertionFailure("subclass-should-override") + return "" + } + + /// `true` if this region has been closed. + public fileprivate(set) var closed = false + + /// Re-synchronizes the region by manually fetching objects from the server again. + public func forceSynchronize() -> Guarantee { + self.synchronized = false + return self.synchronize() + } + + /// Currently executing synchronization promise. `nil` if there is no synchronization underway. + private var _syncPromise: Guarantee? + + /// Attempts to stablaize the region by querying the backend for all data. + /// + /// - Returns: Promise which resolves when complete. + @discardableResult + public func synchronize() -> Guarantee { + + self.emit(.synchronizing, userInfo: [:]) + + // stop if already running + if let promise = _syncPromise { + return promise + } + + // remove pending error + self.error = nil + self.emit(.updated) + + // stop if already in sync + if synchronized { + return Guarantee() + } + + // ask the subclass to load it's data + printBV(info: "[DataPool > Region] Starting synchronization for region \(self.stateKey)") + + // load objects + _syncPromise = self.load().map { ids -> Void in + + /* + The subclass is expected to call the add method as it finds object, and then, once + all ids are known, return all the newly added ids. + Super (this) then calls the remove method on all ids that are no longer present. + */ + + // check if subclass returned an array of IDs + if let ids = ids { + + // create a list of keys to remove + var keysToRemove: [String] = [] + for id in self.objects.keys { + + // check if it's in our list + if !ids.contains(id) { + keysToRemove.append(id) + } + + } + + // Rrmove objects + self.remove(ids: keysToRemove) + + } + + // data is up to date + self.synchronized = true + self._syncPromise = nil + printBV(info: "[DataPool > Region] Region '\(self.stateKey)' is now in sync!") + + }.recover { err in + // error handling, notify listeners of an error + self._syncPromise = nil + self.error = err + printBV(error: "[DataPool > Region] Unable to load: " + err.localizedDescription) + self.emit(.error, userInfo: ["error": err]) + } + + // return promise + return _syncPromise! + + } + + /// Start load of remote objects. The promise should resolve once the region is up to date and provides the + /// set of object ids. + /// + /// This function should fetch the _entire_ region. + /// + /// - Returns: A promise which will fullsil with an array of object IDs, or `nil`. If an array of object IDs is + /// returned, any IDs not in this list should be removed from the region. + func load() -> Promise<[String]?> { + return Promise(error: NSError("Subclasses must override Region.load()")) + } + + /// Stop and destroy this region. Subclasses can override this to do stuff on close. + public func close() { + + // notify data pool we have closed + DataPool.removeRegion(region: self) + // we're closed + self.closed = true + + } + + /// Checks if the specified query matches our region. This is used to identify if a region request + /// can be satisfied by this region, or if a new region should be created. + /// + /// - Parameters: + /// - id: The region plugin ID + /// - descriptor: Region-specific filter data + /// - Returns: True if the described region is this region. + func matches(id: String, descriptor: Any) -> Bool { + fatalError("Subclasses muct override Region.matches()") + } + + /// Add DataObjects to our pool. + /// + /// - Parameter objects: The objects to add + func add(objects: [DataObject]) { + + // go through each object + for obj in objects { + + // skip if no data + guard let data = obj.data else { + continue + } + + // check if exists already + if let existingObject = self.objects[obj.id] { + + // notify + self.will(update: existingObject, withFields: data) + + // it exists already, update the object (replace data) + existingObject.data = data + existingObject.cached = nil + + self.did(update: existingObject, withFields: data) + + } else { + + // it does not exist, add it + self.objects[obj.id] = obj + + // notify + self.did(add: obj) + + } + + // emit event + //FIXME: Why was this being broadcast? +// self.emit(.objectUpdated, userInfo: ["id": obj.id]) + + } + + // Notify updated + if objects.count > 0 { + self.emit(.updated) + self.save() + } + + } + + /// Updates data objects within our pool. + /// + /// - Parameter objects: The list of changes to perform to our data objects. + func update(objects: [DataObjectUpdateRecord]) { + + // batch emit events, so if a object is updated multiple times, only one event is sent + var changedIDs = Set() + + for obj in objects { + + // fetch existing object + guard let existingObject = self.objects[obj.id] else { + continue + } + + // stop if existing object doesn't have the full data + guard let existingData = existingObject.data else { + continue + } + + // notify + self.will(update: existingObject, withFields: obj.changes) + + // update fields + existingObject.data = existingData.deepMerged(with: obj.changes) + + // clear cached values + existingObject.cached = nil + + // notify + self.did(update: existingObject, withFields: obj.changes) + + // emit event + changedIDs.insert(obj.id) + + } + + // notify each item that was updated + for id in changedIDs { + self.emit(.objectUpdated, userInfo: ["id": id]) + } + + // notify overall update + if changedIDs.count > 0 { + self.emit(.updated) + self.save() + } + + } + + /// Removes the specified objects from our pool. + /// + /// - Parameter ids: The IDs of objects to remove + func remove(ids: [String]) { + + // remove all data objects with the specified IDs + var didUpdate = false + for id in ids { + + // remove it + guard let object = self.objects.removeValue(forKey: id) else { + continue + } + + // notify + didUpdate = true + self.will(remove: object) //FIXME: Should be didRemove? + + } + + // notify region updated + if didUpdate { + self.emit(.updated) + self.save() + } + + } + + /// If a region plugin depends on the session data, it may override this method and `self.close()` itself if needed. + /// + /// - Parameter info: The new app-specific session info + func onSessionInfoChanged(info: Any?) {} + + /// If the plugin wants, it can map DataObjects to another type. This takes in a DataObject and returns a new type. + /// If the plugin returns `nil`, the specified data object will not be returned and will be skipped. + /// + /// The default implementation simply returns the DataObject. + /// + /// - Parameter object: The DataObject as input + /// - Returns: The new output object. + func map(_ object: DataObject) -> Any? { + return object + } + + /// Returns all the objects within this region. Waits until the region is stable first. + /// + /// - Returns: Array of objects. Check the region-specific map() function to see what types are returned. + public func getAllStable() -> Guarantee<[Any]> { + + // synchronize now + return self.synchronize().map({ + return self.getAll() + }) + + } + + /// Returns all the objects within this region. Does NOT wait until the region is stable first. + public func getAll() -> [Any] { + + // create array of all items + var items: [Any] = [] + for object in objects.values { + + // check for cached concrete type + if let cached = object.cached { + items.append(cached) + continue + } + + // map to the plugin's intended type + guard let mapped = self.map(object) else { + continue + } + + // cache it + object.cached = mapped + + // add to list + items.append(mapped) + + } + + // done + return items + + } + + /// Returns an object within this region by it's ID. Waits until the region is stable first. + public func getStable(id: String) -> Guarantee { + + // synchronize now + return self.synchronize().map { + // get item + return self.get(id: id) + } + + } + + /// Returns an object within this region by it's ID. + public func get(id: String) -> Any? { + + // get object + guard let object = objects[id] else { + return nil + } + + // check for cached concrete type + if let cached = object.cached { + return cached + } + + // map to the plugin's intended type + guard let mapped = self.map(object) else { + return nil + } + + // cache it + object.cached = mapped + + // done + return mapped + + } + + /// Load objects from local storage. + func loadFromCache() -> Promise { + + // get filename + let startTime = Date.timeIntervalSinceReferenceDate + let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") + + // get temporary file location + let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) + .appendingPathExtension("json") + + // read data + guard let data = try? Data(contentsOf: file) else { + printBV(error: ("[DataPool > Region] Unable to read cached data")) + return Promise() + } + + // parse JSON + guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[Any]] else { + printBV(error: "[DataPool > Region] Unable to parse cached JSON") + return Promise() + } + + // create objects + let objects = json.map { fields -> DataObject? in + + // get fields + guard let id = fields[0] as? String, let type = fields[1] as? String, + let data = fields[2] as? [String: Any] else { + return nil + } + + // create DataObject + let obj = DataObject() + obj.id = id + obj.type = type + obj.data = data + return obj + + } + + // Strip out nils + let cleanObjects = objects.compactMap { $0 } + + // add objects + self.add(objects: cleanObjects) + + // done + let delay = (Date.timeIntervalSinceReferenceDate - startTime) * 1000 + printBV(info: ("[DataPool > Region] Loaded \(cleanObjects.count) from cache in \(Int(delay))ms")) + return Promise() + + } + + var saveTask: DispatchWorkItem? + + /// Saves the region to local storage. + func save() { + + // cancel the pending save task + if saveTask != nil { + saveTask?.cancel() + } + + // create save task + saveTask = DispatchWorkItem { () -> Void in + + // create data to save + let startTime = Date.timeIntervalSinceReferenceDate + let json = self.objects.values.map { return [ + $0.id, + $0.type, + $0.data ?? [:] + ]} + + // convert to JSON + guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else { + printBV(error: ("[DataPool > Region] Unable to convert data objects to JSON")) + return + } + + // get filename + let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") + + // get temporary file location + let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) + .appendingPathExtension("json") + + // make sure folder exists + try? FileManager.default.createDirectory(at: file.deletingLastPathComponent(), + withIntermediateDirectories: true, attributes: nil) + + // write file + do { + try data.write(to: file) + } catch let err { + printBV(error: ("[DataPool > Region] Unable to save data to disk: " + err.localizedDescription)) + return + } + + // done + let delay = (Date.timeIntervalSinceReferenceDate - startTime) * 1000 + printBV(info: ("[DataPool > Region] Saved \(self.objects.count) items to disk in \(Int(delay))ms")) + + } + + // Debounce save task + DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: saveTask!) + + } + + /// Call this to undo an action. + typealias UndoFunction = () -> Void + + /// Change a field, and return a function which can be called to undo the change. + /// + /// - Parameters: + /// - id: The object ID + /// - keyPath: The key to change + /// - value: The new value + /// - Returns: An undo function + func preemptiveChange(id: String, keyPath: String, value: Any) -> UndoFunction { + + // get object. If it doesn't exist, do nothing and return an undo function which does nothing. + guard let object = objects[id], object.data != nil else { + return {} + } + + // get current value + let oldValue = object.data![keyPath: KeyPath(keyPath)] + + // notify + self.will(update: object, keyPath: keyPath, oldValue: oldValue, newValue: value) + + // update to new value + object.data![keyPath: KeyPath(keyPath)] = value + object.cached = nil + self.emit(.objectUpdated, userInfo: ["id": id]) + self.emit(.updated) + self.save() + + // return undo function + return { + + // notify + self.will(update: object, keyPath: keyPath, oldValue: value, newValue: oldValue) + + // update to new value + object.data![keyPath: KeyPath(keyPath)] = oldValue + object.cached = nil + self.emit(.objectUpdated, userInfo: ["id": id]) + self.emit(.updated) + self.save() + + } + + } + + /// Remove an object, and return an undo function. + /// + /// - Parameter id: The object ID to remove + /// - Returns: An undo function + func preemptiveRemove(id: String) -> UndoFunction { + + // remove object + guard let removedObject = objects.removeValue(forKey: id) else { + // no object, do nothing + return {} + } + + // notify + self.will(remove: removedObject) //FIXME: should be didRemove + self.emit(.updated) + self.save() + + // return undo function + return { + + // check that a new object wasn't added in the mean time + guard self.objects[id] == nil else { + return + } + + // notify + self.will(add: removedObject) + self.add(objects: [removedObject]) + self.save() + + } + + } + + // MARK: - Listener functions, can be overridden by subclasses + + func will(add: DataObject) {} + func will(update: DataObject, withFields: [String: Any]) {} + func will(update: DataObject, keyPath: String, oldValue: Any?, newValue: Any?) {} + func will(remove object: DataObject) {} + + func did(add: DataObject) {} + func did(update: DataObject, withFields: [String: Any]) {} +// func did(update: DataObject, keyPath: String, oldValue: Any?, newValue: Any?) {} +// func did(remove object: DataObject) {} + +} diff --git a/BlockV/Core/Extensions/NSError+Convenience.swift b/BlockV/Core/Extensions/NSError+Convenience.swift new file mode 100644 index 00000000..2baa61f0 --- /dev/null +++ b/BlockV/Core/Extensions/NSError+Convenience.swift @@ -0,0 +1,21 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +extension NSError { + + /// Creates an error with the specified error message. + convenience init(_ text: String, code: Int = 0) { + self.init(domain: "blockv_sdk", code: code, userInfo: [ NSLocalizedDescriptionKey: text ]) + } + +} diff --git a/BlockV/Core/Helpers/KeyPath.swift b/BlockV/Core/Helpers/KeyPath.swift index 7277a8b3..6cef1cf6 100644 --- a/BlockV/Core/Helpers/KeyPath.swift +++ b/BlockV/Core/Helpers/KeyPath.swift @@ -12,7 +12,7 @@ import Foundation /// Simple struct that models a key path. -public struct KeyPath: Equatable { +struct KeyPath: Equatable { private(set) var segments: [String] var isEmpty: Bool { return segments.isEmpty } @@ -32,7 +32,7 @@ public struct KeyPath: Equatable { } /// Initializes a KeyPath with a string of the form "this.is.a.keypath" -public extension KeyPath { +extension KeyPath { /// This init is only required for testing - file access is limited to internal to allow for testing export init() { @@ -46,13 +46,67 @@ public extension KeyPath { /// Initializ a KeyPath using a string literal. extension KeyPath: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { + init(stringLiteral value: String) { self.init(value) } - public init(unicodeScalarLiteral value: String) { + init(unicodeScalarLiteral value: String) { self.init(value) } - public init(extendedGraphemeClusterLiteral value: String) { + init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } + +// MARK: - Dictionary Keypath + +extension Dictionary where Key == String { + subscript(keyPath keyPath: KeyPath) -> Any? { + get { + switch keyPath.headAndTail() { + case nil: + // key path is empty. + return nil + case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty: + // Reached the end of the key path. + let key = Key(stringLiteral: head) + return self[key] + case let (head, remainingKeyPath)?: + // Key path has a tail we need to traverse. + let key = Key(stringLiteral: head) + switch self[key] { + case let nestedDict as [Key: Any]: + // Next nest level is a dictionary. + // Start over with remaining key path. + return nestedDict[keyPath: remainingKeyPath] + default: + // Next nest level isn't a dictionary. + // Invalid key path, abort. + return nil + } + } + } + set { + switch keyPath.headAndTail() { + case nil: + // key path is empty. + return + case let (head, remainingKeyPath)? where remainingKeyPath.isEmpty: + // Reached the end of the key path. + let key = Key(stringLiteral: head) + self[key] = newValue as? Value + case let (head, remainingKeyPath)?: + let key = Key(stringLiteral: head) + let value = self[key] + switch value { + case var nestedDict as [Key: Any]: + // Key path has a tail we need to traverse + nestedDict[keyPath: remainingKeyPath] = newValue + self[key] = nestedDict as? Value + default: + // Invalid keyPath + return + } + } + } + } +} diff --git a/BlockV/Core/Helpers/Performance.swift b/BlockV/Core/Helpers/Performance.swift new file mode 100644 index 00000000..6dc2ba04 --- /dev/null +++ b/BlockV/Core/Helpers/Performance.swift @@ -0,0 +1,25 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +func BVTimeBlock(_ block: () -> Void) -> TimeInterval { + var info = mach_timebase_info() + guard mach_timebase_info(&info) == KERN_SUCCESS else { return -1 } + + let start = mach_absolute_time() + block() + let end = mach_absolute_time() + + let elapsed = end - start + let nanos = elapsed * UInt64(info.numer) / UInt64(info.denom) + return TimeInterval(nanos) / TimeInterval(NSEC_PER_SEC) +} diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index a46953a6..99000e99 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -557,8 +557,10 @@ "${BUILT_PRODUCTS_DIR}/FLAnimatedImage/FLAnimatedImage.framework", "${BUILT_PRODUCTS_DIR}/GenericJSON/GenericJSON.framework", "${BUILT_PRODUCTS_DIR}/JWTDecode/JWTDecode.framework", + "${BUILT_PRODUCTS_DIR}/MoreCodable/MoreCodable.framework", "${BUILT_PRODUCTS_DIR}/NVActivityIndicatorView/NVActivityIndicatorView.framework", "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework", + "${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework", "${BUILT_PRODUCTS_DIR}/Signals/Signals.framework", "${BUILT_PRODUCTS_DIR}/Starscream/Starscream.framework", "${BUILT_PRODUCTS_DIR}/VatomFace3D/VatomFace3D.framework", @@ -570,8 +572,10 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FLAnimatedImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GenericJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JWTDecode.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoreCodable.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NVActivityIndicatorView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Signals.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Starscream.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/VatomFace3D.framework", diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 9e3f9ede..2098b96b 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,11 +1,13 @@ PODS: - - Alamofire (4.8.1) + - Alamofire (4.8.2) - BLOCKv (3.1.0): - BLOCKv/Face (= 3.1.0) - BLOCKv/Core (3.1.0): - Alamofire (~> 4.7) - GenericJSON (~> 1.2) - JWTDecode (~> 2.1) + - MoreCodable + - PromiseKit (~> 6.8) - Signals (~> 6.0) - Starscream (~> 3.0.6) - SwiftLint (~> 0.26) @@ -16,13 +18,23 @@ PODS: - FLAnimatedImage (1.0.12) - GenericJSON (1.2.0) - JWTDecode (2.2.0) + - MoreCodable (0.2.0) - Nuke (7.5.2) - NVActivityIndicatorView (4.6.1): - NVActivityIndicatorView/Presenter (= 4.6.1) - NVActivityIndicatorView/Presenter (4.6.1) - - Signals (6.0.0) + - PromiseKit (6.8.3): + - PromiseKit/CorePromise (= 6.8.3) + - PromiseKit/Foundation (= 6.8.3) + - PromiseKit/UIKit (= 6.8.3) + - PromiseKit/CorePromise (6.8.3) + - PromiseKit/Foundation (6.8.3): + - PromiseKit/CorePromise + - PromiseKit/UIKit (6.8.3): + - PromiseKit/CorePromise + - Signals (6.0.1) - Starscream (3.0.6) - - SwiftLint (0.30.1) + - SwiftLint (0.31.0) - VatomFace3D (2.0.0): - BLOCKv/Face - FLAnimatedImage @@ -40,8 +52,10 @@ SPEC REPOS: - FLAnimatedImage - GenericJSON - JWTDecode + - MoreCodable - Nuke - NVActivityIndicatorView + - PromiseKit - Signals - Starscream - SwiftLint @@ -52,16 +66,18 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Alamofire: 16ce2c353fb72865124ddae8a57c5942388f4f11 - BLOCKv: 214559aa90809f52e7bd2ffbf733d895b3c5ada1 + Alamofire: ae5c501addb7afdbb13687d7f2f722c78734c2d3 + BLOCKv: 9a5d9e0921189177ddfdabc1bc957bef08645530 FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 GenericJSON: 05eb212cd12bf0562b816075090bea3eda2ab2ed JWTDecode: 85a405ab16d5473e99bd89ded1d535090ebd6a0e + MoreCodable: 1dd5f2b2ee520c8e594593389c5e3d21147ed648 Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 NVActivityIndicatorView: 4ca19fccc84595a78957336a086d00a49be6ce61 - Signals: eca6a9098b40ed5f8d8b60498d0e91168fa7c0cc + PromiseKit: 94c6e781838c5bf4717677d0d882b0e7250c80fc + Signals: 2c92e8639f97fe45678460840fabe2056b3190b4 Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5 - SwiftLint: a54bf1fe12b55c68560eb2a7689dfc81458508f7 + SwiftLint: 7a0227733d786395817373b2d0ca799fd0093ff3 VatomFace3D: 78d08c503819d5e2d8e1978d953885d596e379cf PODFILE CHECKSUM: 572540b34367caffea356b90ca04601e730b047d From f6d9dee70ce045e597f6afecaacab942d249a31e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 2 Apr 2019 15:46:07 +0200 Subject: [PATCH 022/165] Core: Feature - Client updates (#184) * Add type aliases * Improve docs --- BlockV/Core/Network/Stack/Client.swift | 45 ++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index 326bde63..77ffd01b 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -14,12 +14,13 @@ import Alamofire protocol ClientProtocol { + typealias RawCompletion = (_ data: Data?, _ error: BVError?) -> Void + /// Request that returns raw data. - func request(_ endpoint: Endpoint, completion: @escaping (Data?, BVError?) -> Void ) + func request(_ endpoint: Endpoint, completion: @escaping RawCompletion) /// Request that returns native object (must conform to decodable). - func request(_ endpoint: Endpoint, - completion: @escaping (Response?, BVError?) -> Void ) where Response: Decodable + func request(_ endpoint: Endpoint, completion: @escaping (T?, BVError?) -> Void ) where T: Decodable } @@ -107,15 +108,17 @@ final class Client: ClientProtocol { /// Endpoints generic over `void` complete by passing in the raw data response. /// - /// This is usefull for actions whose reponse payloads are not know since reactors may change at - /// any time. + /// This is useful for actions whose reponse payloads are not defined. For example, reactors may define their own + /// inner payload structure. /// - /// NOTE: Raw requests do not partake in OAuth and general lifecycle handling. + /// - important: + /// Do not call this endpoint for auth related calls, e.g login. Raw requests do *not* support through the + /// credential refresh mechanism. The access and refresh token will not be extracted and passed to the oauthhandler. /// /// - Parameters: /// - endpoint: Endpoint for the request /// - completion: The completion handler to call when the request is completed. - func request(_ endpoint: Endpoint, completion: @escaping (Data?, BVError?) -> Void) { + func request(_ endpoint: Endpoint, completion: @escaping RawCompletion) { // create request let request = self.sessionManager.request( @@ -148,6 +151,34 @@ final class Client: ClientProtocol { } + /// JSON Completion handler. + typealias JSONCompletion = (_ json: Any?, _ error: BVError?) -> Void + + func requestJSON(_ endpoint: Endpoint, completion: @escaping JSONCompletion) { + + // create request + let request = self.sessionManager.request( + url(path: endpoint.path), + method: endpoint.method, + parameters: endpoint.parameters, + encoding: endpoint.encoding + ) + + // configure validation + request.validate() + + request.responseJSON { dataResponse in + switch dataResponse.result { + case let .success(json): + print("success", json) + completion(json, nil) + case let .failure(err): + print("failure", err) + } + } + + } + /// Performs a request on a given endpoint. /// /// - Parameters: From 1d7afd9d287983a78a7170cbddf0191f72808d43 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 07:51:35 +0200 Subject: [PATCH 023/165] Integrate data pool into session launch --- BlockV/Core/BLOCKv.swift | 63 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index 6897b161..e9e1b02a 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -15,7 +15,7 @@ import JWTDecode /* Goal: - BLOCKv should be invariant over App ID and Environment. In other words, the properties should + BLOCKv should be invariant over App ID and Environment. In other words, the properties should not change, once set. Possibly targets for each environemnt? */ @@ -66,6 +66,12 @@ public final class BLOCKv { selector: #selector(handleUserAuthorisationRequired), name: Notification.Name.BVInternal.UserAuthorizationRequried, object: nil) + + // handle session launch + if self.isLoggedIn { + self.onSessionLaunch() + } + } // MARK: - Client @@ -171,7 +177,7 @@ public final class BLOCKv { fileprivate static var _socket: WebSocketManager? //TODO: What if this is accessed before the client is accessed? - //TODO: What if the viewer suscribes to an event before auth (login/reg) has occured? + //TODO: What if the viewer subscribes to an event before auth (login/reg) has occured? public static var socket: WebSocketManager { if _socket == nil { _socket = WebSocketManager(baseURLString: self.environment!.webSocketURLString, @@ -194,8 +200,10 @@ public final class BLOCKv { // disconnect and nil out socekt self._socket?.disconnect() self._socket = nil + // clear data pool + DataPool.clear() - printBV(info: "Reset") + printBV(info: "Reseting SDK") } // - Public Lifecycle @@ -223,15 +231,15 @@ public final class BLOCKv { BLOCKv.client.getAccessToken(completion: completion) } - /// Called when the networking client detects the user is unathorized. + /// Called when the networking client detects the user is unauthenticated. /// /// This method perfroms a clean up operation before notifying the viewer that the SDK requires - /// user authorization. + /// user authentication. /// /// - important: This method may be called multiple times. For example, consider the case where /// multiple requests fail due to the refresh token being invalid. @objc - private static func handleUserAuthorisationRequired() { + private static func handleUserAuthorisationRequired() { //FIXME: Rename to handleUserAuthenticationRequired() printBV(info: "Authorization - User is unauthorized.") @@ -245,6 +253,17 @@ public final class BLOCKv { } + /// Called when the user authenticates (logs in). + /// + /// - important: + /// This method is *not* called when for access token refreshes. + static internal func onLogin() { + + // stand up the session + self.onSessionLaunch() + + } + /// Holds a closure to call on logout public static var onLogout: (() -> Void)? @@ -262,6 +281,38 @@ public final class BLOCKv { } + /// This function is called everytime a user session is launched. + /// + /// A 'session launch' means the user has logged in (received a new refresh token), or the app has been cold + /// launched with an existing *valid* refresh token. + /// + /// - note: + /// This is slightly broader than 'log in' since it includes the lifecycle of the app. This function is responsible + /// for creating objects which are depenedent on a user session, e.g. data pool. + /// + /// Its compainion `onSessionTerminated` is `onLogout` since there is no app event signalling app termination. + /// + /// Triggered by: + /// - User authentication + /// - App launch & user is authenticated + static private func onSessionLaunch() { + + guard let refreshToken = CredentialStore.refreshToken?.token else { + fatalError("Invlalid session") + } + + guard let claim = try? decode(jwt: refreshToken).claim(name: "user_id"), let userId = claim.string else { + fatalError("Invalid cliam") + } + + // standup the client + _ = client + + // standup data pool + DataPool.sessionInfo = ["userID": userId] + + } + // MARK: - Resources enum URLEncodingError: Error { From cc4d5abfa59841025b1f1ca5697cc0936df5d1bf Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 07:51:52 +0200 Subject: [PATCH 024/165] Call onLogin in auth requests --- BlockV/Core/Requests/BLOCKv+AuthRequests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index c81f01b7..09af50f5 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -75,7 +75,8 @@ extension BLOCKv { // persist credentials CredentialStore.saveRefreshToken(authModel.refreshToken) CredentialStore.saveAssetProviders(authModel.assetProviders) - + // noifty + self.onLogin() completion(authModel.user, nil) } @@ -149,7 +150,8 @@ extension BLOCKv { // persist credentials CredentialStore.saveRefreshToken(authModel.refreshToken) CredentialStore.saveAssetProviders(authModel.assetProviders) - + // notify + self.onLogin() // completion completion(authModel.user, nil) } From 4ba646a169ee924144b9522719e060f1b7bacc81 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 07:54:54 +0200 Subject: [PATCH 025/165] Clean up --- BlockV/Core/BLOCKv.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index e9e1b02a..47ac3b2a 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -239,7 +239,7 @@ public final class BLOCKv { /// - important: This method may be called multiple times. For example, consider the case where /// multiple requests fail due to the refresh token being invalid. @objc - private static func handleUserAuthorisationRequired() { //FIXME: Rename to handleUserAuthenticationRequired() + private static func handleUserAuthorisationRequired() { printBV(info: "Authorization - User is unauthorized.") @@ -256,7 +256,7 @@ public final class BLOCKv { /// Called when the user authenticates (logs in). /// /// - important: - /// This method is *not* called when for access token refreshes. + /// This method is *not* called when the access token refreshes. static internal func onLogin() { // stand up the session From f5dc9029968e43ef8eb7242b382827655a79e08f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 08:39:07 +0200 Subject: [PATCH 026/165] Remove uneccessary pods --- BLOCKv.podspec | 2 -- Example/BlockV.xcodeproj/project.pbxproj | 2 -- Example/Podfile.lock | 6 +----- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/BLOCKv.podspec b/BLOCKv.podspec index 565ddb4a..8d95ab7a 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -25,8 +25,6 @@ Pod::Spec.new do |s| s.dependency 'SwiftLint', '~> 0.26' # Linter s.dependency 'GenericJSON', '~> 1.2' # JSON s.dependency 'PromiseKit', '~> 6.8' # Promises -# s.dependency 'MoreCodable' - s.dependency 'DictionaryCoding' #s.exclude_files = '**/Info*.plist' end diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 99000e99..3acdc253 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -557,7 +557,6 @@ "${BUILT_PRODUCTS_DIR}/FLAnimatedImage/FLAnimatedImage.framework", "${BUILT_PRODUCTS_DIR}/GenericJSON/GenericJSON.framework", "${BUILT_PRODUCTS_DIR}/JWTDecode/JWTDecode.framework", - "${BUILT_PRODUCTS_DIR}/MoreCodable/MoreCodable.framework", "${BUILT_PRODUCTS_DIR}/NVActivityIndicatorView/NVActivityIndicatorView.framework", "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework", "${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework", @@ -572,7 +571,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FLAnimatedImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GenericJSON.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JWTDecode.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MoreCodable.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NVActivityIndicatorView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework", diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 2098b96b..9ade2cd5 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -6,7 +6,6 @@ PODS: - Alamofire (~> 4.7) - GenericJSON (~> 1.2) - JWTDecode (~> 2.1) - - MoreCodable - PromiseKit (~> 6.8) - Signals (~> 6.0) - Starscream (~> 3.0.6) @@ -18,7 +17,6 @@ PODS: - FLAnimatedImage (1.0.12) - GenericJSON (1.2.0) - JWTDecode (2.2.0) - - MoreCodable (0.2.0) - Nuke (7.5.2) - NVActivityIndicatorView (4.6.1): - NVActivityIndicatorView/Presenter (= 4.6.1) @@ -52,7 +50,6 @@ SPEC REPOS: - FLAnimatedImage - GenericJSON - JWTDecode - - MoreCodable - Nuke - NVActivityIndicatorView - PromiseKit @@ -67,11 +64,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: ae5c501addb7afdbb13687d7f2f722c78734c2d3 - BLOCKv: 9a5d9e0921189177ddfdabc1bc957bef08645530 + BLOCKv: 391d6db2099bedc152bdc629eca934d26b0a612d FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 GenericJSON: 05eb212cd12bf0562b816075090bea3eda2ab2ed JWTDecode: 85a405ab16d5473e99bd89ded1d535090ebd6a0e - MoreCodable: 1dd5f2b2ee520c8e594593389c5e3d21147ed648 Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 NVActivityIndicatorView: 4ca19fccc84595a78957336a086d00a49be6ce61 PromiseKit: 94c6e781838c5bf4717677d0d882b0e7250c80fc From e625a3b6d9d1ca65b25c4bf7f26f7351465dae13 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 08:56:07 +0200 Subject: [PATCH 027/165] Update UI --- Example/BlockV/Actions.storyboard | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/BlockV/Actions.storyboard b/Example/BlockV/Actions.storyboard index 073edd32..4e3b9c75 100644 --- a/Example/BlockV/Actions.storyboard +++ b/Example/BlockV/Actions.storyboard @@ -14,13 +14,13 @@ - + - + - + From c0d1eb244de45d0748cc934c427a0f251ec21325 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 08:58:20 +0200 Subject: [PATCH 028/165] Add available actions --- .../ActionListTableViewController.swift | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift index cd6a6ef2..a04aa038 100644 --- a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift +++ b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift @@ -39,6 +39,9 @@ class ActionListTableViewController: UITableViewController { var vatom: VatomModel! + /// Currently selected action. + var selectedAction: AvailableAction? + // table view data model struct AvailableAction { let action: ActionModel @@ -100,6 +103,26 @@ class ActionListTableViewController: UITableViewController { } } + + /// Prompts the user to optionally delete the vatom. + fileprivate func deleteVatom() { + + let message = "This vAtom will be deleted from all your devices." + let alert = UIAlertController.confirmAlert(title: "Delete vAtom", message: message) { confirmed in + if confirmed { + BLOCKv.trashVatom(self.vatom.id) { [weak self] _ in + self?.hide() + } + } + } + + self.present(alert, animated: true, completion: nil) + + } + + fileprivate func hide() { + self.presentingViewController?.presentingViewController?.dismiss(animated: true, completion: nil) + } // MARK: - Navigation @@ -114,8 +137,6 @@ class ActionListTableViewController: UITableViewController { destination.actionName = self.selectedAction!.action.name } - var selectedAction: AvailableAction? - } // MARK: - Table view data source @@ -123,27 +144,43 @@ class ActionListTableViewController: UITableViewController { extension ActionListTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { - return 1 + return 2 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return availableActions.count + if section == 0 { + return availableActions.count + } + return 1 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell.action.id", for: indexPath) - let availableAction = availableActions[indexPath.row] - cell.textLabel?.text = availableAction.action.name - cell.textLabel?.alpha = availableAction.isSupported ? 1 : 0.5 - cell.accessoryView = nil + if indexPath.section == 0 { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell.action.id", for: indexPath) + let availableAction = availableActions[indexPath.row] + cell.textLabel?.text = availableAction.action.name + cell.textLabel?.alpha = availableAction.isSupported ? 1 : 0.5 + cell.accessoryView = nil + return cell + } + + let cell = UITableViewCell.init(style: .default, reuseIdentifier: "id.cell.delete") + cell.textLabel?.textColor = UIColor.destruciveOrange + cell.textLabel?.text = "Delete" return cell + } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - selectedAction = availableActions[indexPath.row] - if selectedAction!.isSupported { - self.performSegue(withIdentifier: "seg.action.selection", sender: self) + + if indexPath.section == 0 { + selectedAction = availableActions[indexPath.row] + if selectedAction!.isSupported { + self.performSegue(withIdentifier: "seg.action.selection", sender: self) + } + } else { + self.deleteVatom() } } From ca7deb8c603a9b69e96c312f3800f885afc2a530 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 09:01:57 +0200 Subject: [PATCH 029/165] Update actions to call into data pool --- .../OutboundActionViewController.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift index 1c19930c..feddcccc 100644 --- a/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift +++ b/Example/BlockV/Controllers/Actions/OutboundActionViewController.swift @@ -88,21 +88,23 @@ class OutboundActionViewController: UIViewController { /// Performs the appropriate action using the func performAction(token: UserToken) { - let completionHandler: (([String: Any]?, BVError?) -> Void) = { [weak self] (data, error) in - // handle error - guard let data = data, error == nil else { - print(error?.localizedDescription) + let resultHandler: ((Result<[String: Any], BVError>) -> Void) = { [weak self] result in + switch result { + case .success(let json): + // success + print("Action response: \(json.debugDescription)") + self?.hide() + case .failure(let error): + print(error.localizedDescription) + return } - // handle success - print("Action response: \(data.debugDescription)") - self?.hide() } switch actionName { - case "Transfer": self.vatom.transfer(toToken: token, completion: completionHandler) - case "Clone": self.vatom.clone(toToken: token, completion: completionHandler) - case "Redeem": self.vatom.redeem(toToken: token, completion: completionHandler) + case "Transfer": self.vatom.transfer(toToken: token, completion: resultHandler) + case "Clone": self.vatom.clone(toToken: token, completion: resultHandler) + case "Redeem": self.vatom.redeem(toToken: token, completion: resultHandler) default: return } From 88d18dfcac274fd01d4c3769ca7172122f84aa19 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 09:02:30 +0200 Subject: [PATCH 030/165] Add confirm alert --- .../Extensions/UIAlertController+Etx.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Example/BlockV/Extensions/UIAlertController+Etx.swift b/Example/BlockV/Extensions/UIAlertController+Etx.swift index 634d7fc9..89f679e5 100644 --- a/Example/BlockV/Extensions/UIAlertController+Etx.swift +++ b/Example/BlockV/Extensions/UIAlertController+Etx.swift @@ -73,4 +73,22 @@ extension UIAlertController { return alertController } + static func confirmAlert(title: String, + message: String, + confirmed: @escaping (Bool) -> Void) -> UIAlertController { + let alertController = UIAlertController.init(title: title, + message: message, + preferredStyle: .alert) + + let okAction = UIAlertAction.init(title: "OK", style: .default) { action in + confirmed(true) + } + let cancelAction = UIAlertAction.init(title: "Cancel", style: .destructive) { action in + confirmed(false) + } + alertController.addAction(okAction) + alertController.addAction(cancelAction) + return alertController + } + } From 89c3c1b2460e216a94ef52c6799dfc86054490ff Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 09:02:45 +0200 Subject: [PATCH 031/165] Add orange color --- Example/BlockV/Extensions/UIColor+Ext.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Example/BlockV/Extensions/UIColor+Ext.swift b/Example/BlockV/Extensions/UIColor+Ext.swift index f1f7499c..0d9d580c 100644 --- a/Example/BlockV/Extensions/UIColor+Ext.swift +++ b/Example/BlockV/Extensions/UIColor+Ext.swift @@ -29,4 +29,8 @@ extension UIColor { return UIColor(red: 86.0 / 255.0, green: 194.0 / 255.0, blue: 201.0 / 255.0, alpha: 1.0) } + @nonobjc class var destruciveOrange: UIColor { + return UIColor(red: 219.0 / 255.0, green: 76.0 / 255.0, blue: 43.0 / 255.0, alpha: 1.0) + } + } From fbd19f2b28f29386621839903d64827674c8848e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 11:28:25 +0200 Subject: [PATCH 032/165] Add preemptive actions --- .../Helpers/Vatom+PreemptiveActions.swift | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift new file mode 100644 index 00000000..8fbdfdeb --- /dev/null +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -0,0 +1,202 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/// Extends VatomModel with common vatom actions available on owned vatoms. +/// +/// All actions are *preemptive* where possible. That is, data pool is updated locally before the network request is +/// made performing the action on the server. +extension VatomModel { + + // MARK: - Common Actions + + /// Performs the **Transfer** action on the current vatom and preeempts the action result. + /// + /// - Parameters: + /// - token: User token to which the vatom should be transferred. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func transfer(toToken token: UserToken, + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // prempt reactor outcome + // remove vatom from inventory region + let undo = DataPool.inventory().preemptiveRemove(id: self.id) + + // perform the action + self.performAction("Transfer", payload: body, undos: [undo], completion: completion) + + } + + /// Perform the **Redeem** action on the current vatom and preeempts the action result. + /// + /// - Parameters: + /// - token: User token to which the vatom should be redeemed. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func redeem(toToken token: UserToken, + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // prempt reactor outcome + // remove vatom from inventory region + let undo = DataPool.inventory().preemptiveRemove(id: self.id) + + // perform the action + self.performAction("Redeem", payload: body, undos: [undo], completion: completion) + + } + + /// Performs the **Activate** action on the current vatom and preeempts the action result. + /// + /// - Parameters: + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func activate(completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = ["this.id": self.id] + + // prempt reactor outcome + // remove vatom from inventory region + let undo = DataPool.inventory().preemptiveRemove(id: self.id) + + // perform the action + self.performAction("Redeem", payload: body, undos: [undo], completion: completion) + + } + + /// Performs the **Clone** action on the current vatom and preeempts the action result. + /// + /// - Parameters: + /// - token: User token to which the vatom should be cloned. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func clone(toToken token: UserToken, + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = [ + "this.id": self.id, + "new.owner.\(token.type.rawValue)": token.value + ] + + // prempt reactor outcome + + /* + 1. `num_direct_clones` is increased by 1. + 2. `cloning_score` is dependent on the clone gain - which is not know at this point. + */ + let undo = DataPool.inventory().preemptiveChange(id: self.id, + keyPath: "vAtom::vAtomType.num_direct_clones", + value: self.props.numberDirectClones + 1) + + // perform the action + self.performAction("Clone", payload: body, undos: [undo], completion: completion) + + } + + /// Performs the **Drop** action on the current vatom and preeempts the action result. + /// + /// - Parameters: + /// - longitude: The longitude component of the coordinate. + /// - latitude: The latitude component of the coordinate. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func dropAt(longitude: Double, + latitude: Double, + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = [ + "geo.pos": [ + "Lat": latitude, + "Lon": longitude + ] + ] + + // preempt the reactor outcome + let undoCoords = DataPool.inventory().preemptiveChange(id: self.id, + keyPath: "vAtom::vAtomType.geo_pos.coordinates", + value: [longitude, latitude]) + + let undoDropped = DataPool.inventory().preemptiveChange(id: self.id, + keyPath: "vAtom::vAtomType.dropped", + value: true) + + // perform the action + self.performAction("Drop", payload: body, undos: [undoCoords, undoDropped], completion: completion) + + } + + /// Performs the **Pickup** action on the current vatom and preeempts the action result. + /// + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public func pickUp(completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = ["this.id": self.id] + + // preempt the reactor outcome + let undo = DataPool.inventory().preemptiveChange(id: self.id, + keyPath: "vAtom::vAtomType.dropped", + value: false) + + // perform the action + self.performAction("Pickup", payload: body, undos: [undo], completion: completion) + + } + + private typealias Undo = () -> Void + + /// Performs the action and rolls back unsing the undo functions if an error occurs. + /// + /// - Parameters: + /// - name: Name of the action, e.g. "Transfer". + /// - payload: Action payload + /// - undos: Array of undo closures. If the action fails, each undo closure will be executed. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + private func performAction(_ name: String, + payload: [String: Any], + undos: [Undo] = [], + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + /// passed in vatom id must match `this.id` + guard let vatomId = payload["this.id"] as? String, self.id == vatomId else { + let error = BVError.custom(reason: "Invalid payload. Value `this.id` must be match the current vAtom.") + completion(.failure(error)) + return + } + + // perform the action + BLOCKv.performAction(name: name, payload: payload) { (payload, error) in + // convert closure to result + if let payload = payload, error == nil { + completion(.success(payload)) + } else { + // run undo closures + undos.forEach { $0() } + completion(.failure(error!)) + } + } + + } + +} From c521dfd1cfd679232f9aeebf7729e958d9df7822 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 3 Apr 2019 11:32:14 +0200 Subject: [PATCH 033/165] Remove import --- BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index 8fbdfdeb..329cd6aa 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -10,7 +10,6 @@ // import Foundation -import PromiseKit /// Extends VatomModel with common vatom actions available on owned vatoms. /// From d849b3adb03bb2eb7df9af3809cc06ffc818d1e7 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 5 Apr 2019 13:31:07 +0200 Subject: [PATCH 034/165] Remove old actions --- BlockV/Core/vAtom/Vatom+Actions.swift | 92 --------------------------- 1 file changed, 92 deletions(-) delete mode 100644 BlockV/Core/vAtom/Vatom+Actions.swift diff --git a/BlockV/Core/vAtom/Vatom+Actions.swift b/BlockV/Core/vAtom/Vatom+Actions.swift deleted file mode 100644 index 986bcbc0..00000000 --- a/BlockV/Core/vAtom/Vatom+Actions.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// - -import Foundation - -/// This file adds actions to VatomModel (simply for convenience). -extension VatomModel { - - /// Transfers this vAtom to the specified token. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - token: Standard UserToken (Phone, Email, or User ID) - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func transfer(toToken token: UserToken, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - // perform the action - BLOCKv.performAction(name: "Transfer", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - - /// Drops this vAtom as the specified location. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - latitude: The latitude component of the coordinate. - /// - longitude: The longitude component of the coordinate. - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func drop(latitude: Double, longitude: Double, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body: [String: Any] = [ - "this.id": self.id, - "geo.pos": [ - "lat": latitude, - "lon": longitude - ] - ] - - // perform the action - BLOCKv.performAction(name: "Drop", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - - /// Picks up this vAtom from it's dropped location. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func pickUp(completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id - ] - - // perform the action - BLOCKv.performAction(name: "Pickup", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - -} From 33937d9d1cf6d019716149edd18ba37229781036 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 5 Apr 2019 14:28:16 +0200 Subject: [PATCH 035/165] Response json overlay --- BlockV/Core/Data Pool/Core+Overlays.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift index ccd7badc..d638e7e7 100644 --- a/BlockV/Core/Data Pool/Core+Overlays.swift +++ b/BlockV/Core/Data Pool/Core+Overlays.swift @@ -20,6 +20,10 @@ internal extension Client { func request(_ endpoint: Endpoint) -> Promise { return Promise { self.request(endpoint, completion: $0.resolve) } } + + func requestJSON(_ endpoint: Endpoint) -> Promise { + return Promise { self.requestJSON(endpoint, completion: $0.resolve) } + } } From ae47129bd2429792bd12cdf49705a8117dfbd6d0 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 5 Apr 2019 14:28:52 +0200 Subject: [PATCH 036/165] Use responseJSON method --- .../Core/Data Pool/Regions/InventoryRegion.swift | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 1ce08e51..4157e3d3 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -96,23 +96,14 @@ class InventoryRegion: BLOCKvRegion { printBV(info: "[DataPool > InventoryRegion] Loading page \(page), got \(previousItems.count) items so far...") // build raw request - let endpoint = API.Raw.getInventory(parentID: "*", page: page, limit: 100) + let endpoint = API.Raw.getInventory(parentID: "*", page: page) - return BLOCKv.client.request(endpoint).then { data -> Promise<[String]?> in + return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[String]?> in - //TODO: Use a json returning request instead of a raw data request. - - // convert - guard let object = try? JSONSerialization.jsonObject(with: data), let json = object as? [String: Any] else { + guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { throw NSError.init("Unable to load") //FIXME: Create a better error } - guard let payload = json["payload"] as? [String: Any] else { - throw NSError.init("Unable to load") //FIXME: Create a better error - } - - //TODO: This should be factored out. - // create list of items var items: [DataObject] = [] var ids = previousItems From 7063e3e7c0eff4c246a431310f4d38c608404679 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 5 Apr 2019 14:42:21 +0200 Subject: [PATCH 037/165] Lint --- BlockV/Core/Data Pool/Core+Overlays.swift | 2 +- BlockV/Core/Data Pool/DataPool.swift | 4 ++-- BlockV/Core/Data Pool/Regions/InventoryRegion.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift index d638e7e7..42bc56f3 100644 --- a/BlockV/Core/Data Pool/Core+Overlays.swift +++ b/BlockV/Core/Data Pool/Core+Overlays.swift @@ -20,7 +20,7 @@ internal extension Client { func request(_ endpoint: Endpoint) -> Promise { return Promise { self.request(endpoint, completion: $0.resolve) } } - + func requestJSON(_ endpoint: Endpoint) -> Promise { return Promise { self.requestJSON(endpoint, completion: $0.resolve) } } diff --git a/BlockV/Core/Data Pool/DataPool.swift b/BlockV/Core/Data Pool/DataPool.swift index 216bc17a..692a84a0 100644 --- a/BlockV/Core/Data Pool/DataPool.swift +++ b/BlockV/Core/Data Pool/DataPool.swift @@ -98,10 +98,10 @@ public final class DataPool { /// /// - Parameter region: The region to remove static func removeRegion(region: Region) { - + // remove region regions = regions.filter { $0 !== region } - + } /// Clear out the session info. diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 4157e3d3..bef7713b 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -258,7 +258,7 @@ class InventoryRegion: BLOCKvRegion { printBV(error: "[DataPool > InventoryRegion] Couldn't process incoming vatom") return } - + //FIXME: Consider where to add onReceived }.catch { error in From 3792164ba75b2d4f475dd4e93c3e98f8e5ed300e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 5 Apr 2019 15:30:27 +0200 Subject: [PATCH 038/165] Add raw vatom patch --- BlockV/Core/Data Pool/Core+Overlays.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift index ccd7badc..0442a171 100644 --- a/BlockV/Core/Data Pool/Core+Overlays.swift +++ b/BlockV/Core/Data Pool/Core+Overlays.swift @@ -60,6 +60,13 @@ internal extension API { parameters: ["ids": ids] ) } + + /// Builds the endpoint to update a vatom. + static func updateVatom(payload: [String: Any]) -> Endpoint { + return Endpoint(method: .post, + path: "/v1/vatoms", + parameters: payload) + } } From f9cee10dda8b4fb4d12b3c93d2e6e58df85d1d6a Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 9 Apr 2019 08:50:51 +0200 Subject: [PATCH 039/165] Parse response on concurrent queue --- BlockV/Core/Network/Stack/Client.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index 77ffd01b..71f2715b 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -131,7 +131,7 @@ final class Client: ClientProtocol { // configure validation request.validate() //TODO: May need manual validation - request.responseData { (dataResponse) in + request.responseData(queue: queue) { (dataResponse) in switch dataResponse.result { case let .success(data): completion(data, nil) case let .failure(err): @@ -142,7 +142,7 @@ final class Client: ClientProtocol { if let err = err as? BVError { completion(nil, err) } else { - // create a wrapped networking errir + // create a wrapped networking error let error = BVError.networking(error: err) completion(nil, error) } @@ -166,14 +166,14 @@ final class Client: ClientProtocol { // configure validation request.validate() - - request.responseJSON { dataResponse in + request.responseJSON(queue: queue) { dataResponse in switch dataResponse.result { case let .success(json): - print("success", json) completion(json, nil) case let .failure(err): - print("failure", err) + // create a wrapped networking error + let error = BVError.networking(error: err) + completion(nil, error) } } From 989db805eae9d31e38ae3686d2c559f3be83bb97 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 10 Apr 2019 15:55:52 +0200 Subject: [PATCH 040/165] Add VatomChildRegion.swift --- .../Regions/VatomChildrenRegion.swift | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift diff --git a/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift b/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift new file mode 100644 index 00000000..90121d0b --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift @@ -0,0 +1,146 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/// This region plugin provides access to a collection of vatoms that are children of another vatom. +/// The 'inventory' region is much mor reliable, so if you know that your vatoms are owned by the current user, +/// use the inventory region with a filter rather. +/// +/// To get an instance, call `DataPool.region("children", "parent-id")` +class VatomChildrenRegion: BLOCKvRegion { + + /// Plugin identifier. + override class var id: String { return "children" } + + /// Parent ID. + let parentID: String + + /// Constructor. + required init(descriptor: Any) throws { + + // store ID + parentID = descriptor as? String ?? "" + + // setup base class + try super.init(descriptor: descriptor) + + } + + /// Our state key is the list of IDs. + override var stateKey: String { + return "children:" + self.parentID + } + + /// Check if a region request matches our region. + override func matches(id: String, descriptor: Any) -> Bool { + return id == "children" && (descriptor as? String) == parentID + } + + /// Load current state from the server. + override func load() -> Promise<[String]?> { + + // pause websocket events + self.pauseMessages() + + // fetch all pages recursively + return self.fetch().map { dataObjects -> [String] in + + // add all objects + self.add(objects: dataObjects) + + // return IDs + return dataObjects.map { $0.id } + + }.ensure { + + // resume websocket events + self.resumeMessages() + + } + + } + + /// Recursively fetch all pages of data from the server + fileprivate func fetch(page: Int = 1, previousItems: [DataObject] = []) -> Promise<[DataObject]> { + + // create discover query + let builder = DiscoverQueryBuilder() + builder.setScope(scope: .parentID, value: parentID) + builder.page = page + builder.limit = 1000 + + printBV(info: "[DataPool > VatomChildrenRegion] Loading page \(page), got \(previousItems.count) items so far.") + + let endpoint = API.Raw.discover(builder.toDictionary()) + return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[DataObject]> in + + // extract payload + guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + // create list of items + var items = previousItems + + // add vatoms to the list + guard let vatomInfos = payload["results"] as? [[String: Any]] else { return Promise.value(items) } + for vatomInfo in vatomInfos { + + // add data object + let obj = DataObject() + obj.type = "vatom" + obj.id = vatomInfo["id"] as? String ?? "" + obj.data = vatomInfo + items.append(obj) + + } + + // add faces to the list + guard let faces = payload["faces"] as? [[String: Any]] else { return Promise.value(items) } + for face in faces { + + // add data object + let obj = DataObject() + obj.type = "face" + obj.id = face["id"] as? String ?? "" + obj.data = face + items.append(obj) + + } + + // add actions to the list + guard let actions = payload["actions"] as? [[String: Any]] else { return Promise.value(items) } + for action in actions { + + // add data object + let obj = DataObject() + obj.type = "action" + obj.id = action["name"] as? String ?? "" + obj.data = action + items.append(obj) + + } + + // if no more data, stop + if vatomInfos.count == 0 { + return Promise.value(items) + } + + // done, get next page + return self.fetch(page: page+1, previousItems: items) + + } + + } + +} From 77ebb04a64907911d936acc0deb4c380f62242ac Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 10 Apr 2019 15:56:36 +0200 Subject: [PATCH 041/165] Update ids to use class variables --- BlockV/Core/Data Pool/DataPool.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Data Pool/DataPool.swift b/BlockV/Core/Data Pool/DataPool.swift index 692a84a0..3a49c09a 100644 --- a/BlockV/Core/Data Pool/DataPool.swift +++ b/BlockV/Core/Data Pool/DataPool.swift @@ -115,7 +115,7 @@ extension DataPool { /// Returns the global inventory region. public static func inventory() -> Region { - return DataPool.region(id: "inventory", descriptor: "") + return DataPool.region(id: InventoryRegion.id, descriptor: "") } /// Returns the global vatom regions for the specified identifier. @@ -125,7 +125,7 @@ extension DataPool { /// Returns the global children region for the specifed parent identifier. public static func children(parentID: String) -> Region { - return DataPool.region(id: "children", descriptor: parentID) + return DataPool.region(id: VatomChildrenRegion.id, descriptor: parentID) } } From 322e4c7a0df52a934fb4075f2a964083a81cc40c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 06:37:06 +0200 Subject: [PATCH 042/165] Improve safety --- BlockV/Core/Data Pool/DataPool.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/DataPool.swift b/BlockV/Core/Data Pool/DataPool.swift index 3a49c09a..da4f95a9 100644 --- a/BlockV/Core/Data Pool/DataPool.swift +++ b/BlockV/Core/Data Pool/DataPool.swift @@ -120,7 +120,7 @@ extension DataPool { /// Returns the global vatom regions for the specified identifier. public static func vatom(id: String) -> Region { - return DataPool.region(id: "ids", descriptor: [id]) + return DataPool.region(id: VatomIDRegion.id, descriptor: [id]) } /// Returns the global children region for the specifed parent identifier. From 01d86f498ef63e083d13a63004e982949e1f8e66 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 06:37:29 +0200 Subject: [PATCH 043/165] Add unpacked payload parsing --- .../Core/Data Pool/Regions/BLOCKvRegion.swift | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index ea84f889..656608c0 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -260,6 +260,61 @@ class BLOCKvRegion: Region { } + /// Parses the unpackaged vatom payload from the server and returns an array of `DataObject`. + /// + /// Returns `nil` if the payload cannot be parsed. + func parseDataObject(from payload: [String: Any]) -> [DataObject]? { + + // create list of items + var items: [DataObject] = [] + + // ensure + guard + let vatoms = payload["vatoms"] as? [[String: Any]] ?? payload["results"] as? [[String: Any]], + let faces = payload["faces"] as? [[String: Any]], + let actions = payload["actions"] as? [[String: Any]] + else { return nil } + + // add faces to the list + for face in faces { + + // add data object + let obj = DataObject() + obj.type = "face" + obj.id = face["id"] as? String ?? "" + obj.data = face + items.append(obj) + + } + + // add actions to the list + for action in actions { + + // add data object + let obj = DataObject() + obj.type = "action" + obj.id = action["name"] as? String ?? "" + obj.data = action + items.append(obj) + + } + + // add vatoms to the list + for vatom in vatoms { + + // add data object + let obj = DataObject() + obj.type = "vatom" + obj.id = vatom["id"] as? String ?? "" + obj.data = vatom + items.append(obj) + + } + + return items + + } + // MARK: - Notifications // - Add From 7dfc34af972c7e8330e30d87536888923b10dec2 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 06:38:04 +0200 Subject: [PATCH 044/165] Leverage superclass parse function --- .../Data Pool/Regions/InventoryRegion.swift | 111 ++---------------- 1 file changed, 13 insertions(+), 98 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index bef7713b..bddca686 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -35,8 +35,6 @@ class InventoryRegion: BLOCKvRegion { required init(descriptor: Any) throws { try super.init(descriptor: descriptor) - print(DataPool.sessionInfo["userID"]) - // make sure we have a valid current user guard let userID = DataPool.sessionInfo["userID"] as? String, !userID.isEmpty else { throw NSError("You cannot query the inventory region without being logged in.") @@ -105,56 +103,20 @@ class InventoryRegion: BLOCKvRegion { } // create list of items - var items: [DataObject] = [] var ids = previousItems - // add faces to the list - guard let faces = payload["faces"] as? [[String: Any]] else { return Promise.value(ids) } - for face in faces { - - // add data object - let obj = DataObject() - obj.type = "face" - obj.id = face["id"] as? String ?? "" - obj.data = face - items.append(obj) - ids.append(obj.id) - - } - - // add actions to the list - guard let actions = payload["actions"] as? [[String: Any]] else { return Promise.value(ids) } - for action in actions { - - // add data object - let obj = DataObject() - obj.type = "action" - obj.id = action["name"] as? String ?? "" - obj.data = action - items.append(obj) - ids.append(obj.id) - - } - - // add vatoms to the list - guard let vatomInfos = payload["vatoms"] as? [[String: Any]] else { return Promise.value(ids) } - for vatomInfo in vatomInfos { - - // add data object - let obj = DataObject() - obj.type = "vatom" - obj.id = vatomInfo["id"] as? String ?? "" - obj.data = vatomInfo - items.append(obj) - ids.append(obj.id) - + // parse out data objects + guard let items = self.parseDataObject(from: payload) else { + return Promise.value(ids) } + // append new ids + ids.append(contentsOf: items.map { $0.id }) // add data objects self.add(objects: items) // if no more data, stop - if vatomInfos.count == 0 { + if items.count == 0 { return Promise.value(ids) } @@ -199,68 +161,21 @@ class InventoryRegion: BLOCKvRegion { BLOCKv.client.request(endpoint).done { data in // convert - guard let object = try? JSONSerialization.jsonObject(with: data), - let json = object as? [String: Any] else { + guard + let object = try? JSONSerialization.jsonObject(with: data), + let json = object as? [String: Any], + let payload = json["payload"] as? [String: Any] else { throw NSError.init("Unable to load") //FIXME: Create a better error } - guard let payload = json["payload"] as? [String: Any] else { - throw NSError.init("Unable to load") //FIXME: Create a better error - } - - // add vatom to new objects list - var items: [DataObject] = [] - - // add faces to the list - guard let faces = payload["faces"] as? [[String: Any]] else { return } - for face in faces { - - // add data object - let obj = DataObject() - obj.type = "face" - obj.id = face["id"] as? String ?? "" - obj.data = face - items.append(obj) - - } - - // add actions to the list - guard let actions = payload["actions"] as? [[String: Any]] else { return } - for action in actions { - - // add data object - let obj = DataObject() - obj.type = "action" - obj.id = action["name"] as? String ?? "" - obj.data = action - items.append(obj) - - } - - // add vatoms to the list - guard let vatomInfos = payload["vatoms"] as? [[String: Any]] else { return } - for vatomInfo in vatomInfos { - - // add data object - let obj = DataObject() - obj.type = "vatom" - obj.id = vatomInfo["id"] as? String ?? "" - obj.data = vatomInfo - items.append(obj) - + // parse out objects + guard let items = self.parseDataObject(from: payload) else { + throw NSError.init("Unable to parse data") //FIXME: Create a better error } // add new objects self.add(objects: items) - // notify vatom received - guard let vatom = self.get(id: vatomInfos[0]["id"] as? String ?? "") as? VatomModel else { - printBV(error: "[DataPool > InventoryRegion] Couldn't process incoming vatom") - return - } - - //FIXME: Consider where to add onReceived - }.catch { error in printBV(error: "[InventoryRegion] Unable to fetch inventory. \(error.localizedDescription)") }.finally { From 478517a8923c402359800644087a9a6b9fef15a8 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 06:39:10 +0200 Subject: [PATCH 045/165] Add VatomIDRegion.swift --- .../Data Pool/Regions/VatomIDRegion.swift | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 BlockV/Core/Data Pool/Regions/VatomIDRegion.swift diff --git a/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift b/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift new file mode 100644 index 00000000..0ef2e88d --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift @@ -0,0 +1,92 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +/// This region plugin provides access to a collection of vatoms identified by their IDs. +/// The 'inventory' region is much more reliable, so if you know that your vatoms are owned by the current user, +/// use the inventory region with a filter rather. +/// +/// TODO: Retry a few times +/// +/// To get an instance, call `DataPool.region("ids", ["id1", "id2"])` +class VatomIDRegion: BLOCKvRegion { + + /// Plugin identifier. + override class var id: String { return "ids" } + + /// IDs being fetched in this region. + let ids: [String] + + /// Constructor. + required init(descriptor: Any) throws { + + // store IDs + ids = descriptor as? [String] ?? [] + + // setup base class + try super.init(descriptor: descriptor) + + } + + /// Our state key is the list of IDs. + override var stateKey: String { + return "ids:" + self.ids.joined(separator: ",") + } + + /// Check if a region request matches our region. + override func matches(id: String, descriptor: Any) -> Bool { + + // check all filters match + if id != "ids" { return false } + guard let otherIds = descriptor as? [String] else { return false } + // check ids match + return self.ids == otherIds + + } + + /// Load current state from the server. + override func load() -> Promise<[String]?> { + + // pause websocket events + self.pauseMessages() + + let endpoint = API.Raw.getVatoms(withIDs: ids) + return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[String]?> in + + guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + // parse items + guard let items = self.parseDataObject(from: payload) else { + throw NSError.init("Unable to parse data") //FIXME: Create a better error + } + + // add all objects + self.add(objects: items) + + // return IDs + let ids = items.map { $0.id } + + return Promise.value(ids) + + }.ensure { + + // resume websocket events + self.resumeMessages() + + } + + } + +} From 77761c2125406974f6f41a84f3b760af1bddba53 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 06:44:17 +0200 Subject: [PATCH 046/165] Comment clean up --- .../Core/Data Pool/Regions/BLOCKvRegion.swift | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index 656608c0..9a2e97dc 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -11,11 +11,6 @@ import Foundation -/* - Issues: - 1. Dependecy injection of socket manager. - */ - /// Abstract subclass of `Region`. This intermediate class handles updates from the BLOCKv Web socket. Regions should /// subclass to automatically handle Web socket updates. /// @@ -31,6 +26,7 @@ import Foundation /// - Notifications /// > Add, update, remove notification are broadcast for the changed vatom. An update is always emitted for the parent /// of the changed vatom. This is useful since the parent can then update its state, e.g. on child removal. +/// - Parsing unpackaged vatom payload into data-objects. class BLOCKvRegion: Region { /// Constructor @@ -52,28 +48,6 @@ class BLOCKvRegion: Region { } - /* - The initialiser below allows for dependency injection of the socket manager. - */ - - /// Initialize with a descriptor and a socket. -// init(descriptor: Any, socket: WebSocketManager) throws { -// try super.init(descriptor: descriptor) -// -// // subscribe to socket connections -// socket.onConnected.subscribe(with: self) { _ in -// self.onWebSocketConnect() -// } -// -// // subscribe to raw socket messages -// socket.onMessageReceivedRaw.subscribe(with: self) { descriptor in -// self.onWebSocketMessage(descriptor) -// } -// -// // Monitor for timed updates -// DataObjectAnimator.shared.add(region: self) -// } - deinit { // stop listening for animation updates @@ -92,8 +66,6 @@ class BLOCKvRegion: Region { override func close() { super.close() - //FIXME: Double check that signals do not need to be unsubscribed from. - // remove listeners DataObjectAnimator.shared.remove(region: self) From 430d49b1e8ea21986144e13c09a5fdcb829a96d1 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 12:38:22 +0200 Subject: [PATCH 047/165] User descriptor initialization --- .../Core/Data Pool/Regions/BLOCKvRegion.swift | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index ea84f889..2afffcec 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -10,6 +10,7 @@ // import Foundation +//import DictionaryCoding /* Issues: @@ -244,19 +245,48 @@ class BLOCKvRegion: Region { let actions = objects.values.filter { $0.type == "action" && ($0.data?["name"] as? String)? .starts(with: actionNamePrefix) == true } objectData["actions"] = actions.map { $0.data } - + + // Experiment 1: Descriptor initialiser + do { - if JSONSerialization.isValidJSONObject(objectData) { - let rawData = try JSONSerialization.data(withJSONObject: objectData) - let vatoms = try JSONDecoder.blockv.decode(VatomModel.self, from: rawData) - return vatoms - } else { - throw NSError.init("Invalid JSON for Vatom: \(object.id)") - } + let faces = faces.compactMap { $0.data }.compactMap { try? FaceModel(from: $0) } + let actions = actions.compactMap { $0.data }.compactMap { try? ActionModel(from: $0) } + var vatom = try VatomModel(from: objectData) + vatom.faceModels = faces + vatom.actionModels = actions + return vatom } catch { printBV(error: error.localizedDescription) return nil } + + // Experiemnt 2: Dictionary decoder (dictionary > vatom model) + +// let decoder = DictionaryDecoder() +// decoder.dateDecodingStrategy = .iso8601 +// +// do { +// let vatoms = try decoder.decode(VatomModel.self, from: objectData) +// return vatoms +// } catch { +// printBV(error: error.localizedDescription) +// return nil +// } + + // Experiment 3: Data decoder (json decoder) + +// do { +// if JSONSerialization.isValidJSONObject(objectData) { +// let rawData = try JSONSerialization.data(withJSONObject: objectData) +// let vatoms = try JSONDecoder.blockv.decode(VatomModel.self, from: rawData) +// return vatoms +// } else { +// throw NSError.init("Invalid JSON for Vatom: \(object.id)") +// } +// } catch { +// printBV(error: error.localizedDescription) +// return nil +// } } From b1eaa3362718778ecee857708d33e031cb25ca0f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 12:38:43 +0200 Subject: [PATCH 048/165] Elevate access control --- BlockV/Core/Network/Models/Package/ActionModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Network/Models/Package/ActionModel.swift b/BlockV/Core/Network/Models/Package/ActionModel.swift index 3df60763..eb409d5c 100644 --- a/BlockV/Core/Network/Models/Package/ActionModel.swift +++ b/BlockV/Core/Network/Models/Package/ActionModel.swift @@ -84,7 +84,7 @@ extension ActionModel: Codable { extension ActionModel { /// Extract action and template name from the compound name. - private static func splitCompoundName(_ compoundName: String) throws -> (String, String) { + static func splitCompoundName(_ compoundName: String) throws -> (String, String) { // find the marker guard let markerRange = compoundName.range(of: "::action::", From 42b29d00c4f197f8e32648753f2dd5cdbd68a45c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 12:38:59 +0200 Subject: [PATCH 049/165] Descriptor initialiser --- .../Package/VatomModel+Descriptor.swift | 376 ++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift diff --git a/BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift new file mode 100644 index 00000000..dcbe37cc --- /dev/null +++ b/BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift @@ -0,0 +1,376 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import GenericJSON + +protocol Descriptable { + init(from descriptor: [String: Any]) throws +} + +extension VatomModel: Descriptable { + + init(from descriptor: [String: Any]) throws { + + guard + let _id = descriptor["id"] as? String, + let _version = descriptor["version"] as? String, + let _whenCreated = descriptor["when_created"] as? String, + let _whenModified = descriptor["when_modified"] as? String, + let _isUnpublished = descriptor["unpublished"] as? Bool, + let _private = descriptor["private"] as? [String: Any], + let _rootDescriptor = descriptor["vAtom::vAtomType"] as? [String: Any] + + else { throw BVError.modelDecoding(reason: "Model decoding failed: \(type(of: self))") } + + self.id = _id + self.version = _version + self.whenCreated = DateFormatter.blockvDateFormatter.date(from: _whenCreated)! //FIXME: force + self.whenModified = DateFormatter.blockvDateFormatter.date(from: _whenModified)! //FIXME: force + self.isUnpublished = _isUnpublished + self.private = try? JSON.init(_private) + self.props = try RootProperties(from: _rootDescriptor) + + self.faceModels = [] + self.actionModels = [] + + self.eth = nil + self.eos = nil + + } + +} + +extension RootProperties: Descriptable { + + init(from descriptor: [String : Any]) throws { + + guard + let _author = descriptor["author"] as? String, + let _rootType = descriptor["category"] as? String, + let _templateID = descriptor["template"] as? String, + let _templateVariationID = descriptor["template_variation"] as? String, + let _publisherFQDN = descriptor["publisher_fqdn"] as? String, + let _title = descriptor["title"] as? String, + let _description = descriptor["description"] as? String, + + let _category = descriptor["category"] as? String, + let _clonedFrom = descriptor["cloned_from"] as? String, + let _cloningScore = descriptor["cloning_score"] as? Double, + let _commerceDescriptor = descriptor["commerce"] as? [String: Any], + let _isInContract = descriptor["in_contract"] as? Bool, + let _inContractWith = descriptor["in_contract_with"] as? String, + let _notifyMessage = descriptor["notify_msg"] as? String, + let _numberDirectClones = descriptor["num_direct_clones"] as? Int, + let _owner = descriptor["owner"] as? String, + let _parentID = descriptor["parent_id"] as? String, + let _transferredBy = descriptor["transferred_by"] as? String, + let _visibilityDescriptor = descriptor["visibility"] as? [String: Any], + + let _isAcquirable = descriptor["acquirable"] as? Bool, + let _isRedeemable = descriptor["redeemable"] as? Bool, + let _isDisabled = descriptor["disabled"] as? Bool, + let _isDropped = descriptor["dropped"] as? Bool, + let _isTradeable = descriptor["tradeable"] as? Bool, + let _isTransferable = descriptor["transferable"] as? Bool, + + let _geoPositionDescriptor = descriptor["geo_pos"] as? [String: Any], + let _resourceDescriptor = descriptor["resources"] as? [[String: Any]] + + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + // decode if present + let _childPolicyDescriptor = descriptor["child_policy"] as? [[String: Any]] ?? [] + let _tags = descriptor["tags"] as? [String] ?? [] + + self.author = _author + self.rootType = _rootType + self.templateID = _templateID + self.templateVariationID = _templateVariationID + self.publisherFQDN = _publisherFQDN + self.title = _title + self.description = _description + + self.category = _category + self.childPolicy = _childPolicyDescriptor.compactMap { try? VatomChildPolicy(from: $0) } + self.clonedFrom = _clonedFrom + self.cloningScore = _cloningScore + self.commerce = try Commerce(from: _commerceDescriptor) + self.isInContract = _isInContract + self.inContractWith = _inContractWith + self.notifyMessage = _notifyMessage + self.numberDirectClones = _numberDirectClones + self.owner = _owner + self.parentID = _parentID + self.tags = _tags + self.transferredBy = _transferredBy + self.visibility = try Visibility(from: _visibilityDescriptor) + + self.isAcquirable = _isAcquirable + self.isRedeemable = _isRedeemable + self.isDisabled = _isDisabled + self.isDropped = _isDropped + self.isTradeable = _isTradeable + self.isTransferable = _isTransferable + + self.geoPosition = try GeoPosition(from: _geoPositionDescriptor) + self.resources = _resourceDescriptor.compactMap { try? VatomResourceModel(from: $0) } + + } + +} + +extension RootProperties.Visibility: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _type = descriptor["type"] as? String, + let _value = descriptor["value"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.type = _type + self.value = _value + + } + +} + +extension RootProperties.Commerce: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _pricingDescriptor = descriptor["pricing"] as? [String: Any], + let _vatomPricing = try? VatomPricing(from: _pricingDescriptor) + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.pricing = _vatomPricing + + } + +} + +extension VatomPricing: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _pricingType = descriptor["pricingType"] as? String, + let _valueDescriptor = descriptor["value"] as? [String: Any], + let _currency = _valueDescriptor["currency"] as? String, + let _price = _valueDescriptor["price"] as? String, + let _validFrom = _valueDescriptor["valid_from"] as? String, + let _validThrough = _valueDescriptor["valid_through"] as? String, + let _isVatIncluded = _valueDescriptor["vat_included"] as? Bool + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.pricingType = _pricingType + self.currency = _currency + self.price = _price + self.validFrom = _validFrom + self.validThrough = _validThrough + self.isVatIncluded = _isVatIncluded + } + +} + +extension VatomChildPolicy: Descriptable { + + init(from descriptor: [String : Any]) throws { + + guard + let _count = descriptor["count"] as? Int, + let _creationPolicyDescriptor = descriptor["creation_policy"] as? [String: Any], + let _templateVariationID = descriptor["template_variation"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.count = _count + self.creationPolicy = try CreationPolicy(from: _creationPolicyDescriptor) + self.templateVariationID = _templateVariationID + } + +} + +extension VatomChildPolicy.CreationPolicy: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _autoCreate = descriptor["auto_create"] as? String, + let _autoCreateCount = descriptor["auto_create_count"] as? Int, + let _autoCreateCountRandom = descriptor["auto_create_count_random"] as? Bool, + let _enforcePolicyCountMax = descriptor["enforce_policy_count_max"] as? Bool, + let _enforcePolicyCountMin = descriptor["enforce_policy_count_min"] as? Bool, + let _policyCountMax = descriptor["policy_count_max"] as? Int, + let _policyCountMin = descriptor["policy_count_min"] as? Int + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.autoCreate = _autoCreate + self.autoCreateCount = _autoCreateCount + self.autoCreateCountRandom = _autoCreateCountRandom + self.enforcePolicyCountMax = _enforcePolicyCountMax + self.enforcePolicyCountMin = _enforcePolicyCountMin + self.policyCountMax = _policyCountMax + self.policyCountMin = _policyCountMin + + } + +} + +extension RootProperties.GeoPosition: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _type = descriptor["type"] as? String, + let _coordinates = descriptor["coordinates"] as? [Double] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.type = _type + self.coordinates = _coordinates + } + +} + +extension VatomResourceModel: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _name = descriptor["name"] as? String, + let _type = descriptor["resourceType"] as? String, + let _value = (descriptor["value"] as? [String: Any])?["value"] as? String, + let _url = URL(string: _value) + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.name = _name + self.type = _type + self.url = _url + + } + +} + +// MARK: - FaceModel + Descriptable + +extension FaceModel: Descriptable { + + init(from descriptor: [String : Any]) throws { + + guard + let _id = descriptor["id"] as? String, + let _templateID = descriptor["template"] as? String, + let _metaDescriptor = descriptor["meta"] as? [String: Any], + let _propertiesDescriptor = descriptor["properties"] as? [String: Any] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.id = _id + self.templateID = _templateID + self.properties = try Properties(from: _propertiesDescriptor) + self.meta = try MetaModel(from: _metaDescriptor) + // convenience + isNative = properties.displayURL.hasPrefix("native://") + isWeb = properties.displayURL.hasPrefix("https://") + + } + + +} + +extension FaceModel.Properties: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _displayURL = descriptor["display_url"] as? String, + let _constraintsDescriptor = descriptor["constraints"] as? [String: Any], + let _resources = descriptor["resources"] as? [String] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + let _config = descriptor["config"] as? [String: Any] + + self.displayURL = _displayURL + self.constraints = try FaceModel.Properties.Constraints(from: _constraintsDescriptor) + self.resources = _resources + self.config = try? JSON(_config) + + } + +} + +extension FaceModel.Properties.Constraints: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _viewMode = descriptor["view_mode"] as? String, + let _platform = descriptor["platform"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.viewMode = _viewMode + self.platform = _platform + + } + +} + +extension MetaModel: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _createdBy = descriptor["created_by"] as? String, + let _dataType = descriptor["data_type"] as? String, + let _whenCreated = descriptor["when_created"] as? String, + let _whenModified = descriptor["when_modified"] as? String, + let _whenCreatedDate = DateFormatter.blockvDateFormatter.date(from: _whenCreated), + let _whenModifiedDate = DateFormatter.blockvDateFormatter.date(from: _whenModified) + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.init(createdBy: _createdBy, + dataType: _dataType, + whenCreated: _whenCreatedDate, + whenModified: _whenModifiedDate) + + } + +} + +// MARK: - FaceModel + Descriptable + +extension ActionModel: Descriptable { + + init(from descriptor: [String : Any]) throws { + + guard + let _compoundName = descriptor["name"] as? String, + let _metaDescriptor = descriptor["meta"] as? [String: Any], + let _propertiesDescriptor = descriptor["properties"] as? [String: Any] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + let (templateID, actionName) = try ActionModel.splitCompoundName(_compoundName) + let meta = try MetaModel(from: _metaDescriptor) + let properties = try Properties(from: _propertiesDescriptor) + + self.init(compoundName: _compoundName, + name: actionName, + templateID: templateID, + meta: meta, + properties: properties) + + } + + +} + +extension ActionModel.Properties: Descriptable { + + init(from descriptor: [String : Any]) throws { + guard + let _reactor = descriptor["reactor"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.init(reactor: _reactor) + } + +} From 00931054cf58b1760fad3753fbf1b7d8ddbbbd41 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 14:27:27 +0200 Subject: [PATCH 050/165] Use Result type for completion handlers --- .../Helpers/Vatom+PreemptiveActions.swift | 13 +- BlockV/Core/Network/Stack/Client.swift | 25 +- .../Requests/BLOCKv+ActivityRequests.swift | 67 ++--- .../Core/Requests/BLOCKv+AuthRequests.swift | 105 +++---- .../Core/Requests/BLOCKv+UserRequests.swift | 233 +++++++-------- .../Core/Requests/BLOCKv+VatomRequest.swift | 250 ++++++++-------- .../Face Views/Utilities/VatomObserver.swift | 20 +- .../Utilities/VatomObserverStore.swift | 72 ++--- .../Web/Face Bridge/CoreBridgeV1.swift | 270 ++++++++++-------- .../Web/Face Bridge/CoreBridgeV2.swift | 91 +++--- 10 files changed, 599 insertions(+), 547 deletions(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index 329cd6aa..2d8a27d2 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -185,15 +185,18 @@ extension VatomModel { } // perform the action - BLOCKv.performAction(name: name, payload: payload) { (payload, error) in - // convert closure to result - if let payload = payload, error == nil { + BLOCKv.performAction(name: name, payload: payload) { result in + + switch result { + case .success(let payload): completion(.success(payload)) - } else { + + case .failure(let error): // run undo closures undos.forEach { $0() } - completion(.failure(error!)) + completion(.failure(error)) } + } } diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index 71f2715b..e7e5e45f 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -14,13 +14,13 @@ import Alamofire protocol ClientProtocol { - typealias RawCompletion = (_ data: Data?, _ error: BVError?) -> Void + typealias RawCompletion = (Result) -> Void /// Request that returns raw data. func request(_ endpoint: Endpoint, completion: @escaping RawCompletion) /// Request that returns native object (must conform to decodable). - func request(_ endpoint: Endpoint, completion: @escaping (T?, BVError?) -> Void ) where T: Decodable + func request(_ endpoint: Endpoint, completion: @escaping (Result) -> Void ) where T: Decodable } @@ -133,18 +133,19 @@ final class Client: ClientProtocol { request.responseData(queue: queue) { (dataResponse) in switch dataResponse.result { - case let .success(data): completion(data, nil) + case let .success(data): + completion(.success(data)) case let .failure(err): //TODO: The error should be parsed and a BVError created and passed in. // check for a BVError if let err = err as? BVError { - completion(nil, err) + completion(.failure(err)) } else { // create a wrapped networking error let error = BVError.networking(error: err) - completion(nil, error) + completion(.failure(error)) } } } @@ -152,7 +153,7 @@ final class Client: ClientProtocol { } /// JSON Completion handler. - typealias JSONCompletion = (_ json: Any?, _ error: BVError?) -> Void + typealias JSONCompletion = (Result) -> Void func requestJSON(_ endpoint: Endpoint, completion: @escaping JSONCompletion) { @@ -169,11 +170,11 @@ final class Client: ClientProtocol { request.responseJSON(queue: queue) { dataResponse in switch dataResponse.result { case let .success(json): - completion(json, nil) + completion(.success(json)) case let .failure(err): // create a wrapped networking error let error = BVError.networking(error: err) - completion(nil, error) + completion(.failure(error)) } } @@ -185,7 +186,7 @@ final class Client: ClientProtocol { /// - endpoint: Endpoint for the request /// - completion: The completion handler to call when the request is completed. func request(_ endpoint: Endpoint, - completion: @escaping (Response?, BVError?) -> Void ) where Response: Decodable { + completion: @escaping (Result) -> Void ) where Response: Decodable { // create request (starts immediately) let request = self.sessionManager.request( @@ -228,7 +229,7 @@ final class Client: ClientProtocol { // return // } - completion(val, nil) + completion(.success(val)) //TODO: Add some thing like this to pull back to a completion thread? /* @@ -246,10 +247,10 @@ final class Client: ClientProtocol { //FIXME: Can this error casting be done away with? if let err = err as? BVError { - completion(nil, err) + completion(.failure(err)) } else { let error = BVError.networking(error: err) - completion(nil, error) + completion(.failure(error)) } } diff --git a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift index daa81353..e0954a6c 100644 --- a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift @@ -25,23 +25,23 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func getActivityThreads(cursor: String = "", count: Int = 0, - completion: @escaping (ThreadListModel?, Error?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.UserActivity.getThreads(cursor: cursor, count: count) - self.client.request(endpoint) { (baseModel, error) in - - // extract model, ensure no error - guard let threadListModel = baseModel?.payload, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(threadListModel, nil) } } @@ -61,23 +61,23 @@ extension BLOCKv { public static func getActivityMessages(forThreadId threadId: String, cursor: String = "", count: Int = 0, - completion: @escaping (MessageListModel?, Error?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.UserActivity.getMessages(forThreadId: threadId, cursor: cursor, count: count) - self.client.request(endpoint) { (baseModel, error) in - - // extract model, ensure no error - guard let messageListModel = baseModel?.payload, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(messageListModel, nil) } } @@ -96,18 +96,19 @@ extension BLOCKv { let endpoint = API.CurrentUser.sendMessage(message, toUserId: userId) - self.client.request(endpoint) { (baseModel, error) in - - guard baseModel?.payload != nil, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available + DispatchQueue.main.async { + completion(nil) + } + case .failure(let error): + // handle error DispatchQueue.main.async { completion(error) } - return - } - - // call was successful - DispatchQueue.main.async { - completion(nil) } } diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index 09af50f5..faab6f84 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -28,7 +28,7 @@ extension BLOCKv { public static func register(withUserToken token: String, type: UserTokenType, userInfo: UserInfo? = nil, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let registerToken = UserToken(value: token, type: type) self.register(tokens: [registerToken], userInfo: userInfo, completion: completion) } @@ -43,7 +43,7 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func register(withOAuthToken oauthToken: OAuthTokenRegisterParams, userInfo: UserInfo? = nil, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { self.register(tokens: [oauthToken], userInfo: userInfo, completion: completion) } @@ -56,28 +56,29 @@ extension BLOCKv { /// authorized to perform requests. public static func register(tokens: [RegisterTokenParams], userInfo: UserInfo? = nil, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.Session.register(tokens: tokens, userInfo: userInfo) - self.client.request(endpoint) { (baseModel, error) in - - // extract model, ensure no error - guard let authModel = baseModel?.payload, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + let authModel = baseModel.payload + // persist credentials + CredentialStore.saveRefreshToken(authModel.refreshToken) + CredentialStore.saveAssetProviders(authModel.assetProviders) + // noifty + self.onLogin() + completion(.success(authModel.user)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - // persist credentials - CredentialStore.saveRefreshToken(authModel.refreshToken) - CredentialStore.saveAssetProviders(authModel.assetProviders) - // noifty - self.onLogin() - completion(authModel.user, nil) } } @@ -97,7 +98,7 @@ extension BLOCKv { public static func login(withUserToken token: String, type: UserTokenType, password: String, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let params = UserTokenLoginParams(value: token, type: type, password: password) self.login(tokenParams: params, completion: completion) } @@ -111,7 +112,7 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func login(withOAuthToken oauthToken: String, provider: String, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let params = OAuthTokenLoginParams(provider: provider, oauthToken: oauthToken) self.login(tokenParams: params, completion: completion) } @@ -123,37 +124,37 @@ extension BLOCKv { /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. public static func login(withGuestID id: String, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let params = GuestIdLoginParams(id: id) self.login(tokenParams: params, completion: completion) } /// Login using token params fileprivate static func login(tokenParams: LoginTokenParams, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.Session.login(tokenParams: tokenParams) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let authModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + let authModel = baseModel.payload + // persist credentials + CredentialStore.saveRefreshToken(authModel.refreshToken) + CredentialStore.saveAssetProviders(authModel.assetProviders) + // notify + self.onLogin() + // completion + completion(.success(authModel.user)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - - // persist credentials - CredentialStore.saveRefreshToken(authModel.refreshToken) - CredentialStore.saveAssetProviders(authModel.assetProviders) - // notify - self.onLogin() - // completion - completion(authModel.user, nil) } } @@ -173,24 +174,24 @@ extension BLOCKv { let endpoint = API.CurrentUser.logOut() - self.client.request(endpoint) { (baseModel, error) in - + self.client.request(endpoint) { result in + // reset DispatchQueue.main.async { reset() } - - // extract model, ensure no error - guard baseModel?.payload != nil, error == nil else { + + switch result { + case .success(let model): + // model is available DispatchQueue.main.async { - completion(error!) + completion(nil) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(error) } - return - } - - // model is available - DispatchQueue.main.async { - completion(nil) } } diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index 4aff3cb3..036eeb0a 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -20,23 +20,23 @@ extension BLOCKv { /// /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. - public static func getCurrentUser(completion: @escaping (UserModel?, BVError?) -> Void) { + public static func getCurrentUser(completion: @escaping (Result) -> Void) { let endpoint = API.CurrentUser.get() - self.client.request(endpoint) { (baseModel, error) in - - // extract model, ensure no error - guard let userModel = baseModel?.payload, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userModel, nil) } } @@ -51,23 +51,23 @@ extension BLOCKv { /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. public static func updateCurrentUser(_ userInfo: UserInfo, - completion: @escaping (UserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.CurrentUser.update(userInfo: userInfo) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let userModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userModel, nil) } } @@ -130,24 +130,24 @@ extension BLOCKv { public static func verifyUserToken(_ token: String, type: UserTokenType, code: String, - completion: @escaping (UserToken?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let userToken = UserToken(value: token, type: type) let endpoint = API.CurrentUser.verifyToken(userToken, code: code) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let userTokenModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userTokenModel, nil) } } @@ -166,24 +166,24 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func resetVerification(forUserToken token: String, type: UserTokenType, - completion: @escaping (UserToken?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let userToken = UserToken(value: token, type: type) let endpoint = API.CurrentUser.resetTokenVerification(forToken: userToken) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, handle error - guard let userTokenModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userTokenModel, nil) } } @@ -202,24 +202,24 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func resetToken(_ token: String, type: UserTokenType, - completion: @escaping (UserToken?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let userToken = UserToken(value: token, type: type) let endpoint = API.CurrentUser.resetToken(userToken) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let userTokenModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userTokenModel, nil) } } @@ -237,23 +237,23 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func addCurrentUserToken(token: UserToken, isPrimary: Bool = false, - completion: @escaping (FullTokenModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.CurrentUser.addToken(token, isPrimary: isPrimary) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, handle error - guard let fullToken = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(fullToken, nil) } } @@ -264,23 +264,23 @@ extension BLOCKv { /// /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. - public static func getCurrentUserTokens(completion: @escaping ([FullTokenModel]?, BVError?) -> Void) { + public static func getCurrentUserTokens(completion: @escaping (Result<[FullTokenModel], BVError>) -> Void) { let endpoint = API.CurrentUser.getTokens() - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, handle error - guard let fullTokens = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(fullTokens, nil) } } @@ -300,18 +300,19 @@ extension BLOCKv { let endpoint = API.CurrentUser.deleteToken(id: tokenId) - self.client.request(endpoint) { (baseModel, error) in - - guard baseModel?.payload != nil, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available + DispatchQueue.main.async { + completion(nil) + } + case .failure(let error): + // handle error DispatchQueue.main.async { completion(error) } - return - } - - // call was successful - DispatchQueue.main.async { - completion(nil) } } @@ -334,19 +335,19 @@ extension BLOCKv { let endpoint = API.CurrentUser.setDefaultToken(id: tokenId) - self.client.request(endpoint) { (baseModel, error) in - - // - guard baseModel?.payload != nil, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available + DispatchQueue.main.async { + completion(nil) + } + case .failure(let error): + // handle error DispatchQueue.main.async { completion(error) } - return - } - - // call was succesful - DispatchQueue.main.async { - completion(nil) } } @@ -365,23 +366,23 @@ extension BLOCKv { /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. public static func getPublicUser(withID userId: String, - completion: @escaping (PublicUserModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.PublicUser.get(id: userId) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let userModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - completion(userModel, nil) } } @@ -395,17 +396,19 @@ extension BLOCKv { let endpoint = API.CurrentUser.deleteCurrentUser() - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - guard baseModel?.payload != nil, error == nil else { + switch result { + case .success(let baseModel): + // model is available + DispatchQueue.main.async { + completion(nil) + } + case .failure(let error): + // handle error DispatchQueue.main.async { completion(error) } - return - } - - DispatchQueue.main.async { - completion(nil) } } diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index 5c7714d3..b83c382c 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -36,25 +36,25 @@ extension BLOCKv { public static func getInventory(id: String = ".", page: Int = 0, limit: Int = 0, - completion: @escaping (_ vatoms: [VatomModel], _ error: BVError?) -> Void) { + completion: @escaping (Result<[VatomModel], BVError>) -> Void) { let endpoint = API.UserVatom.getInventory(parentID: id, page: page, limit: limit) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let unpackedModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available + let unpackedModel = baseModel.payload + let packedVatoms = unpackedModel.package() DispatchQueue.main.async { - completion([], error!) + completion(.success(packedVatoms)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - let packedVatoms = unpackedModel.package() - - DispatchQueue.main.async { - completion(packedVatoms, nil) } } @@ -73,25 +73,25 @@ extension BLOCKv { /// action models as populated properties. /// - error: BLOCKv error. public static func getVatoms(withIDs ids: [String], - completion: @escaping (_ vatoms: [VatomModel], _ error: BVError?) -> Void) { + completion: @escaping (Result<[VatomModel], BVError>) -> Void) { let endpoint = API.UserVatom.getVatoms(withIDs: ids) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard let unpackedModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available + let unpackedModel = baseModel.payload + let packedVatoms = unpackedModel.package() DispatchQueue.main.async { - completion([], error!) + completion(.success(packedVatoms)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - let packedVatoms = unpackedModel.package() - - DispatchQueue.main.async { - completion(packedVatoms, nil) } } @@ -110,19 +110,19 @@ extension BLOCKv { let endpoint = API.UserVatom.trashVatom(id) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, ensure no error - guard baseModel?.payload.message != nil, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { completion(nil) } - return - } - - // model is available - DispatchQueue.main.async { - completion(error) + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(error) + } } } @@ -140,12 +140,25 @@ extension BLOCKv { /// action models as populated properties. /// - error: BLOCKv error. public static func discover(_ builder: DiscoverQueryBuilder, - completion: @escaping(_ vatoms: [VatomModel], _ error: BVError?) -> Void) { + completion: @escaping(Result<[VatomModel], BVError>) -> Void) { // explicitly set return type to payload builder.setReturn(type: .payload) - self.discover(payload: builder.toDictionary()) { (result, error) in - completion(result?.vatoms ?? [], error) + self.discover(payload: builder.toDictionary()) { result in + + switch result { + case .success(let discoverResult): + // model is available + DispatchQueue.main.async { + completion(.success(discoverResult.vatoms)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } } @@ -165,27 +178,25 @@ extension BLOCKv { /// - count: Number of discovered vAtoms. /// - error: BLOCKv error. public static func discover(payload: [String: Any], - completion: @escaping (_ result: DiscoverResult?, _ error: BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.VatomDiscover.discover(payload) - self.client.request(endpoint) { (baseModel, error) in - - // extract model, handle error - guard let unpackedModel = baseModel?.payload, error == nil else { + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available + let unpackedModel = baseModel.payload + let packedVatoms = unpackedModel.package() DispatchQueue.main.async { - print(error!.localizedDescription) - completion(nil, error!) + completion(.success((packedVatoms, packedVatoms.count))) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - let packedVatoms = unpackedModel.package() - - DispatchQueue.main.async { - //print(model) - completion((packedVatoms, packedVatoms.count), nil) } } @@ -215,7 +226,7 @@ extension BLOCKv { topRightLat: Double, topRightLon: Double, filter: VatomGeoFilter = .vatoms, - completion: @escaping (_ vatoms: [VatomModel], _ error: BVError?) -> Void) { + completion: @escaping (Result<[VatomModel], BVError>) -> Void) { let endpoint = API.VatomDiscover.geoDiscover(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, @@ -223,23 +234,21 @@ extension BLOCKv { topRightLon: topRightLon, filter: filter.rawValue) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract model, handle error - guard let unpackedModel = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available + let unpackedModel = baseModel.payload + let packedVatoms = unpackedModel.package() DispatchQueue.main.async { - print(error!.localizedDescription) - completion([], error!) + completion(.success(packedVatoms)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - let packedVatoms = unpackedModel.package() - - DispatchQueue.main.async { - //print(model) - completion(packedVatoms, nil) } } @@ -263,7 +272,7 @@ extension BLOCKv { topRightLon: Double, precision: Int, filter: VatomGeoFilter = .vatoms, - completion: @escaping (GeoModel?, BVError?) -> Void) { + completion: @escaping (Result) -> Void) { let endpoint = API.VatomDiscover.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, @@ -272,21 +281,19 @@ extension BLOCKv { precision: precision, filter: filter.rawValue) - BLOCKv.client.request(endpoint) { (baseModel, error) in + BLOCKv.client.request(endpoint) { result in - // extract model, handle error - guard let geoGroupModels = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - print(error!.localizedDescription) - completion(nil, error!) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // model is available - DispatchQueue.main.async { - //print(model) - completion(geoGroupModels, nil) } } @@ -302,23 +309,23 @@ extension BLOCKv { /// - completion: The completion handler to call when the call is completed. /// This handler is executed on the main queue. public static func getActions(forTemplateID id: String, - completion: @escaping ([ActionModel]?, BVError?) -> Void) { + completion: @escaping (Result<[ActionModel], BVError>) -> Void) { let endpoint = API.UserActions.getActions(forTemplateID: id) - self.client.request(endpoint) { (baseModel, error) in + self.client.request(endpoint) { result in - // extract array of actions, ensure no error - guard let actions = baseModel?.payload, error == nil else { + switch result { + case .success(let baseModel): + // model is available DispatchQueue.main.async { - completion(nil, error!) + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) } - return - } - - // data is available - DispatchQueue.main.async { - completion(actions, nil) } } @@ -336,39 +343,43 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func performAction(name: String, payload: [String: Any], - completion: @escaping ([String: Any]?, BVError?) -> Void) { + completion: @escaping (Result<[String: Any], BVError>) -> Void) { let endpoint = API.VatomAction.custom(name: name, payload: payload) - self.client.request(endpoint) { (data, error) in - - // extract data, ensure no error - guard let data = data, error == nil else { - DispatchQueue.main.async { - completion(nil, error!) + self.client.request(endpoint) { result in + + switch result { + case .success(let data): + + do { + guard + let object = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let payload = object["payload"] as? [String: Any] else { + throw BVError.modelDecoding(reason: "Unable to extract payload.") + } + // model is available + DispatchQueue.main.async { + completion(.success(payload)) + } + + } catch { + let error = BVError.modelDecoding(reason: error.localizedDescription) + completion(.failure(error)) } - return - } - // - do { - guard - let object = try JSONSerialization.jsonObject(with: data) as? [String: Any], - let payload = object["payload"] as? [String: Any] else { - throw BVError.modelDecoding(reason: "Unable to extract payload.") - } - // json is available + + case .failure(let error): + // handle error DispatchQueue.main.async { - completion(payload, nil) + completion(.failure(error)) } - - } catch { - let error = BVError.modelDecoding(reason: error.localizedDescription) - completion(nil, error) } } } + + // MARK: - Common Actions for Unowned vAtoms /// Performs an acquire action on a vAtom. /// @@ -380,13 +391,16 @@ extension BLOCKv { /// - completion: The completion handler to call when the action is completed. /// This handler is executed on the main queue. public static func acquireVatom(withID id: String, - completion: @escaping ([String: Any]?, BVError?) -> Void) { + completion: @escaping (Result<[String: Any], BVError>) -> Void) { let body = ["this.id": id] // perform the action - self.performAction(name: "Acquire", payload: body) { (data, error) in - completion(data, error) + self.performAction(name: "Acquire", payload: body) { result in + + //FIXME: Call into Data Pool + + completion(result) } } diff --git a/BlockV/Face/Face Views/Utilities/VatomObserver.swift b/BlockV/Face/Face Views/Utilities/VatomObserver.swift index c5513c85..0099a2ef 100644 --- a/BlockV/Face/Face Views/Utilities/VatomObserver.swift +++ b/BlockV/Face/Face Views/Utilities/VatomObserver.swift @@ -158,17 +158,17 @@ class VatomObserver { /// Replace all the root vAtom's direct children using remote state. private func updateChildVatoms() { - BLOCKv.getInventory(id: self.rootVatomID) { [weak self] (vatoms, error) in - - // ensure no error - guard error == nil else { - printBV(error: "Unable to fetch children. Error: \(String(describing: error?.localizedDescription))") - return + BLOCKv.getInventory(id: self.rootVatomID) { [weak self] result in + + switch result { + case .success(let vatoms): + // ensure correct parent ID + let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } + // replace the list of children + self?.childVatomIDs = Set(validChildren.map { $0.id }) + case .failure(let error): + printBV(error: "Unable to fetch children. Error: \(String(describing: error.localizedDescription))") } - // ensure correct parent ID - let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } - // replace the list of children - self?.childVatomIDs = Set(validChildren.map { $0.id }) } diff --git a/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift b/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift index 5e507061..cd07ab2a 100644 --- a/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift +++ b/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift @@ -183,17 +183,22 @@ class VatomObserverStore { /// Fetch root vAtom's remote state. private func updateRootVatom(completion: Completion?) { - BLOCKv.getVatoms(withIDs: [self.rootVatomID]) { [weak self] (vatoms, error) in + BLOCKv.getVatoms(withIDs: [self.rootVatomID]) { [weak self] result in - // ensure no error - guard let rootVatom = vatoms.first, error == nil else { - //printBV(error: "Unable to fetch root vAtom: \(String(describing: error?.localizedDescription))") + switch result { + case .success(let vatoms): + // ensure no error + guard let rootVatom = vatoms.first else { + return + } + // update root vAtom + self?.rootVatom = rootVatom + completion?(nil) + + case .failure(let error): completion?(error) - return } - // update root vAtom - self?.rootVatom = rootVatom - completion?(nil) + } } @@ -201,19 +206,23 @@ class VatomObserverStore { /// Fetch remote state for the specified child vAtom and adds it to the list of children. private func addChildVatom(withID childID: String) { - BLOCKv.getVatoms(withIDs: [childID]) { [weak self] (vatoms, error) in - - // ensure no error - guard let childVatom = vatoms.first, error == nil else { - //printBV(error: "Unable to vAtom. Error: \(String(describing: error?.localizedDescription))") - return - } - - // ensure the vatom is still a child - // there is a case, due to the async arch, where the retrieved vAtom may no longer be a child - if childVatom.props.parentID == self?.rootVatomID { - // insert child (async) - self?.childVatoms.insert(childVatom) + BLOCKv.getVatoms(withIDs: [childID]) { [weak self] result in + + switch result { + case .success(let vatoms): + + guard let childVatom = vatoms.first else { + return + } + + // ensure the vatom is still a child + // there is a case, due to the async arch, where the retrieved vAtom may no longer be a child + if childVatom.props.parentID == self?.rootVatomID { + // insert child (async) + self?.childVatoms.insert(childVatom) + } + case .failure(let error): + printBV(error: "Unable to add child. Error: \(String(describing: error.localizedDescription))") } } @@ -223,19 +232,18 @@ class VatomObserverStore { /// Replace all the root vAtom's direct children using remote state. private func updateChildVatoms(completion: Completion?) { - BLOCKv.getInventory(id: self.rootVatomID) { [weak self] (vatoms, error) in - - // ensure no error - guard error == nil else { - //printBV(error: "Unable to fetch children. Error: \(String(describing: error?.localizedDescription))") + BLOCKv.getInventory(id: self.rootVatomID) { [weak self] result in + + switch result { + case .success(let vatoms): + // ensure correct parent ID + let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } + // replace the list of children + self?.childVatoms = Set(validChildren) + completion?(nil) + case .failure(let error): completion?(error) - return } - // ensure correct parent ID - let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } - // replace the list of children - self?.childVatoms = Set(validChildren) - completion?(nil) } diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift index fab3198f..81ddef9d 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift @@ -213,58 +213,62 @@ class CoreBridgeV1: CoreBridge { let viewMode = faceView.faceModel.properties.constraints.viewMode // async fetch current user - BLOCKv.getCurrentUser { [weak self] (user, error) in - - // ensure no error - guard let user = user, error == nil else { - let bridgeError = BridgeError.viewer("Unable to fetch current user.") - completion(nil, bridgeError) - return - } - // encode url - var encodedURL: URL? - if let url = user.avatarURL { - encodedURL = try? BLOCKv.encodeURL(url) - } - // build user - let userInfo = BRUser(id: user.id, - firstName: user.firstName, - lastName: user.lastName, - avatarURL: encodedURL?.absoluteString ?? "") - - // fetch backing vAtom - self?.getVatomsFormatted(withIDs: [faceView.vatom.id], completion: { (vatoms, error) in - - // ensure no error - guard error == nil else { - let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") - completion(nil, bridgeError) - return + BLOCKv.getCurrentUser { [weak self] result in + + switch result { + case .success(let user): + // model is available + // encode url + var encodedURL: URL? + if let url = user.avatarURL { + encodedURL = try? BLOCKv.encodeURL(url) } - // ensure a single vatom - guard let firstVatom = vatoms.first else { - let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") - completion(nil, bridgeError) - return - } - // create bridge response - let vatomInfo = BRVatom(id: firstVatom.id, - properties: firstVatom.properties, - resources: firstVatom.resources) - let response = BRSetup(viewMode: viewMode, - user: userInfo, - vatomInfo: vatomInfo) - - do { - // json encode the model - let json = try JSON.init(encodable: response) - completion(json, nil) - } catch { - let bridgeError = BridgeError.viewer("Unable to encode response.") + // build user + let userInfo = BRUser(id: user.id, + firstName: user.firstName, + lastName: user.lastName, + avatarURL: encodedURL?.absoluteString ?? "") + + // fetch backing vAtom + self?.getVatomsFormatted(withIDs: [faceView.vatom.id], completion: { (vatoms, error) in + + // ensure no error + guard error == nil else { + let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") + completion(nil, bridgeError) + return + } + // ensure a single vatom + guard let firstVatom = vatoms.first else { + let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") + completion(nil, bridgeError) + return + } + // create bridge response + let vatomInfo = BRVatom(id: firstVatom.id, + properties: firstVatom.properties, + resources: firstVatom.resources) + let response = BRSetup(viewMode: viewMode, + user: userInfo, + vatomInfo: vatomInfo) + + do { + // json encode the model + let json = try JSON.init(encodable: response) + completion(json, nil) + } catch { + let bridgeError = BridgeError.viewer("Unable to encode response.") + completion(nil, bridgeError) + } + + }) + + case .failure(let error): + // handle error + let bridgeError = BridgeError.viewer("Unable to fetch current user.") completion(nil, bridgeError) - } - - }) + + } } @@ -329,32 +333,34 @@ class CoreBridgeV1: CoreBridge { /// Fetches the publically available properties of the user specified by the id. private func getPublicUser(forUserID id: String, completion: @escaping Completion) { - BLOCKv.getPublicUser(withID: id) { (user, error) in - - // ensure no error - guard let user = user, error == nil else { + BLOCKv.getPublicUser(withID: id) { result in + + switch result { + case .success(let user): + // encode url + var encodedURL: URL? + if let url = user.properties.avatarURL { + encodedURL = try? BLOCKv.encodeURL(url) + } + // build response + let response = BRUser(id: user.id, + firstName: user.properties.firstName, + lastName: user.properties.lastName, + avatarURL: encodedURL?.absoluteString ?? "") + + do { + // json encode the model + let json = try JSON.init(encodable: response) + completion(json, nil) + } catch { + let bridgeError = BridgeError.viewer("Unable to encode response.") + completion(nil, bridgeError) + } + + case .failure(let error): + // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") completion(nil, bridgeError) - return - } - // encode url - var encodedURL: URL? - if let url = user.properties.avatarURL { - encodedURL = try? BLOCKv.encodeURL(url) - } - // build response - let response = BRUser(id: user.id, - firstName: user.properties.firstName, - lastName: user.properties.lastName, - avatarURL: encodedURL?.absoluteString ?? "") - - do { - // json encode the model - let json = try JSON.init(encodable: response) - completion(json, nil) - } catch { - let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) } } @@ -369,29 +375,31 @@ class CoreBridgeV1: CoreBridge { /// Fetches the avatar URL of the user specified by the id. private func getPublicAvatarURL(forUserID id: String, completion: @escaping Completion) { - BLOCKv.getPublicUser(withID: id) { (user, error) in - - // ensure no error - guard let user = user, error == nil else { + BLOCKv.getPublicUser(withID: id) { result in + + switch result { + case .success(let user): + // encode url + var encodedURL: URL? + if let url = user.properties.avatarURL { + encodedURL = try? BLOCKv.encodeURL(url) + } + // create avatar response + let response = PublicAvatarFormat(id: user.id, avatarURL: encodedURL?.absoluteString ?? "") + + do { + // json encode the model + let json = try JSON.init(encodable: response) + completion(json, nil) + } catch { + let bridgeError = BridgeError.viewer("Unable to encode response.") + completion(nil, bridgeError) + } + + case .failure(let error): + // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") completion(nil, bridgeError) - return - } - // encode url - var encodedURL: URL? - if let url = user.properties.avatarURL { - encodedURL = try? BLOCKv.encodeURL(url) - } - // create avatar response - let response = PublicAvatarFormat(id: user.id, avatarURL: encodedURL?.absoluteString ?? "") - - do { - // json encode the model - let json = try JSON.init(encodable: response) - completion(json, nil) - } catch { - let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) } } @@ -412,16 +420,20 @@ class CoreBridgeV1: CoreBridge { throw BridgeError.viewer("Unable to encode data.") } - BLOCKv.performAction(name: name, payload: dict) { (payload, error) in - // ensure no error - guard let payload = payload, error == nil else { + BLOCKv.performAction(name: name, payload: dict) { result in + + switch result { + case .success(let payload): + // convert to json + let json = try? JSON(payload) + completion(json, nil) + + case .failure(let error): + // handle error let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") completion(nil, bridgeError) - return } - // convert to json - let json = try? JSON(payload) - completion(json, nil) + } } catch { @@ -456,19 +468,23 @@ private extension CoreBridgeV1 { let builder = DiscoverQueryBuilder() builder.setScope(scope: .parentID, value: backingID) - BLOCKv.discover(builder) { (vatoms, error) in - - // ensure no error - guard error == nil else { + BLOCKv.discover(builder) { result in + + switch result { + case .success(let vatoms): + // create a list of the child vatoms and add the backing (parent vatom) + var permittedIDs = vatoms.map { $0.id } + permittedIDs.append(backingID) + + completion(permittedIDs, nil) + case .failure(let error): + // handle error let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) - return } - // create a list of the child vatoms and add the backing (parent vatom) - var permittedIDs = vatoms.map { $0.id } - permittedIDs.append(backingID) - completion(permittedIDs, nil) + + } } @@ -481,18 +497,18 @@ private extension CoreBridgeV1 { /// The method uses the vatom endpoint. Therefore, only public vAtoms are returned (irrespecitve of ownership). private func getVatomsFormatted(withIDs ids: [String], completion: @escaping BFVatomCompletion) { - BLOCKv.getVatoms(withIDs: ids) { (vatoms, error) in - - // ensure no error - guard error == nil else { + BLOCKv.getVatoms(withIDs: ids) { result in + + switch result { + case .success(let vatoms): + // convert vAtom into bridge format + completion(self.formatVatoms(vatoms), nil) + + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") completion([], bridgeError) - return } - // convert vAtom into bridge format - completion(self.formatVatoms(vatoms), nil) - } } @@ -505,16 +521,18 @@ private extension CoreBridgeV1 { let builder = DiscoverQueryBuilder() builder.setScope(scope: .parentID, value: id) - BLOCKv.discover(builder) { (vatoms, error) in - - // ensure no error - guard error == nil else { + BLOCKv.discover(builder) { result in + + switch result { + case .success(let vatoms): + // format vatoms + completion(self.formatVatoms(vatoms), nil) + + case .failure(let error): + // handle error let bridgeError = BridgeError.viewer("Unable to fetch children for vAtom \(id).") completion([], bridgeError) - return } - // format vatoms - completion(self.formatVatoms(vatoms), nil) } diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift index 63a5c4bb..41ef2ff6 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift @@ -281,18 +281,18 @@ class CoreBridgeV2: CoreBridge { private func getVatoms(withIDs ids: [String], completion: @escaping ([String: [VatomModel]]?, BridgeError?) -> Void) { - BLOCKv.getVatoms(withIDs: ids) { (vatoms, error) in - - // ensure no error - guard error == nil else { + BLOCKv.getVatoms(withIDs: ids) { result in + + switch result { + case .success(let vatoms): + let response = ["vatoms": vatoms] + completion(response, nil) + + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) - return } - let response = ["vatoms": vatoms] - completion(response, nil) - } } @@ -312,19 +312,20 @@ class CoreBridgeV2: CoreBridge { let builder = DiscoverQueryBuilder() builder.setScope(scope: .parentID, value: backingID) - BLOCKv.discover(builder) { (vatoms, error) in + BLOCKv.discover(builder) { result in - // ensure no error - guard error == nil else { + switch result { + case .success(let vatoms): + // create a list of the child vatoms and add the backing (parent vatom) + var permittedIDs = vatoms.map { $0.id } + permittedIDs.append(backingID) + completion(permittedIDs, nil) + + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) - return } - // create a list of the child vatoms and add the backing (parent vatom) - var permittedIDs = vatoms.map { $0.id } - permittedIDs.append(backingID) - - completion(permittedIDs, nil) + } } @@ -342,18 +343,17 @@ class CoreBridgeV2: CoreBridge { let builder = DiscoverQueryBuilder() builder.setScope(scope: .parentID, value: id) - BLOCKv.discover(builder) { (vatoms, error) in - - // ensure no error - guard error == nil else { + BLOCKv.discover(builder) { result in + + switch result { + case .success(let vatoms): + let response = ["vatoms": vatoms] + completion(response, nil) + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch children for vAtom \(id).") completion(nil, bridgeError) - return } - let response = ["vatoms": vatoms] - completion(response, nil) - } } @@ -365,22 +365,22 @@ class CoreBridgeV2: CoreBridge { /// - completion: Completion handler to call with JSON data to be passed to the webpage. private func getPublicUser(userID id: String, completion: @escaping (BRUser?, BridgeError?) -> Void) { - BLOCKv.getPublicUser(withID: id) { (user, error) in - - // ensure no error - guard let user = user, error == nil else { + BLOCKv.getPublicUser(withID: id) { result in + + switch result { + case .success(let user): + // build response + let properties = BRUser.Properties(firstName: user.properties.firstName, + lastName: user.properties.lastName, + avatarURI: user.properties.avatarURL?.absoluteString ?? "") + let response = BRUser(id: user.id, properties: properties) + completion(response, nil) + + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") completion(nil, bridgeError) - return } - // build response - let properties = BRUser.Properties(firstName: user.properties.firstName, - lastName: user.properties.lastName, - avatarURI: user.properties.avatarURL?.absoluteString ?? "") - let response = BRUser(id: user.id, properties: properties) - completion(response, nil) - } } @@ -405,16 +405,19 @@ class CoreBridgeV2: CoreBridge { throw BridgeError.viewer("Unable to encode data.") } - BLOCKv.performAction(name: name, payload: dict) { (payload, error) in - // ensure no error - guard let payload = payload, error == nil else { + BLOCKv.performAction(name: name, payload: dict) { result in + + switch result { + case .success(let payload): + // convert to json + let json = try? JSON(payload) + completion(json, nil) + + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") completion(nil, bridgeError) - return } - // convert to json - let json = try? JSON(payload) - completion(json, nil) + } } catch { From 6d1c83a5956f4b70c82928e244a6e809f6db58ae Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 11 Apr 2019 14:28:03 +0200 Subject: [PATCH 051/165] Wrap result completion with promise --- BlockV/Core/Data Pool/Core+Overlays.swift | 27 +++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift index d97c38cd..52a3e900 100644 --- a/BlockV/Core/Data Pool/Core+Overlays.swift +++ b/BlockV/Core/Data Pool/Core+Overlays.swift @@ -18,11 +18,34 @@ import PromiseKit internal extension Client { func request(_ endpoint: Endpoint) -> Promise { - return Promise { self.request(endpoint, completion: $0.resolve) } + + return Promise { seal in + // convert result type into promise + self.request(endpoint) { result in + switch result { + case .success(let model): + seal.fulfill(model) + case .failure(let error): + seal.reject(error) + } + } + } + } func requestJSON(_ endpoint: Endpoint) -> Promise { - return Promise { self.requestJSON(endpoint, completion: $0.resolve) } + + return Promise { seal in + self.requestJSON(endpoint) { result in + switch result { + case .success(let model): + seal.fulfill(model) + case .failure(let error): + seal.reject(error) + } + } + } + } } From 393cac4668cd027992e6bd4568050d629198b5d4 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sat, 13 Apr 2019 14:33:53 +0200 Subject: [PATCH 052/165] Core: Feature - Vatom containment (#192) * List children * Add extension to VatomModel to assist with containment * Add VatomUpdateModel as native representation of patch vatom reponse * Expose method to allow vatom parent id update * Update setParentID to use Data Pool --- .../Data Pool/Helpers/Vatom+Containment.swift | 171 ++++++++++++++++++ .../Models/Package/VatomUpdateModel.swift | 36 ++++ BlockV/Core/Network/Stack/API.swift | 69 ++++--- .../Core/Requests/BLOCKv+VatomRequest.swift | 54 ++++++ 4 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift create mode 100644 BlockV/Core/Network/Models/Package/VatomUpdateModel.swift diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift new file mode 100644 index 00000000..391624b8 --- /dev/null +++ b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift @@ -0,0 +1,171 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +// MARK: - Containment Management + +/// Extension to VatomModel add convenience methods for dealing with the children from the perspective of a +/// container vatom. +extension VatomModel { + + /// Fetches the first-level child vatom of this container vatom. + /// + /// Available on both owned and unowned vatoms. + /// + /// - important: + /// This method will inspect the 'inventory' and 'children' regions, if the regions are synchronized the vatoms + /// are returned, if not, the regions are first synchronized. This means the method is potentially slow. + public func listChildren(completion: @escaping (Result<[VatomModel], BVError>) -> Void) { + + // check if the vatom is in the owner's inventory region (and stabalized) + DataPool.inventory().getStable(id: self.id).map { inventoryVatom -> Guarantee<[VatomModel]> in + + if inventoryVatom == nil { + // inspect the child region (owner & unowned) + return DataPool.children(parentID: self.id) + .getAllStable() + .map { $0 as! [VatomModel] } // swiftlint:disable:this force_cast + + } else { + // filter current children + let children = (DataPool.inventory() + .getAll() + .compactMap { $0 as? VatomModel } + .filter { $0.props.parentID == self.id }) + return Guarantee.value(children) + } + + //FIXME: Very strange double unwrapping - I think it's to do with .map double wrapping? + }.done { body in + body.done({ children in + completion(.success(children)) + }) + } + + } + + /// Fetches the first-level child vatoms for this container vatom. + /// + /// Only available on *owned* container vatoms. Use this function to get a best-effort snapshot of the number of + /// children contained by this vatom. This call is useful where getting the number of children is critical, e.g. + /// face code in a re-use list. + /// + /// - important: + /// This method will inspect the 'inventory' region irrespective of sync state. This means the method is fast. + public func listCachedChildren() -> [VatomModel] { + + // fetch children from inventory region + let children = DataPool.inventory().getAll() + .compactMap { $0 as? VatomModel } + .filter { $0.props.parentID == self.id } + + return children + + } + +} + +// MARK: - Containment + +extension VatomModel { + + /// Returns `true` if the container vatom will accept a request to contain the child vatom, `false` otherwise. + /// + /// Only applicable to *owned* vatoms. + public func canContainChild(_ childVatom: VatomModel) -> Bool { + + // ensure this vatom is a container + if !self.isContainer { return false } + + // folder containers will always accept requests to contain children + if self.rootType == .container(.folder) { return true } + + // defined containers will only accept requests to contain vatoms matching their child policy rules + if self.rootType == .container(.defined) { + return doesChildPolicyAllowContainmentOf(childVatom) + } + + // standard vatoms cannot contain children + // discover and package containers will not accept requests to contain children + return false + + } + + /// Returns `true` if this container vatom's child policy permits containment of the child vatom. `false` otherwise. + /// + /// Only applicable to *owned* vatoms. + private func doesChildPolicyAllowContainmentOf(_ childVatom: VatomModel) -> Bool { + + // check if any policies match this template variation + for policy in self.props.childPolicy where policy.templateVariationID == childVatom.props.templateVariationID { + + // check if there is a maximum number of children + if policy.creationPolicy.enforcePolicyCountMax { + // check if current child count is less then policy max + if policy.creationPolicy.policyCountMax > self.listCachedChildren().count { + return true + } + } else { + return true + } + } + + return false + + } + + /// Returns `true` if the vatom's root type is a 'Container' type. + /// + /// Container vatoms have the ability to have parent-child relationships. + public var isContainer: Bool { + if case RootType.container = self.rootType { + return true + } + return false + } + + /// Enum modeling the root type of this vatom. + public var type: RootType { + + if self.props.rootType == "vAtom::vAtomType" { + return .standard + } else { + if self.props.rootType.hasSuffix("FolderContainerType") { + return .container(.folder) + } else if self.props.rootType.hasSuffix("PackageContainerType") { + return .container(.package) + } else if self.props.rootType.hasSuffix("DiscoverContainerType") { + return .container(.discover) + } else if self.props.rootType.hasSuffix("DefinedFolderContainerType") { + return .container(.defined) + } else { + return .unknown + } + } + + } + + public enum RootType: Equatable { + case standard + case container(ContainerType) + case unknown + } + + public enum ContainerType: Equatable { + case folder + case package + case discover + case defined + } + +} diff --git a/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift b/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift new file mode 100644 index 00000000..f2e22d1c --- /dev/null +++ b/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift @@ -0,0 +1,36 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +public struct VatomUpdateModel: Decodable, Equatable { + + let numberUpdated: Int + let numberErrors: Int + let errorMessage: [String: String] + let ids: [String] + + enum CodingKeys: String, CodingKey { + case numberUpdated = "num_updated" + case numberErrors = "num_errors" + case errorMessage = "error_messages" + case ids + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + numberUpdated = try values.decode(Int.self, forKey: .numberUpdated) + numberErrors = try values.decode(Int.self, forKey: .numberErrors) + errorMessage = try values.decodeIfPresent([String: String].self, forKey: .errorMessage) ?? [:] + ids = try values.decode([String].self, forKey: .ids) + } + +} diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 396c7da2..65ad64af 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -14,12 +14,11 @@ import Alamofire // swiftlint:disable file_length -/// Consolidates all BlockV API endpoints. +/// Consolidates all BLOCKv API endpoints. /// /// Endpoints are namespaced to furture proof. /// /// Endpoints closely match their server counterparts where possible. -/// If appropriate, some endpoints may represent a single server endpoint. /// /// The goal is to abstract endpoint specific details. The networking client should /// not need to taylor requests for the specific of an endpoint. @@ -28,8 +27,9 @@ enum API { } extension API { /* - All Session, Current User, and Public User endpoints are wrapped in a (unnecessary) - container object. This is modelled here using a `BaseModel`. + Notes: + All Session, Current User, and Public User endpoints are wrapped in a container object. This is modelled as + BaseModel. */ // MARK: - @@ -43,7 +43,7 @@ extension API { /// Builds the endpoint for new user registration. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func register(tokens: [RegisterTokenParams], userInfo: UserInfo? = nil) -> Endpoint> { @@ -71,7 +71,7 @@ extension API { /// Builds the endpoint for user login. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func login(tokenParams: LoginTokenParams) -> Endpoint> { return Endpoint(method: .post, path: loginPath, @@ -89,28 +89,28 @@ extension API { /// Builds the endpoint to get the current user's properties. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func get() -> Endpoint> { return Endpoint(path: currentUserPath) } /// Builds the endpoint to get the current user's tokens. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getTokens() -> Endpoint> { return Endpoint(path: currentUserPath + "/tokens") } /// Builds the endpoint to log out the current user. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func logOut() -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/logout") } /// Builds the endpoint to update current user's information. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func update(userInfo: UserInfo) -> Endpoint> { return Endpoint(method: .patch, path: currentUserPath, @@ -120,7 +120,7 @@ extension API { /// Builds the endpoint to verify a token with an OTP code. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func verifyToken(_ token: UserToken, code: String) -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/verify_token", @@ -134,7 +134,7 @@ extension API { /// Builds the endpoint to reset a user token. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func resetToken(_ token: UserToken) -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/reset_token", @@ -144,7 +144,7 @@ extension API { /// Builds the endpoint to send a verification request for a specific token. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func resetTokenVerification(forToken token: UserToken) -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/reset_token_verification", @@ -154,7 +154,7 @@ extension API { /// Builds the endpoint to add a token to the current user. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func addToken(_ token: UserToken, isPrimary: Bool) -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/tokens", @@ -168,7 +168,7 @@ extension API { /// Builds the endpoint to delete a token. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func deleteToken(id: String) -> Endpoint> { return Endpoint(method: .delete, path: currentUserPath + "/tokens/\(id)") @@ -176,7 +176,7 @@ extension API { /// Builds the endpoint to set a default token. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func setDefaultToken(id: String) -> Endpoint> { return Endpoint(method: .put, path: currentUserPath + "/tokens/\(id)/default") @@ -186,7 +186,7 @@ extension API { /// Builds the endpoint for the user's avatar. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func uploadAvatar(_ imageData: Data) -> UploadEndpoint> { let bodyPart = MultiformBodyPart(data: imageData, @@ -205,8 +205,7 @@ extension API { /// - Parameters: /// - message: Content of the message. /// - userID: Unique identifier of the recipient user. - /// - Returns: The endpoint is generic over a response model. This model is parsed on - /// success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func sendMessage(_ message: String, toUserId userId: String) -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/message", @@ -242,7 +241,7 @@ extension API { /// Builds the endpoint to get a public user's details. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func get(id: String) -> Endpoint> { return Endpoint(path: "/v1/users/\(id)") } @@ -263,7 +262,7 @@ extension API { /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's /// perspetive. That is, we specify the id of the parent who's children are to be retunred. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getInventory(parentID: String, page: Int = 0, limit: Int = 0) -> Endpoint> { @@ -279,7 +278,7 @@ extension API { /// Builds the endpoint to get a vAtom by its unique identifier. /// - /// The endpoint is generic over a response model. This model is parsed on success responses (200...299). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getVatoms(withIDs ids: [String]) -> Endpoint> { return Endpoint(method: .post, path: userVatomPath + "/get", @@ -289,7 +288,7 @@ extension API { /// Builds the endpoint to trash a vAtom specified by its id. /// - /// Returns an endpoint over a BaseModel over a GeneralModel. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func trashVatom(_ id: String) -> Endpoint> { return Endpoint(method: .post, path: "/v1/user/vatom/trash", @@ -297,6 +296,16 @@ extension API { } + /// Builds the endpoint to update a vAtom. + /// + /// - Parameter payload: Raw payload. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func updateVatom(payload: [String: Any]) -> Endpoint> { + return Endpoint(method: .patch, + path: "/v1/vatoms", + parameters: payload) + } + } // MARK: - @@ -307,7 +316,7 @@ extension API { /// Builds the endpoint to search for vAtoms. /// /// - Parameter payload: Raw request payload. - /// - Returns: Endpoint generic over `UnpackedModel`. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func discover(_ payload: [String: Any]) -> Endpoint> { return Endpoint(method: .post, @@ -325,7 +334,7 @@ extension API { /// - topRightLat: Top right latitude coordinate. /// - topRightLon: Top right longitude coordinte. /// - filter: The vAtom filter option to apply. - /// - Returns: Endpoint generic over `UnpackedModel`. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func geoDiscover(bottomLeftLat: Double, bottomLeftLon: Double, topRightLat: Double, @@ -367,7 +376,7 @@ extension API { /// - topRightLon: Top right longitude coordinte. /// - precision: The grouping precision applied when computing the groups. /// - filter: The vAtom filter option to apply. - /// - Returns: Endpoint generic over `GeoGroupModel`. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func geoDiscoverGroups(bottomLeftLat: Double, bottomLeftLon: Double, topRightLat: Double, @@ -419,7 +428,7 @@ extension API { /// - Parameters: /// - name: Action name. /// - payload: Raw payload for the action. - /// - Returns: Returns endpoint generic over Void, i.e. caller will receive raw data. + /// - Returns: Constructed endpoint generic over `Void` that may be passed to a request. static func custom(name: String, payload: [String: Any]) -> Endpoint { return Endpoint(method: .post, path: actionPath + "/\(name)", @@ -438,7 +447,7 @@ extension API { /// Builds the endpoint for fetching the actions configured for a template ID. /// /// - Parameter id: Uniquie identifier of the template. - /// - Returns: Endpoint for fectching actions. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getActions(forTemplateID id: String) -> Endpoint> { return Endpoint(method: .get, path: userActionsPath + "/\(id)") @@ -459,7 +468,7 @@ extension API { /// - cursor: Filters out all threads more recent than the cursor (useful for paging). /// If omitted or set as zero, the most recent threads are returned. /// - count: Defines the number of messages to return (after the cursor). - /// - Returns: Endpoint for fetching the thread for the current user. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getThreads(cursor: String, count: Int) -> Endpoint> { let payload: [String: Any] = [ @@ -479,7 +488,7 @@ extension API { /// - cursor: Filters out all message more recent than the cursor (useful for paging). /// If omitted or set as zero, the most recent threads are returned. /// - count: Defines the number of messages to return (after the cursor). - /// - Returns: Endpoint for fetching the messages for a specific thread invoving the current user. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> Endpoint> { diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index b83c382c..51aa9b03 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -129,6 +129,60 @@ extension BLOCKv { } + /// Sets the parent ID of the specified vatom. + /// + /// - Parameters: + /// - vatom: Vatom whose parent ID must be set. + /// - parentID: Unique identifier of the parent vatom. + /// - completion: The completion hanlder to call when the request is completed. + /// This handler is executed on the main thread. + public static func setParentID(ofVatoms vatoms: [VatomModel], to parentID: String, + completion: @escaping (Result) -> Void) { + + // perform preemptive action, store undo functions + let undos = vatoms.map { + // tuple: (vatom id, undo function) + (id: $0.id, undo: DataPool.inventory().preemptiveChange(id: $0.id, + keyPath: "vAtom::vAtomType.parent_id", + value: parentID)) + } + + let ids = vatoms.map { $0.id } + let payload: [String: Any] = [ + "ids": ids, + "parent_id": parentID + ] + + let endpoint = API.UserVatom.updateVatom(payload: payload) + + BLOCKv.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + + /* + # Note + The most likely scenario where there will be partial containment errors is when setting the parent id + to a container vatom of type `DefinedFolderContainerType`. However, as of writting, the server does + not enforce child policy rules so this always succeed (using the current API). + */ + let updateVatomModel = baseModel.payload + // roll back only those failed containments + let undosToRollback = undos.filter { !updateVatomModel.ids.contains($0.id) } + undosToRollback.forEach { $0.undo() } + // complete + completion(.success(updateVatomModel)) + + case .failure(let error): + // roll back all containments + undos.forEach { $0.undo() } + completion(.failure(error)) + } + + } + + } + /// Searches for vAtoms on the BLOCKv platform. /// /// - Parameters: From de4639ebac3954ca99ec0e00de99275a9f531c2e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sat, 13 Apr 2019 14:45:31 +0200 Subject: [PATCH 053/165] Fix naming --- BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift index 391624b8..08db246c 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift @@ -135,7 +135,7 @@ extension VatomModel { } /// Enum modeling the root type of this vatom. - public var type: RootType { + public var rootType: RootType { if self.props.rootType == "vAtom::vAtomType" { return .standard From 064db8c55ed32c349b5848d203f32435e978c034 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 09:26:52 +0200 Subject: [PATCH 054/165] Core: Feature - Expose endpoint generic constructors (#198) * Add promise versions of client requests. * Remove void requests * Add generic endpoint constructors to all the caller to create void endpoints * Move generic functions under a namespace --- BlockV/Core/Data Pool/Client+PromiseKit.swift | 57 +++ BlockV/Core/Data Pool/Core+Overlays.swift | 100 ----- .../Data Pool/Regions/InventoryRegion.swift | 5 +- .../Regions/VatomChildrenRegion.swift | 3 +- .../Data Pool/Regions/VatomIDRegion.swift | 2 +- BlockV/Core/Network/Stack/API.swift | 355 ++++++++++++------ .../Core/Requests/BLOCKv+VatomRequest.swift | 14 +- 7 files changed, 313 insertions(+), 223 deletions(-) create mode 100644 BlockV/Core/Data Pool/Client+PromiseKit.swift delete mode 100644 BlockV/Core/Data Pool/Core+Overlays.swift diff --git a/BlockV/Core/Data Pool/Client+PromiseKit.swift b/BlockV/Core/Data Pool/Client+PromiseKit.swift new file mode 100644 index 00000000..41c43d4e --- /dev/null +++ b/BlockV/Core/Data Pool/Client+PromiseKit.swift @@ -0,0 +1,57 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit + +// PromiseKit Overlays on Client +internal extension Client { + + /// Performs a request on a given endpoint. + /// + /// - Parameter endpoint: Endpoint on which to perform the request. + /// - Returns: A promise that resolves with the response as `Data`. + func request(_ endpoint: Endpoint) -> Promise { + + return Promise { seal in + // convert result type into promise + self.request(endpoint) { result in + switch result { + case .success(let model): + seal.fulfill(model) + case .failure(let error): + seal.reject(error) + } + } + } + + } + + /// Performs a request on a given endpoint. + /// + /// - Parameter endpoint: Endpoint on which to perform the request. + /// - Returns: A promise that resolves with the reponse as JSON. + func requestJSON(_ endpoint: Endpoint) -> Promise { + + return Promise { seal in + self.requestJSON(endpoint) { result in + switch result { + case .success(let model): + seal.fulfill(model) + case .failure(let error): + seal.reject(error) + } + } + } + + } + +} diff --git a/BlockV/Core/Data Pool/Core+Overlays.swift b/BlockV/Core/Data Pool/Core+Overlays.swift deleted file mode 100644 index 52a3e900..00000000 --- a/BlockV/Core/Data Pool/Core+Overlays.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// - -import Foundation -import PromiseKit - -// Core overlays to support Data Pool. - -// PromiseKit Overlays -internal extension Client { - - func request(_ endpoint: Endpoint) -> Promise { - - return Promise { seal in - // convert result type into promise - self.request(endpoint) { result in - switch result { - case .success(let model): - seal.fulfill(model) - case .failure(let error): - seal.reject(error) - } - } - } - - } - - func requestJSON(_ endpoint: Endpoint) -> Promise { - - return Promise { seal in - self.requestJSON(endpoint) { result in - switch result { - case .success(let model): - seal.fulfill(model) - case .failure(let error): - seal.reject(error) - } - } - } - - } - -} - -internal extension API { - - /// Raw endpoints are generic over `Void`. This informs the networking client to return the raw data (instead of - /// parsing out a model). - /// - /// NB: `Void` endpoints don't partake in the auth cycle. - enum Raw { - - /// Builds the endpoint to search for vAtoms. - static func discover(_ payload: [String: Any]) -> Endpoint { - - return Endpoint(method: .post, - path: "/v1/vatom/discover", - parameters: payload) - } - - /// Build the endpoint to fetch the inventory. - static func getInventory(parentID: String, - page: Int = 0, - limit: Int = 0) -> Endpoint { - return Endpoint(method: .post, - path: "/v1/user/vatom/inventory", - parameters: [ - "parent_id": parentID, - "page": page, - "limit": limit - ] - ) - } - - /// Builds the endpoint to get a vAtom by its unique identifier. - static func getVatoms(withIDs ids: [String]) -> Endpoint { - return Endpoint(method: .post, - path: "/v1/user/vatom/get", - parameters: ["ids": ids] - ) - } - - /// Builds the endpoint to update a vatom. - static func updateVatom(payload: [String: Any]) -> Endpoint { - return Endpoint(method: .post, - path: "/v1/vatoms", - parameters: payload) - } - - } - -} diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index bddca686..693f4b36 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -94,7 +94,7 @@ class InventoryRegion: BLOCKvRegion { printBV(info: "[DataPool > InventoryRegion] Loading page \(page), got \(previousItems.count) items so far...") // build raw request - let endpoint = API.Raw.getInventory(parentID: "*", page: page) + let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: page) return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[String]?> in @@ -157,7 +157,8 @@ class InventoryRegion: BLOCKvRegion { // pause this instance's message processing and fetch vatom payload self.pauseMessages() - let endpoint = API.Raw.getVatoms(withIDs: [vatomID]) + // create endpoint over void + let endpoint: Endpoint = API.Generic.getVatoms(withIDs: [vatomID]) BLOCKv.client.request(endpoint).done { data in // convert diff --git a/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift b/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift index 90121d0b..298dbe3b 100644 --- a/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift +++ b/BlockV/Core/Data Pool/Regions/VatomChildrenRegion.swift @@ -81,7 +81,8 @@ class VatomChildrenRegion: BLOCKvRegion { printBV(info: "[DataPool > VatomChildrenRegion] Loading page \(page), got \(previousItems.count) items so far.") - let endpoint = API.Raw.discover(builder.toDictionary()) + // create endpoint over void + let endpoint: Endpoint = API.Generic.discover(builder.toDictionary()) return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[DataObject]> in // extract payload diff --git a/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift b/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift index 0ef2e88d..0450021d 100644 --- a/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift +++ b/BlockV/Core/Data Pool/Regions/VatomIDRegion.swift @@ -60,7 +60,7 @@ class VatomIDRegion: BLOCKvRegion { // pause websocket events self.pauseMessages() - let endpoint = API.Raw.getVatoms(withIDs: ids) + let endpoint: Endpoint = API.Generic.getVatoms(withIDs: ids) return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[String]?> in guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 65ad64af..10b2c86b 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -251,40 +251,29 @@ extension API { // MARK: - /// Consolidates all user vatom endpoints. - enum UserVatom { - - private static let userVatomPath = "/v1/user/vatom" - - //TODO: Parameterise parameters. - - /// Builds the endpoint to get the current user's inventory. + enum Vatom { + + /// Builds an endpoint to get the current user's inventory. /// /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's /// perspetive. That is, we specify the id of the parent who's children are to be retunred. /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. static func getInventory(parentID: String, page: Int = 0, limit: Int = 0) -> Endpoint> { - return Endpoint(method: .post, - path: userVatomPath + "/inventory", - parameters: [ - "parent_id": parentID, - "page": page, - "limit": limit - ] - ) + return API.Generic.getInventory(parentID: parentID, page: page, limit: limit) + } - /// Builds the endpoint to get a vAtom by its unique identifier. + /// Builds an endpoint to get a vAtom by its unique identifier. /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + /// - Parameter ids: Unique identifier of the vatom. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. static func getVatoms(withIDs ids: [String]) -> Endpoint> { - return Endpoint(method: .post, - path: userVatomPath + "/get", - parameters: ["ids": ids] - ) + return API.Generic.getVatoms(withIDs: ids) } + /// Builds the endpoint to trash a vAtom specified by its id. /// @@ -295,36 +284,205 @@ extension API { parameters: ["this.id": id]) } - - /// Builds the endpoint to update a vAtom. + + /// Builds an endpoint to update a vAtom. /// /// - Parameter payload: Raw payload. - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + /// - Returns: Constructed endpoint specialized to parse out a `VatomUpdateModel`. static func updateVatom(payload: [String: Any]) -> Endpoint> { - return Endpoint(method: .patch, - path: "/v1/vatoms", - parameters: payload) + return API.Generic.updateVatom(payload: payload) + } + + /// Builds an endpoint to search for vAtoms. + /// + /// - Parameter payload: Raw request payload. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func discover(_ payload: [String: Any]) -> Endpoint> { + return API.Generic.discover(payload) + } + + /// Builds an endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). + /// + /// Use this endpoint to fetch a collection of vAtoms. + /// + /// - Parameters: + /// - bottomLeftLat: Bottom left latitude coordinate. + /// - bottomLeftLon: Bottom left longitude coordinate. + /// - topRightLat: Top right latitude coordinate. + /// - topRightLon: Top right longitude coordinte. + /// - filter: The vAtom filter option to apply. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func geoDiscover(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + filter: String) -> Endpoint> { + + return API.Generic.geoDiscover(bottomLeftLat: bottomLeftLat, + bottomLeftLon: bottomLeftLon, + topRightLat: topRightLat, + topRightLon: topRightLon, + filter: filter) + } + + /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). + /// + /// Use this endpoint to fetch an collection of groups/annotation indicating the count + /// of vAtoms at a particular location. + /// + /// - Parameters: + /// - bottomLeftLat: Bottom left latitude coordinate. + /// - bottomLeftLon: Bottom left longitude coordinate. + /// - topRightLat: Top right latitude coordinate. + /// - topRightLon: Top right longitude coordinte. + /// - precision: The grouping precision applied when computing the groups. + /// - filter: The vAtom filter option to apply. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func geoDiscoverGroups(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + precision: Int, + filter: String) -> Endpoint> { + + return API.Generic.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, + bottomLeftLon: bottomLeftLon, + topRightLat: topRightLat, + topRightLon: topRightLon, + precision: precision, + filter: filter) + } } // MARK: - - /// Consolidtaes all discover endpoints. - enum VatomDiscover { + /// Consolidates all action endpoints. + enum VatomAction { - /// Builds the endpoint to search for vAtoms. + /* + Each action's reactor returns it's own json payload. This does not need to be mapped as yet. + */ + + /// Builds the endpoint to perform and action on a vAtom. /// - /// - Parameter payload: Raw request payload. + /// - Parameters: + /// - name: Action name. + /// - payload: Raw payload for the action. + /// - Returns: Constructed endpoint generic over `Void` that may be passed to a request. + static func custom(name: String, payload: [String: Any]) -> Endpoint { + return API.Generic.perform(name: name, payload: payload) + } + + } + + // MARK: - + + /// Consolidates all the user actions. + enum UserActions { + + /// Builds the endpoint for fetching the actions configured for a template ID. + /// + /// - Parameter id: Uniquie identifier of the template. /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func discover(_ payload: [String: Any]) -> Endpoint> { + static func getActions(forTemplateID id: String) -> Endpoint> { + return API.Generic.getActions(forTemplateID: id) + } + + } + + // MARK: - + + /// Consolidtes all the user activity endpoints. + enum UserActivity { + + /// Builds the endpoint for fetching the threads involving the current user. + /// + /// - Parameters: + /// - cursor: Filters out all threads more recent than the cursor (useful for paging). + /// If omitted or set as zero, the most recent threads are returned. + /// - count: Defines the number of messages to return (after the cursor). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getThreads(cursor: String, count: Int) -> Endpoint> { + return API.Generic.getThreads(cursor: cursor, count: count) + } + /// Builds the endpoint for fetching the message for a specified thread involving the current user. + /// + /// - Parameters: + /// - id: Unique identifier of the thread (a.k.a thread `name`). + /// - cursor: Filters out all message more recent than the cursor (useful for paging). + /// If omitted or set as zero, the most recent threads are returned. + /// - count: Defines the number of messages to return (after the cursor). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> + Endpoint> { + return API.Generic.getMessages(forThreadId: threadId, cursor: cursor, count: count) + } + + } + +} + +extension API { + + enum Generic { + + private static let userVatomPath = "/v1/user/vatom" + private static let userActionsPath = "/v1/user/actions" + private static let actionPath = "/v1/user/vatom/action" + private static let userActivityPath = "/v1/activity" + + // MARK: Vatoms + + /// Builds the generic endpoint to get the current user's inventory. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getInventory(parentID: String, page: Int = 0, limit: Int = 0) -> Endpoint { + return Endpoint(method: .post, + path: userVatomPath + "/inventory", + parameters: [ + "parent_id": parentID, + "page": page, + "limit": limit + ] + ) + } + + /// Builds a generic endpoint to get a vAtom by its unique identifier. + /// + /// - Parameter ids: Unique identifier of the vatom. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getVatoms(withIDs ids: [String]) -> Endpoint { + return Endpoint(method: .post, + path: userVatomPath + "/get", + parameters: ["ids": ids] + ) + } + + /// Builds a generic endpoint to update a vAtom. + /// + /// - Parameter payload: Raw payload. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func updateVatom(payload: [String: Any]) -> Endpoint { + return Endpoint(method: .patch, + path: "/v1/vatoms", + parameters: payload) + } + + /// Builds a generic endpoint to search for vAtoms. + /// + /// - Parameter payload: Raw request payload. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func discover(_ payload: [String: Any]) -> Endpoint { + return Endpoint(method: .post, path: "/v1/vatom/discover", parameters: payload) } - - /// Builds the endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). + + /// Builds a generic endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). /// /// Use this endpoint to fetch a collection of vAtoms. /// @@ -335,12 +493,12 @@ extension API { /// - topRightLon: Top right longitude coordinte. /// - filter: The vAtom filter option to apply. /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func geoDiscover(bottomLeftLat: Double, - bottomLeftLon: Double, - topRightLat: Double, - topRightLon: Double, - filter: String) -> Endpoint> { - + static func geoDiscover(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + filter: String) -> Endpoint { + // create the payload let payload: [String: Any] = [ @@ -356,14 +514,14 @@ extension API { ], "filter": filter ] - + // create the endpoint return Endpoint(method: .post, path: "/v1/vatom/geodiscover", parameters: payload) - + } - + /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). /// /// Use this endpoint to fetch an collection of groups/annotation indicating the count @@ -377,15 +535,15 @@ extension API { /// - precision: The grouping precision applied when computing the groups. /// - filter: The vAtom filter option to apply. /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func geoDiscoverGroups(bottomLeftLat: Double, - bottomLeftLon: Double, - topRightLat: Double, - topRightLon: Double, - precision: Int, - filter: String) -> Endpoint> { - + static func geoDiscoverGroups(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + precision: Int, + filter: String) -> Endpoint { + assert(1...12 ~= precision, "You must specify a value in the open range [1...12].") - + // create the payload let payload: [String: Any] = [ @@ -402,66 +560,40 @@ extension API { "precision": precision, "filter": filter ] - + // create the endpoint return Endpoint(method: .post, path: "/v1/vatom/geodiscovergroups", parameters: payload) - + } - - } - - // MARK: - - - /// Consolidates all action endpoints. - enum VatomAction { - - private static let actionPath = "/v1/user/vatom/action" - - /* - Each action's reactor returns it's own json payload. This does not need to be mapped as yet. - */ - + + // MARK: - Perform Actions + /// Builds the endpoint to perform and action on a vAtom. /// /// - Parameters: /// - name: Action name. /// - payload: Raw payload for the action. - /// - Returns: Constructed endpoint generic over `Void` that may be passed to a request. - static func custom(name: String, payload: [String: Any]) -> Endpoint { + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func perform(name: String, payload: [String: Any]) -> Endpoint { return Endpoint(method: .post, - path: actionPath + "/\(name)", - parameters: payload) + path: actionPath + "/\(name)", parameters: payload) } - - } - - // MARK: - - - /// Consolidates all the user actions. - enum UserActions { - - private static let userActionsPath = "/v1/user/actions" - + + // MARK: Fetch Actions + /// Builds the endpoint for fetching the actions configured for a template ID. /// /// - Parameter id: Uniquie identifier of the template. /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getActions(forTemplateID id: String) -> Endpoint> { + static func getActions(forTemplateID id: String) -> Endpoint { return Endpoint(method: .get, path: userActionsPath + "/\(id)") } - - } - - // MARK: - - - /// Consolidtes all the user activity endpoints. - enum UserActivity { - - private static let userActivityPath = "/v1/activity" - + + // MARK: - User Activity + /// Builds the endpoint for fetching the threads involving the current user. /// /// - Parameters: @@ -469,18 +601,18 @@ extension API { /// If omitted or set as zero, the most recent threads are returned. /// - count: Defines the number of messages to return (after the cursor). /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getThreads(cursor: String, count: Int) -> Endpoint> { - + static func getThreads(cursor: String, count: Int) -> Endpoint { + let payload: [String: Any] = [ "cursor": cursor, "count": count ] - + return Endpoint(method: .post, path: userActivityPath + "/mythreads", parameters: payload) } - + /// Builds the endpoint for fetching the message for a specified thread involving the current user. /// /// - Parameters: @@ -489,21 +621,20 @@ extension API { /// If omitted or set as zero, the most recent threads are returned. /// - count: Defines the number of messages to return (after the cursor). /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> - Endpoint> { - - let payload: [String: Any] = [ - "name": threadId, - "cursor": cursor, - "count": count - ] - - return Endpoint(method: .post, - path: userActivityPath + "/mythreadmessages", - parameters: payload) - + static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> Endpoint { + + let payload: [String: Any] = [ + "name": threadId, + "cursor": cursor, + "count": count + ] + + return Endpoint(method: .post, + path: userActivityPath + "/mythreadmessages", + parameters: payload) + } - + } - + } diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index 51aa9b03..61f37363 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -38,7 +38,7 @@ extension BLOCKv { limit: Int = 0, completion: @escaping (Result<[VatomModel], BVError>) -> Void) { - let endpoint = API.UserVatom.getInventory(parentID: id, page: page, limit: limit) + let endpoint = API.Vatom.getInventory(parentID: id, page: page, limit: limit) self.client.request(endpoint) { result in @@ -75,7 +75,7 @@ extension BLOCKv { public static func getVatoms(withIDs ids: [String], completion: @escaping (Result<[VatomModel], BVError>) -> Void) { - let endpoint = API.UserVatom.getVatoms(withIDs: ids) + let endpoint = API.Vatom.getVatoms(withIDs: ids) self.client.request(endpoint) { result in @@ -108,7 +108,7 @@ extension BLOCKv { /// This handler is executed on the main queue. public static func trashVatom(_ id: String, completion: @escaping (BVError?) -> Void) { - let endpoint = API.UserVatom.trashVatom(id) + let endpoint = API.Vatom.trashVatom(id) self.client.request(endpoint) { result in @@ -153,7 +153,7 @@ extension BLOCKv { "parent_id": parentID ] - let endpoint = API.UserVatom.updateVatom(payload: payload) + let endpoint = API.Vatom.updateVatom(payload: payload) BLOCKv.client.request(endpoint) { result in @@ -234,7 +234,7 @@ extension BLOCKv { public static func discover(payload: [String: Any], completion: @escaping (Result) -> Void) { - let endpoint = API.VatomDiscover.discover(payload) + let endpoint = API.Vatom.discover(payload) self.client.request(endpoint) { result in @@ -282,7 +282,7 @@ extension BLOCKv { filter: VatomGeoFilter = .vatoms, completion: @escaping (Result<[VatomModel], BVError>) -> Void) { - let endpoint = API.VatomDiscover.geoDiscover(bottomLeftLat: bottomLeftLat, + let endpoint = API.Vatom.geoDiscover(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, topRightLat: topRightLat, topRightLon: topRightLon, @@ -328,7 +328,7 @@ extension BLOCKv { filter: VatomGeoFilter = .vatoms, completion: @escaping (Result) -> Void) { - let endpoint = API.VatomDiscover.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, + let endpoint = API.Vatom.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, topRightLat: topRightLat, topRightLon: topRightLon, From 867ea280c4caae44d039142b217b3042b3bcc153 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 09:32:31 +0200 Subject: [PATCH 055/165] Core: Feature - Native bridge 2.1 (#199) * Forward child updates * Lazily instantiate core bridge * Update protocol to include sendVatomChildren and faceView * Conform to CoreCridge * Add support for Bridge Protocol 2.1 --- .../Web/Face Bridge/CoreBridge.swift | 11 +- .../Web/Face Bridge/CoreBridgeV1.swift | 55 +++-- .../Web/Face Bridge/CoreBridgeV2.swift | 210 +++++++++++++++--- BlockV/Face/Face Views/Web/WebFaceView.swift | 5 +- .../Web/WebScriptMessageHandler.swift | 15 +- 5 files changed, 229 insertions(+), 67 deletions(-) diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift index 148d90f8..765be722 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift @@ -36,11 +36,20 @@ protocol CoreBridge { /// Returns `true` if the bridge is capable of processing the message and `false` otherwise. func canProcessMessage(_ message: String) -> Bool - /// Send a formatted vAtom to the Bridge SDK. + /// Sends a Vatom over the native bridge. /// /// This is to specialized for a protocol. Surely something more generic exists. func sendVatom(_ vatom: VatomModel) + /// Sends the vatoms over the native bridge informing the web face of changes to the backing vatom's first-level + /// children. + func sendVatomChildren(_ vatoms: [VatomModel]) + + /// Reference to the face view which this bridge is interacting with. + /// + /// Declare as `weak` to avoid retain cycle. + var faceView: WebFaceView? { get } + } // MARK: - Errors diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift index 81ddef9d..56018f4c 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift @@ -17,6 +17,11 @@ import GenericJSON /// Bridges into the Core module. class CoreBridgeV1: CoreBridge { + // MARK: - Native Bridge Requests + + /// Sends the current vAtom to the Bridge SDK. + /// + /// Called on state update. func sendVatom(_ vatom: VatomModel) { guard @@ -39,6 +44,10 @@ class CoreBridgeV1: CoreBridge { } + func sendVatomChildren(_ vatoms: [VatomModel]) { + // V1.0.0 - Unsupported message. + } + // MARK: - Enums /// Represents the contract for the Web bridge (version 1). @@ -214,7 +223,7 @@ class CoreBridgeV1: CoreBridge { // async fetch current user BLOCKv.getCurrentUser { [weak self] result in - + switch result { case .success(let user): // model is available @@ -228,10 +237,10 @@ class CoreBridgeV1: CoreBridge { firstName: user.firstName, lastName: user.lastName, avatarURL: encodedURL?.absoluteString ?? "") - + // fetch backing vAtom self?.getVatomsFormatted(withIDs: [faceView.vatom.id], completion: { (vatoms, error) in - + // ensure no error guard error == nil else { let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") @@ -251,7 +260,7 @@ class CoreBridgeV1: CoreBridge { let response = BRSetup(viewMode: viewMode, user: userInfo, vatomInfo: vatomInfo) - + do { // json encode the model let json = try JSON.init(encodable: response) @@ -260,14 +269,14 @@ class CoreBridgeV1: CoreBridge { let bridgeError = BridgeError.viewer("Unable to encode response.") completion(nil, bridgeError) } - + }) - + case .failure(let error): // handle error let bridgeError = BridgeError.viewer("Unable to fetch current user.") completion(nil, bridgeError) - + } } @@ -334,7 +343,7 @@ class CoreBridgeV1: CoreBridge { private func getPublicUser(forUserID id: String, completion: @escaping Completion) { BLOCKv.getPublicUser(withID: id) { result in - + switch result { case .success(let user): // encode url @@ -347,7 +356,7 @@ class CoreBridgeV1: CoreBridge { firstName: user.properties.firstName, lastName: user.properties.lastName, avatarURL: encodedURL?.absoluteString ?? "") - + do { // json encode the model let json = try JSON.init(encodable: response) @@ -356,7 +365,7 @@ class CoreBridgeV1: CoreBridge { let bridgeError = BridgeError.viewer("Unable to encode response.") completion(nil, bridgeError) } - + case .failure(let error): // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") @@ -376,7 +385,7 @@ class CoreBridgeV1: CoreBridge { private func getPublicAvatarURL(forUserID id: String, completion: @escaping Completion) { BLOCKv.getPublicUser(withID: id) { result in - + switch result { case .success(let user): // encode url @@ -386,7 +395,7 @@ class CoreBridgeV1: CoreBridge { } // create avatar response let response = PublicAvatarFormat(id: user.id, avatarURL: encodedURL?.absoluteString ?? "") - + do { // json encode the model let json = try JSON.init(encodable: response) @@ -395,7 +404,7 @@ class CoreBridgeV1: CoreBridge { let bridgeError = BridgeError.viewer("Unable to encode response.") completion(nil, bridgeError) } - + case .failure(let error): // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") @@ -421,19 +430,19 @@ class CoreBridgeV1: CoreBridge { } BLOCKv.performAction(name: name, payload: dict) { result in - + switch result { case .success(let payload): // convert to json let json = try? JSON(payload) completion(json, nil) - + case .failure(let error): // handle error let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") completion(nil, bridgeError) } - + } } catch { @@ -469,13 +478,13 @@ private extension CoreBridgeV1 { builder.setScope(scope: .parentID, value: backingID) BLOCKv.discover(builder) { result in - + switch result { case .success(let vatoms): // create a list of the child vatoms and add the backing (parent vatom) var permittedIDs = vatoms.map { $0.id } permittedIDs.append(backingID) - + completion(permittedIDs, nil) case .failure(let error): // handle error @@ -483,8 +492,6 @@ private extension CoreBridgeV1 { completion(nil, bridgeError) } - - } } @@ -498,12 +505,12 @@ private extension CoreBridgeV1 { private func getVatomsFormatted(withIDs ids: [String], completion: @escaping BFVatomCompletion) { BLOCKv.getVatoms(withIDs: ids) { result in - + switch result { case .success(let vatoms): // convert vAtom into bridge format completion(self.formatVatoms(vatoms), nil) - + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") completion([], bridgeError) @@ -522,12 +529,12 @@ private extension CoreBridgeV1 { builder.setScope(scope: .parentID, value: id) BLOCKv.discover(builder) { result in - + switch result { case .success(let vatoms): // format vatoms completion(self.formatVatoms(vatoms), nil) - + case .failure(let error): // handle error let bridgeError = BridgeError.viewer("Unable to fetch children for vAtom \(id).") diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift index 41ef2ff6..6444d456 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift @@ -17,6 +17,42 @@ import GenericJSON /// Bridges into the Core module. class CoreBridgeV2: CoreBridge { + // MARK: - Enums + + /// Represents Web Face Request the contract. + enum MessageName: String { + // 2.0 + case initialize = "core.init" + case getUser = "core.user.get" + case getVatomChildren = "core.vatom.children.get" + case getVatom = "core.vatom.get" + case performAction = "core.action.perform" + case encodeResource = "core.resource.encode" + // 2.1 + case setVatomParent = "core.vatom.parent.set" + case observeVatomChildren = "core.vatom.children.observe" + } + + /// Represents the Native Bridge Request contract. + enum NativeBridgeMessageName: String { + // 2.0 + case vatomUpdate = "core.vatom.update" + // 2.1 + case vatomChildrenUpdate = "core.vatom.children.update" + } + + /// Reference to the face view which this bridge is interacting with. + weak var faceView: WebFaceView? + + // MARK: - Initializer + + required init(faceView: WebFaceView) { + self.faceView = faceView + } + + /// Sends the specified vAtom to the Web Face SDK. + /// + /// Called on state update. func sendVatom(_ vatom: VatomModel) { guard let jsonVatom = try? JSON(encodable: vatom) else { @@ -26,8 +62,8 @@ class CoreBridgeV2: CoreBridge { let payload: [String: JSON] = ["vatom": jsonVatom] let message = RequestScriptMessage(source: "ios-vatoms", - name: "core.vatom.update", - requestID: "req_1", + name: NativeBridgeMessageName.vatomUpdate.rawValue, + requestID: "req_x", version: "2.0.0", payload: payload) @@ -36,27 +72,41 @@ class CoreBridgeV2: CoreBridge { } - // MARK: - Enums + /// List of vatom ids which have requested child observation. + var childObservationVatomIds: Set = [] - /// Represents the contract for the Web bridge (version 2). - enum MessageName: String { - case initialize = "core.init" - case getUser = "core.user.get" - case getVatomChildren = "core.vatom.children.get" - case getVatom = "core.vatom.get" - case performAction = "core.action.perform" - case encodeResource = "core.resource.encode" - } + /// Sends the specified vAtoms to the Web Face SDK. + func sendVatomChildren(_ vatoms: [VatomModel]) { - var faceView: WebFaceView? + // ensure observation has been requested + guard let backingId = self.faceView?.vatom.id, + childObservationVatomIds.contains(backingId) else { + return + } + // encode vatoms + guard let jsonVatoms = try? JSON(encodable: vatoms) else { + printBV(error: "Unable to pass vatom update over bridge.") + return + } - // MARK: - Initializer + // create payload + let payload: [String: JSON] = [ + "id": JSON.string(backingId), + "vatoms": jsonVatoms + ] + // create message + let message = RequestScriptMessage(source: "ios-vatoms", + name: NativeBridgeMessageName.vatomChildrenUpdate.rawValue, + requestID: "req_x", + version: "2.1.0", + payload: payload) + + // fire and forget + self.faceView?.sendRequestMessage(message, completion: nil) - required init(faceView: WebFaceView) { - self.faceView = faceView } - // MARK: - Face Brige + // MARK: - Web Face Requests /// Returns `true` if the bridge is capable of processing the message and `false` otherwise. func canProcessMessage(_ message: String) -> Bool { @@ -196,9 +246,9 @@ class CoreBridgeV2: CoreBridge { // extract urls guard let urlStrings = scriptMessage.payload["urls"]?.arrayValue?.map({ $0.stringValue }) else { - let error = BridgeError.caller("Missing 'urls' key.") - completion(nil, error) - return + let error = BridgeError.caller("Missing 'urls' key.") + completion(nil, error) + return } // ensure all urls are strings let flatURLStrings = urlStrings.compactMap { $0 } @@ -218,8 +268,63 @@ class CoreBridgeV2: CoreBridge { completion(nil, error) } - } + case .setVatomParent: + // ensure caller supplied params + guard + let childVatomId = scriptMessage.payload["id"]?.stringValue, + let parentId = scriptMessage.payload["parent_id"]?.stringValue else { + let error = BridgeError.caller("Missing 'id' or 'parent_id' key.") + completion(nil, error) + return + } + + // security check - backing vatom or first-level children + self.permittedVatomIDs { (permittedIDs, error) in + // ensure no error + guard error == nil, let permittedIDs = permittedIDs else { + let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") + completion(nil, bridgeError) + return + } + // security check + if permittedIDs.contains(childVatomId) { + // set parent + self.setParentId(on: childVatomId, parentId: parentId, completion: completion) + } else { + let bridgeError = BridgeError.viewer("This method is only permitted on the backing vatom or one of its children.") + completion(nil, bridgeError) + } + + } + + case .observeVatomChildren: + // ensure caller supplied params + guard let vatomId = scriptMessage.payload["id"]?.stringValue else { + let error = BridgeError.caller("Missing 'id' key.") + completion(nil, error) + return + } + // security check - backing vatom + guard vatomId == self.faceView?.vatom.id else { + let error = BridgeError.caller("This method is only permitted on the backing vatom.") + completion(nil, error) + return + } + // update observer list (this informs the native bridge to forward child updates to the WFSDK) + childObservationVatomIds.insert(vatomId) + // find current children + self.discoverChildren(forVatomID: vatomId, completion: { (payload, error) in + // json dance + if let payload = payload?["vatoms"] { + let payload = try? JSON.init(encodable: ["vatoms": payload]) + completion(payload, error) + return + } + completion(nil, error) + }) + + } } // MARK: - Bridge Responses @@ -254,7 +359,7 @@ class CoreBridgeV2: CoreBridge { /// /// Creates the bridge initializtion JSON data. /// - /// - Parameter completion: Completion handler to call with JSON data to be passed to the webpage. + /// - Parameter completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func setupBridge(_ completion: @escaping (BRSetup?, BridgeError?) -> Void) { // santiy check @@ -277,17 +382,17 @@ class CoreBridgeV2: CoreBridge { /// /// - Parameters: /// - ids: Unique identifier of the vAtom. - /// - completion: Completion handler to call with JSON data to be passed to the webpage. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func getVatoms(withIDs ids: [String], completion: @escaping ([String: [VatomModel]]?, BridgeError?) -> Void) { BLOCKv.getVatoms(withIDs: ids) { result in - + switch result { case .success(let vatoms): let response = ["vatoms": vatoms] completion(response, nil) - + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) @@ -320,12 +425,12 @@ class CoreBridgeV2: CoreBridge { var permittedIDs = vatoms.map { $0.id } permittedIDs.append(backingID) completion(permittedIDs, nil) - + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) } - + } } @@ -336,7 +441,7 @@ class CoreBridgeV2: CoreBridge { /// /// - Parameters: /// - id: Unique identifier of the vAtom. - /// - completion: Completion handler to call with JSON data to be passed to the webpage. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func discoverChildren(forVatomID id: String, completion: @escaping ([String: [VatomModel]]?, BridgeError?) -> Void) { @@ -344,7 +449,7 @@ class CoreBridgeV2: CoreBridge { builder.setScope(scope: .parentID, value: id) BLOCKv.discover(builder) { result in - + switch result { case .success(let vatoms): let response = ["vatoms": vatoms] @@ -362,11 +467,11 @@ class CoreBridgeV2: CoreBridge { /// /// - Parameters: /// - id: Unique identifier of the user. - /// - completion: Completion handler to call with JSON data to be passed to the webpage. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func getPublicUser(userID id: String, completion: @escaping (BRUser?, BridgeError?) -> Void) { BLOCKv.getPublicUser(withID: id) { result in - + switch result { case .success(let user): // build response @@ -375,7 +480,7 @@ class CoreBridgeV2: CoreBridge { avatarURI: user.properties.avatarURL?.absoluteString ?? "") let response = BRUser(id: user.id, properties: properties) completion(response, nil) - + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") completion(nil, bridgeError) @@ -390,7 +495,7 @@ class CoreBridgeV2: CoreBridge { /// - Parameters: /// - name: Name of the action. /// - payload: Payload to send to the server. - /// - completion: Completion handler to call with JSON data to be passed to the webpage. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func performAction(name: String, payload: [String: JSON], completion: @escaping (JSON?, BridgeError?) -> Void) { @@ -406,13 +511,13 @@ class CoreBridgeV2: CoreBridge { } BLOCKv.performAction(name: name, payload: dict) { result in - + switch result { case .success(let payload): // convert to json let json = try? JSON(payload) completion(json, nil) - + case .failure(let error): let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") completion(nil, bridgeError) @@ -437,7 +542,7 @@ class CoreBridgeV2: CoreBridge { /// /// - Parameters: /// - urlStrings: Array of URL strings to be encoded (if possible). - /// - completion: Completion handler to call with JSON data to be passed to the webpage. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func encodeResources(_ urlStrings: [String], completion: @escaping ([String]?, BridgeError?) -> Void) { // convert to URL type @@ -456,4 +561,37 @@ class CoreBridgeV2: CoreBridge { completion(responseURLs, nil) } + // MARK: - 2.1 + + /// Sets the parent id on the specified vatom. + /// + /// - Parameters: + /// - vatomId: Identifier of the vatom whose parent id is to be set. + /// - parentId: Identifier of the parent vatom. + /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. + private func setParentId(on vatomId: String, parentId: String, completion: @escaping Completion) { + + // fetch from data pool + guard let vatom = DataPool.inventory().get(id: vatomId) as? VatomModel else { + let message = "Unable to set parent Id: \(parentId). Data Pool inventory lookup failed." + let bridgeError = BridgeError.viewer(message) + completion(nil, bridgeError) + return + } + + // update parent id + BLOCKv.setParentID(ofVatoms: [vatom], to: parentId) { result in + switch result { + case .success(let x): + let response: JSON = ["new_parent_id": JSON.string(parentId)] + completion(response, nil) + + case .failure(let error): + let bridgeError = BridgeError.viewer("Unable to set parent Id: \(parentId). \(error.localizedDescription)") + completion(nil, bridgeError) + } + } + + } + } diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index 599d26f2..5abf4f94 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -70,7 +70,6 @@ class WebFaceView: FaceView { super.init(vatom: vatom, faceModel: faceModel) self.addSubview(webView) - self.webView.backgroundColor = UIColor.red.withAlphaComponent(0.3) } required public init?(coder aDecoder: NSCoder) { @@ -99,6 +98,9 @@ class WebFaceView: FaceView { return } self.coreBridge?.sendVatom(vatom) + // fetch first-level children + let children = self.vatom.listCachedChildren() + self.coreBridge?.sendVatomChildren(children) } func unload() { @@ -124,7 +126,6 @@ class WebFaceView: FaceView { extension WebFaceView: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - print(#function) self.completion?(nil) } diff --git a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift index 8dc93c5a..ff5f64d3 100644 --- a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift +++ b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift @@ -57,7 +57,7 @@ extension WebFaceView: WKScriptMessageHandler { script += jsonString } script += ");" - printBV(info: "Posting script for evaluation:\n\(script)") +// printBV(info: "Posting script for evaluation:\n\(script)") // return to main queue DispatchQueue.main.async { @@ -185,13 +185,20 @@ extension WebFaceView { // create bridge switch message.version { - case "1.0.0": // original Face SDK - self.coreBridge = CoreBridgeV1(faceView: self) + case "1.0.0": + // lazily create bridge on first web face request (core.init) + if self.coreBridge == nil { + self.coreBridge = CoreBridgeV1(faceView: self) + } // transform V1 to V2 message = self.transformScriptMessage(message) case "2.0.0": - self.coreBridge = CoreBridgeV2(faceView: self) + // lazily create bridge on first web face request (core.init) + if self.coreBridge == nil { + self.coreBridge = CoreBridgeV2(faceView: self) + } + default: throw BridgeError.caller("Unsupported Bridge version: \(message.version)") } From b7dc5dd68596c4c461cda9d6d74cd92cb91368d4 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 09:33:08 +0200 Subject: [PATCH 056/165] Sample: Fix - Result type (#197) * Remove actions * Convert to Result type --- Example/BlockV.xcodeproj/project.pbxproj | 5 +- .../ActionListTableViewController.swift | 30 +++---- .../InventoryCollectionViewController.swift | 60 +++++++------- .../Inventory/TappedVatomViewController.swift | 39 ++++----- .../VatomDetailTableViewController.swift | 37 ++++----- .../Onboarding/LoginViewController.swift | 41 +++++----- .../RegisterTableViewController.swift | 19 +++-- .../VerifyTableViewController.swift | 78 +++++++++--------- .../PasswordTableViewController.swift | 28 +++---- .../User Profile/ProfileViewController.swift | 31 +++---- .../UserInfoTableViewController.swift | 31 ++++--- .../Extensions/VatomModel+Actions.swift | 80 ------------------- Example/BlockV/Views/LiveVatomView.swift | 14 ++-- 13 files changed, 205 insertions(+), 288 deletions(-) delete mode 100644 Example/BlockV/Extensions/VatomModel+Actions.swift diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 3acdc253..ce2fae81 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */; }; AD36A5B0215E3578009EFD55 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = AD36A5AF215E3578009EFD55 /* CHANGELOG.md */; }; AD3B591D224CAF0600257EF3 /* OutboundActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */; }; - AD3B591F224CBAA000257EF3 /* VatomModel+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */; }; AD3DB5302157FD7200E00B67 /* TappedVatomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */; }; AD61462E213DC88700204E5B /* FaceSelectionProcedure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */; }; AD61462F213DC88B00204E5B /* MockModelFaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD61462C213DC87100204E5B /* MockModelFaces.swift */; }; @@ -92,7 +91,6 @@ AD36A59E215D1CE4009EFD55 /* CustomLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLoaderView.swift; sourceTree = ""; }; AD36A5AF215E3578009EFD55 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; AD3B591C224CAF0600257EF3 /* OutboundActionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundActionViewController.swift; sourceTree = ""; }; - AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VatomModel+Actions.swift"; sourceTree = ""; }; AD3DB52F2157FD7200E00B67 /* TappedVatomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TappedVatomViewController.swift; sourceTree = ""; }; AD61462A213DC83D00204E5B /* FaceSelectionProcedure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceSelectionProcedure_Tests.swift; sourceTree = ""; }; AD61462C213DC87100204E5B /* MockModelFaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModelFaces.swift; sourceTree = ""; }; @@ -396,7 +394,6 @@ D55B21F42056745700B6D5C2 /* UIViewController+Ext.swift */, D55B21F82056DE6D00B6D5C2 /* UIColor+Ext.swift */, D5475763205D124100E6FE90 /* UIView+Ext.swift */, - AD3B591E224CBAA000257EF3 /* VatomModel+Actions.swift */, ); path = Extensions; sourceTree = ""; @@ -472,6 +469,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -626,7 +624,6 @@ D55B21EA2052E38000B6D5C2 /* RoundedImageView.swift in Sources */, D5728D5E206155600041F4F7 /* ActionListTableViewController.swift in Sources */, AD36A59F215D1CE4009EFD55 /* CustomLoaderView.swift in Sources */, - AD3B591F224CBAA000257EF3 /* VatomModel+Actions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift index a04aa038..3af7eefd 100644 --- a/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift +++ b/Example/BlockV/Controllers/Actions/ActionListTableViewController.swift @@ -80,25 +80,25 @@ class ActionListTableViewController: UITableViewController { let templateID = self.vatom.props.templateID - BLOCKv.getActions(forTemplateID: templateID) { (actions, error) in + BLOCKv.getActions(forTemplateID: templateID) { result in self.hideNavBarActivityRight() - // unwrap actions, handle error - guard let actions = actions, error == nil else { - print(error!.localizedDescription) - return - } - - // success - print("Actions: \(actions.debugDescription)") - // update data source - self.availableActions = actions.map { action -> AvailableAction in - // record - let supported = self.supportedActions.contains(action.name) - return AvailableAction(action: action, isSupported: supported) + switch result { + case .success(let actions): + // success + print("Actions: \(actions.debugDescription)") + // update data source + self.availableActions = actions.map { action -> AvailableAction in + // record + let supported = self.supportedActions.contains(action.name) + return AvailableAction(action: action, isSupported: supported) + } + self.tableView.reloadData() + + case .failure(let error): + print(error.localizedDescription) } - self.tableView.reloadData() } diff --git a/Example/BlockV/Controllers/Inventory/InventoryCollectionViewController.swift b/Example/BlockV/Controllers/Inventory/InventoryCollectionViewController.swift index f6db213c..ab84ba08 100644 --- a/Example/BlockV/Controllers/Inventory/InventoryCollectionViewController.swift +++ b/Example/BlockV/Controllers/Inventory/InventoryCollectionViewController.swift @@ -152,30 +152,30 @@ class InventoryCollectionViewController: UICollectionViewController { /// Note: Input parameters are left to their defautls. fileprivate func fetchInventory() { - BLOCKv.getInventory { [weak self] (vatomModels, error) in + BLOCKv.getInventory { [weak self] result in - // handle error - guard error == nil else { - print("\n>>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let vatomModels): + print("\nViewer > Fetched inventory") + + /* + NOTE + + It is sometimes useful to order vAtoms by their `whenModified` date. This will + ensure new vAtoms appear at the top of the user's inventory. + + Additionally, if a vAtom's state changes on the BLOCKv platform so to will its + `whenModifed` date. For example, if a vAtom is picked up off the map, its + `droppped` flag is set as `false` and the `whenModified` date updated. + */ + self?.vatoms = vatomModels.sorted { $0.whenModified > $1.whenModified } + + case .failure(let error): + print("\n>>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - print("\nViewer > Fetched inventory") - - /* - NOTE - - It is sometimes useful to order vAtoms by their `whenModified` date. This will - ensure new vAtoms appear at the top of the user's inventory. - - Additionally, if a vAtom's state changes on the BLOCKv platform so to will its - `whenModifed` date. For example, if a vAtom is picked up off the map, its - `droppped` flag is set as `false` and the `whenModified` date updated. - */ - self?.vatoms = vatomModels.sorted { $0.whenModified > $1.whenModified } - } } @@ -191,19 +191,19 @@ class InventoryCollectionViewController: UICollectionViewController { builder.addDefinedFilter(forField: .templateID, filterOperator: .equal, value: "vatomic.prototyping::DrinkCoupon::v1", combineOperator: .and) // execute the discover call - BLOCKv.discover(builder) { [weak self] (vatomModels, error) in + BLOCKv.discover(builder) { [weak self] result in - // handle error - guard error == nil else { - print("\n>>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let vatomModels): + print("\nViewer > Fetched discover vatom models") + print("\n\(vatomModels)") + + case .failure(let error): + print("\n>>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - - // handle success - print("\nViewer > Fetched discover vatom models") - print("\n\(vatomModels)") - + } } diff --git a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift index 9c4cfa75..37e6e72c 100644 --- a/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift +++ b/Example/BlockV/Controllers/Inventory/TappedVatomViewController.swift @@ -105,27 +105,28 @@ class TappedVatomViewController: UIViewController { print(#function) // refresh the vatom - BLOCKv.getVatoms(withIDs: [vatom!.id]) { [weak self] (responseVatom, error) in - - // handle error - guard error == nil else { - print("\n>>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) - return - } - - // ensure a vatom was returned - guard let responseVatom = responseVatom.first else { - print("\n>>> Error > Viewer: No vAtom found") + BLOCKv.getVatoms(withIDs: [vatom!.id]) { [weak self] result in + + switch result { + case .success(let responseVatoms): + // ensure a vatom was returned + guard let responseVatom = responseVatoms.first else { + print("\n>>> Error > Viewer: No vAtom found") + return + } + + self?.vatom = responseVatom + + // update the vatom view + self?.vatomViewA.update(usingVatom: responseVatom) + self?.vatomViewB.update(usingVatom: responseVatom) + self?.vatomViewC.update(usingVatom: responseVatom) + + case .failure(let error): + print("\n>>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - - self?.vatom = responseVatom - - // update the vatom view - self?.vatomViewA.update(usingVatom: responseVatom) - self?.vatomViewB.update(usingVatom: responseVatom) - self?.vatomViewC.update(usingVatom: responseVatom) } diff --git a/Example/BlockV/Controllers/Inventory/VatomDetailTableViewController.swift b/Example/BlockV/Controllers/Inventory/VatomDetailTableViewController.swift index 311c7dab..2248e4b6 100644 --- a/Example/BlockV/Controllers/Inventory/VatomDetailTableViewController.swift +++ b/Example/BlockV/Controllers/Inventory/VatomDetailTableViewController.swift @@ -102,31 +102,32 @@ class VatomDetailTableViewController: UITableViewController { /// Fetches the input vatom's properties from the BLOCKv platform. fileprivate func fetchVatom() { - BLOCKv.getVatoms(withIDs: [vatom.id]) { [weak self] (vatomModels, error) in + BLOCKv.getVatoms(withIDs: [vatom.id]) { [weak self] result in // end refreshing self?.refreshControl?.endRefreshing() - // handle error - guard error == nil else { - print("\n>>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let vatomModels): + // check for our vatom + guard let newVatom = vatomModels.first else { + let message = "Unable to fetch vAtom with id: \(String(describing: self?.vatom.id))." + print("\n>>> Warning > \(message)") + self?.present(UIAlertController.infoAlert(message: message), animated: true) + return + } + + // handle success + print("\nViewer > Fetched vAtom:\n\(newVatom)") + self?.vatom = newVatom + self?.updateUI() + + case .failure(let error): + print("\n>>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // check for our vatom - guard let newVatom = vatomModels.first else { - let message = "Unable to fetch vAtom with id: \(String(describing: self?.vatom.id))." - print("\n>>> Warning > \(message)") - self?.present(UIAlertController.infoAlert(message: message), animated: true) - return - } - - // handle success - print("\nViewer > Fetched vAtom:\n\(newVatom)") - self?.vatom = newVatom - self?.updateUI() - } } diff --git a/Example/BlockV/Controllers/Onboarding/LoginViewController.swift b/Example/BlockV/Controllers/Onboarding/LoginViewController.swift index a21873e4..b412763c 100644 --- a/Example/BlockV/Controllers/Onboarding/LoginViewController.swift +++ b/Example/BlockV/Controllers/Onboarding/LoginViewController.swift @@ -73,22 +73,22 @@ class LoginViewController: UIViewController { // ask the BV platform to login BLOCKv.login(withUserToken: token, type: tokenType, password: password) { - [weak self] (userModel, error) in + [weak self] result in // reset nav bar self?.hideNavBarActivityRight() - // handle error - guard let model = userModel, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userModel): + self?.performSegue(withIdentifier: "seg.login.success", sender: self) + print("Viewer > \(userModel)\n") + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - self?.performSegue(withIdentifier: "seg.login.success", sender: self) - print("Viewer > \(model)\n") - } } @@ -103,25 +103,24 @@ class LoginViewController: UIViewController { let token = userTokenTextField.text ?? "" // ask the BV platform to reset the token - BLOCKv.resetToken(token, type: tokenType) { - [weak self] (userToken, error) in + BLOCKv.resetToken(token, type: tokenType) { [weak self] result in // hide loader self?.hideNavBarActivityRight() - // handle error - guard let model = userToken, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userToken): + let message = "An OTP has been sent to your token. Please use the OTP as a password to login." + self?.present(UIAlertController.okAlert(title: "Info", message: message), animated: true) + + print("Viewer > \(userToken)\n") + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - self?.present(UIAlertController.okAlert(title: "Info", - message: "An OTP has been sent to your token. Please use the OTP as a password to login."), animated: true) - - print("Viewer > \(model)\n") - } } diff --git a/Example/BlockV/Controllers/Onboarding/RegisterTableViewController.swift b/Example/BlockV/Controllers/Onboarding/RegisterTableViewController.swift index 4bb3a2f0..3ac594d3 100644 --- a/Example/BlockV/Controllers/Onboarding/RegisterTableViewController.swift +++ b/Example/BlockV/Controllers/Onboarding/RegisterTableViewController.swift @@ -165,24 +165,23 @@ class RegisterTableViewController: UITableViewController { } /// Register a user with multiple tokens - BLOCKv.register(tokens: tokens, userInfo: userInfo) { - [weak self] (userModel, error) in + BLOCKv.register(tokens: tokens, userInfo: userInfo) { [weak self] result in // hide loader self?.hideNavBarActivityRight() self?.navigationItem.setRightBarButton(self!.doneButton, animated: true) - // handle error - guard let model = userModel, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userModel): + print("Viewer > \(userModel)\n") + self?.performSegue(withIdentifier: "seg.register.done", sender: self) + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - print("Viewer > \(model)\n") - self?.performSegue(withIdentifier: "seg.register.done", sender: self) - } } diff --git a/Example/BlockV/Controllers/Onboarding/VerifyTableViewController.swift b/Example/BlockV/Controllers/Onboarding/VerifyTableViewController.swift index a1ce1a0b..48c2b816 100644 --- a/Example/BlockV/Controllers/Onboarding/VerifyTableViewController.swift +++ b/Example/BlockV/Controllers/Onboarding/VerifyTableViewController.swift @@ -102,25 +102,25 @@ class VerifyTableViewController: UITableViewController { // show loader self.showNavBarActivityRight() - BLOCKv.getCurrentUserTokens { [weak self] (fullTokens, error) in + BLOCKv.getCurrentUserTokens { [weak self] result in // hide loader self?.hideNavBarActivityRight() - // handle error - guard let model = fullTokens, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let fullTokens): + print("Viewer > \(fullTokens)\n") + + // update the tokens + self?.allTokens = fullTokens + self?.tableView.reloadData() + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - - // handle success - print("Viewer > \(model)\n") - - // update the tokens - self?.allTokens = model - self?.tableView.reloadData() - + } } @@ -224,27 +224,27 @@ extension VerifyTableViewController: VerfiyTokenDelegate { // show loader self.showNavBarActivityRight() - BLOCKv.verifyUserToken(token.value, type: token.type, code: code) { - [weak self] (userToken, error) in + BLOCKv.verifyUserToken(token.value, type: token.type, code: code) { [weak self] result in // hide loader self?.hideNavBarActivityRight() self?.navigationItem.rightBarButtonItem = self?.nextBarButton - // handle error - guard let model = userToken, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userToken): + self?.tableView.reloadData() + self?.present(UIAlertController.okAlert(title: "Info", message: "Token: \(userToken.value) has been verified."), animated: true) + + completion(true) + print("Viewer > \(userToken)\n") + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) completion(false) return } - // handle success - self?.tableView.reloadData() - self?.present(UIAlertController.okAlert(title: "Info", message: "Token: \(model.value) has been verified."), animated: true) - - completion(true) - print("Viewer > \(model)\n") } } @@ -253,29 +253,29 @@ extension VerifyTableViewController: VerfiyTokenDelegate { // show loader self.showNavBarActivityRight() - BLOCKv.resetVerification(forUserToken: token.value, type: token.type) { - [weak self] (userToken, error) in + BLOCKv.resetVerification(forUserToken: token.value, type: token.type) { [weak self] result in // hide loader self?.hideNavBarActivityRight() self?.navigationItem.rightBarButtonItem = self?.nextBarButton - // handle error - guard let model = userToken, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userToken): + self?.tableView.reloadData() + self?.present(UIAlertController.okAlert(title: "Info", + message: "An verification link/OTP has been sent to your token."), + animated: true) + + completion(true) + print("Viewer > \(userToken)\n") + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) completion(false) return } - // handle success - self?.tableView.reloadData() - self?.present(UIAlertController.okAlert(title: "Info", - message: "An verification link/OTP has been sent to your token."), - animated: true) - - completion(true) - print("Viewer > \(model)\n") } } diff --git a/Example/BlockV/Controllers/User Profile/PasswordTableViewController.swift b/Example/BlockV/Controllers/User Profile/PasswordTableViewController.swift index 7d0a9336..888d8210 100644 --- a/Example/BlockV/Controllers/User Profile/PasswordTableViewController.swift +++ b/Example/BlockV/Controllers/User Profile/PasswordTableViewController.swift @@ -102,29 +102,27 @@ class PasswordTableViewController: UITableViewController { let userInfo = buildForm() - BLOCKv.updateCurrentUser(userInfo) { - [weak self] (userModel, error) in + BLOCKv.updateCurrentUser(userInfo) { [weak self] result in // reset nav bar self?.hideNavBarActivityRight() self?.navigationItem.setRightBarButton(self!.doneButton, animated: true) - // handle error - guard let model = userModel, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userModel): + print("Viewer > \(userModel)\n") + + // update the model + self?.tableView.reloadData() + + // pop back + self?.navigationController?.popViewController(animated: true) + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - print("Viewer > \(model)\n") - - // update the model - self?.tableView.reloadData() - - // pop back - self?.navigationController?.popViewController(animated: true) - } } diff --git a/Example/BlockV/Controllers/User Profile/ProfileViewController.swift b/Example/BlockV/Controllers/User Profile/ProfileViewController.swift index e0bb181d..335ffbe7 100644 --- a/Example/BlockV/Controllers/User Profile/ProfileViewController.swift +++ b/Example/BlockV/Controllers/User Profile/ProfileViewController.swift @@ -149,27 +149,28 @@ class ProfileViewController: UITableViewController { /// Fetches the user's profile information. fileprivate func fetchUserProfile() { - BLOCKv.getCurrentUser { [weak self] (userModel, error) in + BLOCKv.getCurrentUser { [weak self] result in // end refreshing self?.refreshControl?.endRefreshing() - - // handle error - guard let model = userModel, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)\n") - self?.present(UIAlertController.errorAlert(error!), animated: true) + + switch result { + case .success(let userModel): + print("\nViewer > Fetched user model:\n\(userModel)") + self?.userModel = userModel + + // update ui + self?.displayNameLabel.text = self?.displayName + self?.userIdLabel.text = self?.userModel?.id + + self?.fetchAvatarImage() + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)\n") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - print("\nViewer > Fetched user model:\n\(model)") - self?.userModel = model - - // update ui - self?.displayNameLabel.text = self?.displayName - self?.userIdLabel.text = self?.userModel?.id - - self?.fetchAvatarImage() } } diff --git a/Example/BlockV/Controllers/User Profile/UserInfoTableViewController.swift b/Example/BlockV/Controllers/User Profile/UserInfoTableViewController.swift index 14805ccc..500493a0 100644 --- a/Example/BlockV/Controllers/User Profile/UserInfoTableViewController.swift +++ b/Example/BlockV/Controllers/User Profile/UserInfoTableViewController.swift @@ -124,30 +124,29 @@ class UserInfoTableViewController: UITableViewController { let userInfo = buildForm() - BLOCKv.updateCurrentUser(userInfo) { - [weak self] (userModel, error) in + BLOCKv.updateCurrentUser(userInfo) { [weak self] result in // reset nav bar self?.hideNavBarActivityRight() self?.navigationItem.setRightBarButton(self!.doneButton, animated: true) - // handle error - guard let model = userModel, error == nil else { - print(">>> Error > Viewer: \(error!.localizedDescription)") - self?.present(UIAlertController.errorAlert(error!), animated: true) + switch result { + case .success(let userModel): + print("Viewer > \(userModel)\n") + + // update the model + self?.userModel = userModel + self?.tableView.reloadData() + + // pop back + self?.navigationController?.popViewController(animated: true) + + case .failure(let error): + print(">>> Error > Viewer: \(error.localizedDescription)") + self?.present(UIAlertController.errorAlert(error), animated: true) return } - // handle success - print("Viewer > \(model)\n") - - // update the model - self?.userModel = model - self?.tableView.reloadData() - - // pop back - self?.navigationController?.popViewController(animated: true) - } } diff --git a/Example/BlockV/Extensions/VatomModel+Actions.swift b/Example/BlockV/Extensions/VatomModel+Actions.swift deleted file mode 100644 index 216bb5f1..00000000 --- a/Example/BlockV/Extensions/VatomModel+Actions.swift +++ /dev/null @@ -1,80 +0,0 @@ -// MIT License -// -// Copyright (c) 2018 BlockV AG -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Foundation -import BLOCKv - -/// Action Extensions -extension VatomModel { - - /// Clones this vAtom to the specified token. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - token: Standard UserToken (Phone, Email, or User ID) - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func clone(toToken token: UserToken, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - // perform the action - BLOCKv.performAction(name: "Clone", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - - /// Redeems this vAtom to the specified token. - /// - /// Note: Calling this action will trigger the action associated with this vAtom's - /// template. If an action has not been configured, an error will be generated. - /// - /// - Parameters: - /// - token: Standard UserToken (Phone, Email, or User ID) - /// - completion: The completion handler to call when the action is completed. - /// This handler is executed on the main queue. - public func redeem(toToken token: UserToken, - completion: @escaping ([String: Any]?, BVError?) -> Void) { - - let body = [ - "this.id": self.id, - "new.owner.\(token.type.rawValue)": token.value - ] - - // perform the action - BLOCKv.performAction(name: "Redeem", payload: body) { (json, error) in - //TODO: should it be weak self? - completion(json, error) - } - - } - -} diff --git a/Example/BlockV/Views/LiveVatomView.swift b/Example/BlockV/Views/LiveVatomView.swift index 11d5aa0d..0432191c 100644 --- a/Example/BlockV/Views/LiveVatomView.swift +++ b/Example/BlockV/Views/LiveVatomView.swift @@ -77,17 +77,19 @@ class LiveVatomView: VatomView { guard let vatomID = self.vatom?.id else { return } // fetch vatom model from remote - BLOCKv.getVatoms(withIDs: [vatomID], completion: { (vatomModels, error) in + BLOCKv.getVatoms(withIDs: [vatomID], completion: { result in - // ensure no error - guard error == nil, let vatom = vatomModels.first else { + switch result { + case .success(let vatomModels): + guard let vatom = vatomModels.first else { return } + // update vatom view using the new state of the vatom + self.update(usingVatom: vatom) + + case .failure: print(">>> Viewer: Unable to fetch vAtom.") return } - // update vatom view using the new state of the vatom - self.update(usingVatom: vatom) - }) } From 35bf5ecc99b3a164789b8aff03b4dfaaedc64ebb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 11:02:33 +0200 Subject: [PATCH 057/165] Core: Feature - Native bridge result (#200) * Convert to Result type --- .../Web/Face Bridge/CoreBridge.swift | 2 +- .../Web/Face Bridge/CoreBridgeV1.swift | 82 ++--- .../Web/Face Bridge/CoreBridgeV2.swift | 311 +++++++++++------- .../Web/WebScriptMessageHandler.swift | 39 +-- 4 files changed, 255 insertions(+), 179 deletions(-) diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift index 765be722..062038b8 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridge.swift @@ -23,7 +23,7 @@ protocol CoreBridge { /// - parameters: /// - payload: Payload portion of the response. /// - error: Any error encountered during message processing. - typealias Completion = (_ payload: JSON?, _ error: BridgeError?) -> Void + typealias Completion = (Result) -> Void /// Processes the message and calls the completion handler once the output is known. /// diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift index 56018f4c..29f2c013 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV1.swift @@ -82,7 +82,7 @@ class CoreBridgeV1: CoreBridge { } // swiftlint:disable cyclomatic_complexity - + // swiftlint:disable function_body_length /// Processes the face script message and calls the completion handler with the result for encoding. func processMessage(_ scriptMessage: RequestScriptMessage, completion: @escaping Completion) { @@ -102,7 +102,7 @@ class CoreBridgeV1: CoreBridge { // ensure caller supplied params guard let vatomID = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing vAtom ID.") - completion(nil, error) + completion(.failure(error)) return } @@ -116,7 +116,7 @@ class CoreBridgeV1: CoreBridge { permittedIDs.contains(vatomID) else { let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) return } @@ -128,13 +128,13 @@ class CoreBridgeV1: CoreBridge { // ensure caller supplied params guard let vatomID = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing vAtom ID.") - completion(nil, error) + completion(.failure(error)) return } // security check guard vatomID == self.faceView?.vatom.id else { let error = BridgeError.caller("This method is only permitted on the backing vatom: \(vatomID).") - completion(nil, error) + completion(.failure(error)) return } self.discoverChildren(forVatomID: vatomID, completion: completion) @@ -143,7 +143,7 @@ class CoreBridgeV1: CoreBridge { // ensure caller supplied params guard let userID = scriptMessage.payload["userID"]?.stringValue else { let error = BridgeError.caller("Missing user ID.") - completion(nil, error) + completion(.failure(error)) return } self.getPublicUser(forUserID: userID, completion: completion) @@ -152,7 +152,7 @@ class CoreBridgeV1: CoreBridge { // ensure caller supplied params guard let userID = scriptMessage.payload["userID"]?.stringValue else { let error = BridgeError.caller("Missing user ID.") - completion(nil, error) + completion(.failure(error)) return } self.getPublicAvatarURL(forUserID: userID, completion: completion) @@ -165,13 +165,13 @@ class CoreBridgeV1: CoreBridge { let thisID = actionData["this.id"]?.stringValue else { let error = BridgeError.caller("Invalid payload.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom guard thisID == self.faceView?.vatom.id else { let error = BridgeError.caller("This method is only permitted for the backing vatom.") - completion(nil, error) + completion(.failure(error)) return } @@ -214,7 +214,7 @@ class CoreBridgeV1: CoreBridge { // santiy check guard let faceView = self.faceView else { let error = BridgeError.viewer("Invalid state.") - completion(nil, error) + completion(.failure(error)) return } @@ -244,13 +244,13 @@ class CoreBridgeV1: CoreBridge { // ensure no error guard error == nil else { let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) return } // ensure a single vatom guard let firstVatom = vatoms.first else { let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) return } // create bridge response @@ -264,18 +264,18 @@ class CoreBridgeV1: CoreBridge { do { // json encode the model let json = try JSON.init(encodable: response) - completion(json, nil) + completion(.success(json)) } catch { let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } }) - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to fetch current user.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } @@ -290,12 +290,12 @@ class CoreBridgeV1: CoreBridge { // ensure no error guard error == nil else { - completion(nil, error!) + completion(.failure(error!)) return } // ensure there is at least one vatom guard let formattedVatom = formattedVatoms.first else { - completion(nil, BridgeError.viewer("vAtom not found.")) + completion(.failure(BridgeError.viewer("vAtom not found."))) return } let response = ["vatomInfo": formattedVatom] @@ -303,10 +303,10 @@ class CoreBridgeV1: CoreBridge { do { // json encode the model let json = try JSON.init(encodable: response) - completion(json, nil) + completion(.success(json)) } catch { let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -322,7 +322,7 @@ class CoreBridgeV1: CoreBridge { // ensure no error guard error == nil else { - completion(nil, error!) + completion(.failure(error!)) return } let vatomItems = formattedVatoms.map { ["vatomInfo": $0] } @@ -330,10 +330,10 @@ class CoreBridgeV1: CoreBridge { do { // json encode the model let json = try JSON.init(encodable: response) - completion(json, nil) + completion(.success(json)) } catch { let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -360,16 +360,16 @@ class CoreBridgeV1: CoreBridge { do { // json encode the model let json = try JSON.init(encodable: response) - completion(json, nil) + completion(.success(json)) } catch { let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -399,16 +399,16 @@ class CoreBridgeV1: CoreBridge { do { // json encode the model let json = try JSON.init(encodable: response) - completion(json, nil) + completion(.success(json)) } catch { let bridgeError = BridgeError.viewer("Unable to encode response.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -434,20 +434,24 @@ class CoreBridgeV1: CoreBridge { switch result { case .success(let payload): // convert to json - let json = try? JSON(payload) - completion(json, nil) + guard let json = try? JSON(payload) else { + let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") + completion(.failure(bridgeError)) + return + } + completion(.success(json)) - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } } catch { let error = BridgeError.viewer("Unable to encode data.") - completion(nil, error) + completion(.failure(error)) } } @@ -486,7 +490,7 @@ private extension CoreBridgeV1 { permittedIDs.append(backingID) completion(permittedIDs, nil) - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") completion(nil, bridgeError) @@ -511,7 +515,7 @@ private extension CoreBridgeV1 { // convert vAtom into bridge format completion(self.formatVatoms(vatoms), nil) - case .failure(let error): + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch backing vAtom.") completion([], bridgeError) } @@ -535,7 +539,7 @@ private extension CoreBridgeV1 { // format vatoms completion(self.formatVatoms(vatoms), nil) - case .failure(let error): + case .failure: // handle error let bridgeError = BridgeError.viewer("Unable to fetch children for vAtom \(id).") completion([], bridgeError) diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift index 6444d456..5ef2f14a 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift @@ -113,27 +113,34 @@ class CoreBridgeV2: CoreBridge { return !(MessageName(rawValue: message) == nil) } - // swiftlint:disable cyclomatic_complexity - /// Processes the face script message and calls the completion handler with the result for encoding. - func processMessage(_ scriptMessage: RequestScriptMessage, completion: @escaping (JSON?, BridgeError?) -> Void) { + // swiftlint:disable cyclomatic_complexity + // swiftlint:disable function_body_length + func processMessage(_ scriptMessage: RequestScriptMessage, + completion: @escaping (Result) -> Void) { let message = MessageName(rawValue: scriptMessage.name)! - printBV(info: "CoreBride_2: \(message)") + //printBV(info: "CoreBride_2: \(message)") // switch and route message switch message { case .initialize: - self.setupBridge { (payload, error) in + self.setupBridge { result in - // json dance - if let payload = payload { - let payload = try? JSON.init(encodable: payload) - completion(payload, error) - return + switch result { + case .success(let payload): + // json dance + guard let payload = try? JSON.init(encodable: payload) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) } @@ -141,32 +148,44 @@ class CoreBridgeV2: CoreBridge { // ensure caller supplied params guard let vatomID = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing 'id' key.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom or first-level children - self.permittedVatomIDs { (permittedIDs, error) in + self.permittedVatomIDs { result in + + switch result { + case .success(let permittedIDs): - // ensure no error - guard error == nil, - let permittedIDs = permittedIDs, // check if the id is permitted to be queried - permittedIDs.contains(vatomID) - else { + guard permittedIDs.contains(vatomID) else { let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) - return - } - - self.getVatoms(withIDs: [vatomID], completion: { (payload, error) in - // json dance - if let payload = payload?["vatoms"]?.first { - let payload = try? JSON.init(encodable: ["vatom": payload]) - completion(payload, error) + completion(.failure(bridgeError)) return } - completion(nil, error) - }) + + self.getVatoms(withIDs: [vatomID], completion: { result in + + switch result { + case .success(let payload): + // json dance + guard let vatom = payload["vatoms"]?.first, + let payload = try? JSON.init(encodable: ["vatom": vatom]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + case .failure(let error): + completion(.failure(error)) + } + + }) + + case .failure: + let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") + completion(.failure(bridgeError)) + } } @@ -174,40 +193,58 @@ class CoreBridgeV2: CoreBridge { // ensure caller supplied params guard let vatomID = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing 'id' key.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom guard vatomID == self.faceView?.vatom.id else { let error = BridgeError.caller("This method is only permitted on the backing vatom.") - completion(nil, error) + completion(.failure(error)) return } - self.discoverChildren(forVatomID: vatomID, completion: { (payload, error) in - // json dance - if let payload = payload?["vatoms"] { - let payload = try? JSON.init(encodable: ["vatoms": payload]) - completion(payload, error) - return + self.discoverChildren(forVatomID: vatomID, completion: { result in + + switch result { + case .success(let payload): + // json dance + guard + let vatoms = payload["vatoms"], + let payload = try? JSON.init(encodable: ["vatoms": vatoms]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) + }) case .getUser: // ensure caller supplied params guard let userID = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing 'id' key.") - completion(nil, error) + completion(.failure(error)) return } - self.getPublicUser(userID: userID) { (payload, error) in - // json dance - if let payload = payload { - let payload = try? JSON.init(encodable: ["user": payload]) - completion(payload, error) - return + self.getPublicUser(userID: userID) { result in + + switch result { + case .success(let payload): + // json dance + guard let payload = try? JSON.init(encodable: ["user": payload]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) + } case .performAction: @@ -218,24 +255,32 @@ class CoreBridgeV2: CoreBridge { let thisID = actionPayload["this.id"]?.stringValue else { let error = BridgeError.caller("Missing 'action_name' or 'payload' keys.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom guard thisID == self.faceView?.vatom.id else { let error = BridgeError.caller("This method is only permitted on the backing vatom.") - completion(nil, error) + completion(.failure(error)) return } // perform action - self.performAction(name: actionName, payload: actionPayload) { (payload, error) in - // json dance - if let payload = payload { - let payload = try? JSON.init(encodable: ["user": payload]) - completion(payload, error) - return + self.performAction(name: actionName, payload: actionPayload) { result in + + switch result { + case .success(let payload): + // json dance + guard let payload = try? JSON.init(encodable: ["user": payload]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) + } case .encodeResource: @@ -247,25 +292,33 @@ class CoreBridgeV2: CoreBridge { // extract urls guard let urlStrings = scriptMessage.payload["urls"]?.arrayValue?.map({ $0.stringValue }) else { let error = BridgeError.caller("Missing 'urls' key.") - completion(nil, error) + completion(.failure(error)) return } // ensure all urls are strings let flatURLStrings = urlStrings.compactMap { $0 } guard urlStrings.count == flatURLStrings.count else { let error = BridgeError.caller("Invalid url data type.") - completion(nil, error) + completion(.failure(error)) return } + // encode resources + self.encodeResources(flatURLStrings) { result in - self.encodeResources(flatURLStrings) { (payload, error) in - // json dance - if let payload = payload { - let payload = try? JSON.init(encodable: ["urls": payload]) - completion(payload, error) - return + switch result { + case .success(let payload): + // json dance + guard let payload = try? JSON.init(encodable: ["urls": payload]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) + } case .setVatomParent: @@ -274,26 +327,27 @@ class CoreBridgeV2: CoreBridge { let childVatomId = scriptMessage.payload["id"]?.stringValue, let parentId = scriptMessage.payload["parent_id"]?.stringValue else { let error = BridgeError.caller("Missing 'id' or 'parent_id' key.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom or first-level children - self.permittedVatomIDs { (permittedIDs, error) in + self.permittedVatomIDs { result in - // ensure no error - guard error == nil, let permittedIDs = permittedIDs else { + switch result { + case .success(let permittedIDs): + // security check + if permittedIDs.contains(childVatomId) { + // set parent + self.setParentId(on: childVatomId, parentId: parentId, completion: completion) + } else { + let message = "This method is only permitted on the backing vatom or one of its children." + let bridgeError = BridgeError.viewer(message) + completion(.failure(bridgeError)) + } + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) - return - } - // security check - if permittedIDs.contains(childVatomId) { - // set parent - self.setParentId(on: childVatomId, parentId: parentId, completion: completion) - } else { - let bridgeError = BridgeError.viewer("This method is only permitted on the backing vatom or one of its children.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -302,26 +356,37 @@ class CoreBridgeV2: CoreBridge { // ensure caller supplied params guard let vatomId = scriptMessage.payload["id"]?.stringValue else { let error = BridgeError.caller("Missing 'id' key.") - completion(nil, error) + completion(.failure(error)) return } // security check - backing vatom guard vatomId == self.faceView?.vatom.id else { let error = BridgeError.caller("This method is only permitted on the backing vatom.") - completion(nil, error) + completion(.failure(error)) return } // update observer list (this informs the native bridge to forward child updates to the WFSDK) childObservationVatomIds.insert(vatomId) + // find current children - self.discoverChildren(forVatomID: vatomId, completion: { (payload, error) in - // json dance - if let payload = payload?["vatoms"] { - let payload = try? JSON.init(encodable: ["vatoms": payload]) - completion(payload, error) - return + self.discoverChildren(forVatomID: vatomId, completion: { result in + + switch result { + case .success(let payload): + // json dance + guard + let vatoms = payload["vatoms"], + let response = try? JSON.init(encodable: ["vatoms": vatoms]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(response)) + + case .failure(let error): + completion(.failure(error)) } - completion(nil, error) + }) } @@ -360,19 +425,19 @@ class CoreBridgeV2: CoreBridge { /// Creates the bridge initializtion JSON data. /// /// - Parameter completion: Completion handler to call with JSON data to be passed to the Web Face SDK. - private func setupBridge(_ completion: @escaping (BRSetup?, BridgeError?) -> Void) { + private func setupBridge(_ completion: @escaping (Result) -> Void) { // santiy check guard let faceView = self.faceView else { let error = BridgeError.viewer("Invalid state.") - completion(nil, error) + completion(.failure(error)) return } let vatom = faceView.vatom let face = faceView.faceModel let response = BRSetup(vatom: vatom, face: face) - completion(response, nil) + completion(.success(response)) } @@ -384,18 +449,18 @@ class CoreBridgeV2: CoreBridge { /// - ids: Unique identifier of the vAtom. /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func getVatoms(withIDs ids: [String], - completion: @escaping ([String: [VatomModel]]?, BridgeError?) -> Void) { + completion: @escaping (Result<[String: [VatomModel]], BridgeError>) -> Void) { BLOCKv.getVatoms(withIDs: ids) { result in switch result { case .success(let vatoms): let response = ["vatoms": vatoms] - completion(response, nil) + completion(.success(response)) - case .failure(let error): + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -405,12 +470,12 @@ class CoreBridgeV2: CoreBridge { /// Returns an array of vAtom IDs which are permitted to be queried. /// /// Business Rule: Only the backing vAtom or one of it's children may be queried. - private func permittedVatomIDs(completion: @escaping ([String]?, Error?) -> Void) { + private func permittedVatomIDs(completion: @escaping (Result<[String], Error>) -> Void) { guard let backingID = self.faceView?.vatom.id else { assertionFailure("The backing vatom must be non-nil.") let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) return } @@ -424,11 +489,11 @@ class CoreBridgeV2: CoreBridge { // create a list of the child vatoms and add the backing (parent vatom) var permittedIDs = vatoms.map { $0.id } permittedIDs.append(backingID) - completion(permittedIDs, nil) + completion(.success(permittedIDs)) - case .failure(let error): + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch vAtoms.") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -443,7 +508,7 @@ class CoreBridgeV2: CoreBridge { /// - id: Unique identifier of the vAtom. /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func discoverChildren(forVatomID id: String, - completion: @escaping ([String: [VatomModel]]?, BridgeError?) -> Void) { + completion: @escaping (Result<[String: [VatomModel]], BridgeError>) -> Void) { let builder = DiscoverQueryBuilder() builder.setScope(scope: .parentID, value: id) @@ -453,10 +518,10 @@ class CoreBridgeV2: CoreBridge { switch result { case .success(let vatoms): let response = ["vatoms": vatoms] - completion(response, nil) - case .failure(let error): + completion(.success(response)) + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch children for vAtom \(id).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -468,7 +533,7 @@ class CoreBridgeV2: CoreBridge { /// - Parameters: /// - id: Unique identifier of the user. /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. - private func getPublicUser(userID id: String, completion: @escaping (BRUser?, BridgeError?) -> Void) { + private func getPublicUser(userID id: String, completion: @escaping (Result) -> Void) { BLOCKv.getPublicUser(withID: id) { result in @@ -479,11 +544,11 @@ class CoreBridgeV2: CoreBridge { lastName: user.properties.lastName, avatarURI: user.properties.avatarURL?.absoluteString ?? "") let response = BRUser(id: user.id, properties: properties) - completion(response, nil) + completion(.success(response)) - case .failure(let error): + case .failure: let bridgeError = BridgeError.viewer("Unable to fetch public user: \(id).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } @@ -497,7 +562,7 @@ class CoreBridgeV2: CoreBridge { /// - payload: Payload to send to the server. /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. private func performAction(name: String, payload: [String: JSON], - completion: @escaping (JSON?, BridgeError?) -> Void) { + completion: @escaping (Result) -> Void) { do { /* @@ -515,19 +580,23 @@ class CoreBridgeV2: CoreBridge { switch result { case .success(let payload): // convert to json - let json = try? JSON(payload) - completion(json, nil) + guard let json = try? JSON(payload) else { + let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") + completion(.failure(bridgeError)) + return + } + completion(.success(json)) - case .failure(let error): + case .failure: let bridgeError = BridgeError.viewer("Unable to perform action: \(name).") - completion(nil, bridgeError) + completion(.failure(bridgeError)) } } } catch { let error = BridgeError.viewer("Unable to encode data.") - completion(nil, error) + completion(.failure(error)) } } @@ -543,7 +612,8 @@ class CoreBridgeV2: CoreBridge { /// - Parameters: /// - urlStrings: Array of URL strings to be encoded (if possible). /// - completion: Completion handler to call with JSON data to be passed to the Web Face SDK. - private func encodeResources(_ urlStrings: [String], completion: @escaping ([String]?, BridgeError?) -> Void) { + private func encodeResources(_ urlStrings: [String], + completion: @escaping (Result<[String], BridgeError>) -> Void) { // convert to URL type let urls = urlStrings.map { URL(string: $0) } @@ -558,7 +628,7 @@ class CoreBridgeV2: CoreBridge { } } - completion(responseURLs, nil) + completion(.success(responseURLs)) } // MARK: - 2.1 @@ -575,20 +645,21 @@ class CoreBridgeV2: CoreBridge { guard let vatom = DataPool.inventory().get(id: vatomId) as? VatomModel else { let message = "Unable to set parent Id: \(parentId). Data Pool inventory lookup failed." let bridgeError = BridgeError.viewer(message) - completion(nil, bridgeError) + completion(.failure(bridgeError)) return } // update parent id BLOCKv.setParentID(ofVatoms: [vatom], to: parentId) { result in switch result { - case .success(let x): + case .success: let response: JSON = ["new_parent_id": JSON.string(parentId)] - completion(response, nil) + completion(.success(response)) case .failure(let error): - let bridgeError = BridgeError.viewer("Unable to set parent Id: \(parentId). \(error.localizedDescription)") - completion(nil, bridgeError) + let message = "Unable to set parent Id: \(parentId). \(error.localizedDescription)" + let bridgeError = BridgeError.viewer(message) + completion(.failure(bridgeError)) } } diff --git a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift index ff5f64d3..616b4019 100644 --- a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift +++ b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift @@ -76,7 +76,7 @@ extension WebFaceView: WKScriptMessageHandler { /// /// Ideally, this function should only allow [String: Any] payloads. However, allows primative types, e.g. Bool, /// to be passed across as Javascript strings. - func sendResponse(forRequestMessage message: RequestScriptMessage, payload: JSON?, error: BridgeError?) { + func sendResponse(forRequestMessage message: RequestScriptMessage, result: Result) { /* Note: Accepted Viewer Payloads @@ -87,17 +87,17 @@ extension WebFaceView: WKScriptMessageHandler { - V2: Only objects. */ - if let payload = payload { - + switch result { + case .success(let payload): // create response var response: JSON? if message.version == "1.0.0" { response = payload } else { response = [ - "name": try! JSON(message.name), - "response_id": try! JSON(message.requestID), - "payload": payload + "name": try! JSON(message.name), + "response_id": try! JSON(message.requestID), + "payload": payload ] } @@ -106,13 +106,12 @@ extension WebFaceView: WKScriptMessageHandler { let jsonString = String.init(data: data, encoding: .utf8) else { // handle error let error = BridgeError.viewer("Unable to encode response.") - self.sendResponse(forRequestMessage: message, payload: nil, error: error) + self.sendResponse(forRequestMessage: message, result: .failure(error)) return } self.postMessage(message.requestID, withJSONString: jsonString) - } else if let error = error { - + case .failure(let error): // create response var response: JSON? if message.version == "1.0.0" { @@ -130,13 +129,11 @@ extension WebFaceView: WKScriptMessageHandler { let jsonString = String.init(data: data, encoding: .utf8) else { // handle error let error = BridgeError.viewer("Unable to encode response.") - self.sendResponse(forRequestMessage: message, payload: nil, error: error) + self.sendResponse(forRequestMessage: message, result: .failure(error)) return } self.postMessage(message.requestID, withJSONString: jsonString) - } else { - assertionFailure("Either 'payload' or 'error' must be non nil.") } } @@ -224,10 +221,16 @@ extension WebFaceView { // determine appropriate responder (core or viewer) if coreBridge.canProcessMessage(message.name) { // forward to core bridge - coreBridge.processMessage(message) { (payload, error) in + coreBridge.processMessage(message) { result in - // post response - self.sendResponse(forRequestMessage: message, payload: payload, error: error) + switch result { + case .success(let payload): + // post response + self.sendResponse(forRequestMessage: message, result: .success(payload)) + case .failure(let error): + // post response + self.sendResponse(forRequestMessage: message, result: .failure(error)) + } } } else { @@ -249,14 +252,12 @@ extension WebFaceView { completion: { result in switch result { case .success(let payload): - self.sendResponse(forRequestMessage: message, payload: payload, error: nil) + self.sendResponse(forRequestMessage: message, result: .success(payload)) return case .failure(let error): // transform the bridge error let bridgeError = BridgeError.viewer(error.message) - self.sendResponse(forRequestMessage: message, - payload: nil, - error: bridgeError) + self.sendResponse(forRequestMessage: message, result: .failure(bridgeError)) return } }) From 76fa5f12251589e7f90fc14378ee05aff454b875 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 11:08:57 +0200 Subject: [PATCH 058/165] Apply linter (#201) --- .../Helpers/Vatom+PreemptiveActions.swift | 4 +- .../Models/Package/VatomUpdateModel.swift | 2 +- BlockV/Core/Network/Stack/API.swift | 75 +++++++++---------- .../Requests/BLOCKv+ActivityRequests.swift | 6 +- .../Core/Requests/BLOCKv+AuthRequests.swift | 6 +- .../Core/Requests/BLOCKv+UserRequests.swift | 6 +- .../Core/Requests/BLOCKv+VatomRequest.swift | 30 ++++---- 7 files changed, 64 insertions(+), 65 deletions(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index 2d8a27d2..712b71e6 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -186,11 +186,11 @@ extension VatomModel { // perform the action BLOCKv.performAction(name: name, payload: payload) { result in - + switch result { case .success(let payload): completion(.success(payload)) - + case .failure(let error): // run undo closures undos.forEach { $0() } diff --git a/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift b/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift index f2e22d1c..fac6dce4 100644 --- a/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift +++ b/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift @@ -24,7 +24,7 @@ public struct VatomUpdateModel: Decodable, Equatable { case errorMessage = "error_messages" case ids } - + public init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) numberUpdated = try values.decode(Int.self, forKey: .numberUpdated) diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 10b2c86b..6c249162 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -252,7 +252,7 @@ extension API { /// Consolidates all user vatom endpoints. enum Vatom { - + /// Builds an endpoint to get the current user's inventory. /// /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's @@ -263,7 +263,7 @@ extension API { page: Int = 0, limit: Int = 0) -> Endpoint> { return API.Generic.getInventory(parentID: parentID, page: page, limit: limit) - + } /// Builds an endpoint to get a vAtom by its unique identifier. @@ -273,7 +273,6 @@ extension API { static func getVatoms(withIDs ids: [String]) -> Endpoint> { return API.Generic.getVatoms(withIDs: ids) } - /// Builds the endpoint to trash a vAtom specified by its id. /// @@ -284,7 +283,7 @@ extension API { parameters: ["this.id": id]) } - + /// Builds an endpoint to update a vAtom. /// /// - Parameter payload: Raw payload. @@ -292,7 +291,7 @@ extension API { static func updateVatom(payload: [String: Any]) -> Endpoint> { return API.Generic.updateVatom(payload: payload) } - + /// Builds an endpoint to search for vAtoms. /// /// - Parameter payload: Raw request payload. @@ -300,7 +299,7 @@ extension API { static func discover(_ payload: [String: Any]) -> Endpoint> { return API.Generic.discover(payload) } - + /// Builds an endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). /// /// Use this endpoint to fetch a collection of vAtoms. @@ -317,7 +316,7 @@ extension API { topRightLat: Double, topRightLon: Double, filter: String) -> Endpoint> { - + return API.Generic.geoDiscover(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, topRightLat: topRightLat, @@ -426,16 +425,16 @@ extension API { } extension API { - + enum Generic { - + private static let userVatomPath = "/v1/user/vatom" private static let userActionsPath = "/v1/user/actions" private static let actionPath = "/v1/user/vatom/action" private static let userActivityPath = "/v1/activity" - + // MARK: Vatoms - + /// Builds the generic endpoint to get the current user's inventory. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -449,7 +448,7 @@ extension API { ] ) } - + /// Builds a generic endpoint to get a vAtom by its unique identifier. /// /// - Parameter ids: Unique identifier of the vatom. @@ -460,7 +459,7 @@ extension API { parameters: ["ids": ids] ) } - + /// Builds a generic endpoint to update a vAtom. /// /// - Parameter payload: Raw payload. @@ -470,18 +469,18 @@ extension API { path: "/v1/vatoms", parameters: payload) } - + /// Builds a generic endpoint to search for vAtoms. /// /// - Parameter payload: Raw request payload. /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func discover(_ payload: [String: Any]) -> Endpoint { - + return Endpoint(method: .post, path: "/v1/vatom/discover", parameters: payload) } - + /// Builds a generic endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). /// /// Use this endpoint to fetch a collection of vAtoms. @@ -498,7 +497,7 @@ extension API { topRightLat: Double, topRightLon: Double, filter: String) -> Endpoint { - + // create the payload let payload: [String: Any] = [ @@ -514,14 +513,14 @@ extension API { ], "filter": filter ] - + // create the endpoint return Endpoint(method: .post, path: "/v1/vatom/geodiscover", parameters: payload) - + } - + /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). /// /// Use this endpoint to fetch an collection of groups/annotation indicating the count @@ -541,9 +540,9 @@ extension API { topRightLon: Double, precision: Int, filter: String) -> Endpoint { - + assert(1...12 ~= precision, "You must specify a value in the open range [1...12].") - + // create the payload let payload: [String: Any] = [ @@ -560,16 +559,16 @@ extension API { "precision": precision, "filter": filter ] - + // create the endpoint return Endpoint(method: .post, path: "/v1/vatom/geodiscovergroups", parameters: payload) - + } - + // MARK: - Perform Actions - + /// Builds the endpoint to perform and action on a vAtom. /// /// - Parameters: @@ -580,9 +579,9 @@ extension API { return Endpoint(method: .post, path: actionPath + "/\(name)", parameters: payload) } - + // MARK: Fetch Actions - + /// Builds the endpoint for fetching the actions configured for a template ID. /// /// - Parameter id: Uniquie identifier of the template. @@ -591,9 +590,9 @@ extension API { return Endpoint(method: .get, path: userActionsPath + "/\(id)") } - + // MARK: - User Activity - + /// Builds the endpoint for fetching the threads involving the current user. /// /// - Parameters: @@ -602,17 +601,17 @@ extension API { /// - count: Defines the number of messages to return (after the cursor). /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getThreads(cursor: String, count: Int) -> Endpoint { - + let payload: [String: Any] = [ "cursor": cursor, "count": count ] - + return Endpoint(method: .post, path: userActivityPath + "/mythreads", parameters: payload) } - + /// Builds the endpoint for fetching the message for a specified thread involving the current user. /// /// - Parameters: @@ -622,19 +621,19 @@ extension API { /// - count: Defines the number of messages to return (after the cursor). /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> Endpoint { - + let payload: [String: Any] = [ "name": threadId, "cursor": cursor, "count": count ] - + return Endpoint(method: .post, path: userActivityPath + "/mythreadmessages", parameters: payload) - + } - + } - + } diff --git a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift index e0954a6c..9a910575 100644 --- a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift @@ -30,7 +30,7 @@ extension BLOCKv { let endpoint = API.UserActivity.getThreads(cursor: cursor, count: count) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -66,7 +66,7 @@ extension BLOCKv { let endpoint = API.UserActivity.getMessages(forThreadId: threadId, cursor: cursor, count: count) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -97,7 +97,7 @@ extension BLOCKv { let endpoint = API.CurrentUser.sendMessage(message, toUserId: userId) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index faab6f84..d9f2f07a 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -61,7 +61,7 @@ extension BLOCKv { let endpoint = API.Session.register(tokens: tokens, userInfo: userInfo) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -175,12 +175,12 @@ extension BLOCKv { let endpoint = API.CurrentUser.logOut() self.client.request(endpoint) { result in - + // reset DispatchQueue.main.async { reset() } - + switch result { case .success(let model): // model is available diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index 036eeb0a..f76f1fa8 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -25,7 +25,7 @@ extension BLOCKv { let endpoint = API.CurrentUser.get() self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -301,7 +301,7 @@ extension BLOCKv { let endpoint = API.CurrentUser.deleteToken(id: tokenId) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -336,7 +336,7 @@ extension BLOCKv { let endpoint = API.CurrentUser.setDefaultToken(id: tokenId) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index 61f37363..841099bb 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -146,20 +146,20 @@ extension BLOCKv { keyPath: "vAtom::vAtomType.parent_id", value: parentID)) } - + let ids = vatoms.map { $0.id } let payload: [String: Any] = [ "ids": ids, "parent_id": parentID ] - + let endpoint = API.Vatom.updateVatom(payload: payload) BLOCKv.client.request(endpoint) { result in - + switch result { case .success(let baseModel): - + /* # Note The most likely scenario where there will be partial containment errors is when setting the parent id @@ -172,7 +172,7 @@ extension BLOCKv { undosToRollback.forEach { $0.undo() } // complete completion(.success(updateVatomModel)) - + case .failure(let error): // roll back all containments undos.forEach { $0.undo() } @@ -199,7 +199,7 @@ extension BLOCKv { // explicitly set return type to payload builder.setReturn(type: .payload) self.discover(payload: builder.toDictionary()) { result in - + switch result { case .success(let discoverResult): // model is available @@ -212,7 +212,7 @@ extension BLOCKv { completion(.failure(error)) } } - + } } @@ -237,7 +237,7 @@ extension BLOCKv { let endpoint = API.Vatom.discover(payload) self.client.request(endpoint) { result in - + switch result { case .success(let baseModel): // model is available @@ -402,10 +402,10 @@ extension BLOCKv { let endpoint = API.VatomAction.custom(name: name, payload: payload) self.client.request(endpoint) { result in - + switch result { case .success(let data): - + do { guard let object = try JSONSerialization.jsonObject(with: data) as? [String: Any], @@ -416,12 +416,12 @@ extension BLOCKv { DispatchQueue.main.async { completion(.success(payload)) } - + } catch { let error = BVError.modelDecoding(reason: error.localizedDescription) completion(.failure(error)) } - + case .failure(let error): // handle error DispatchQueue.main.async { @@ -432,7 +432,7 @@ extension BLOCKv { } } - + // MARK: - Common Actions for Unowned vAtoms /// Performs an acquire action on a vAtom. @@ -451,9 +451,9 @@ extension BLOCKv { // perform the action self.performAction(name: "Acquire", payload: body) { result in - + //FIXME: Call into Data Pool - + completion(result) } From d79eb983945babd9eea90e263b7e02c9c76fa736 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 11:24:42 +0200 Subject: [PATCH 059/165] Face: Fix- Data pool integration (#202) * Replace vatom observer with data pool integration --- .../Image Layered/ImageLayeredFaceView.swift | 59 +--- .../Image Policy/ImagePolicyFaceView.swift | 31 +-- .../Face Views/Utilities/VatomObserver.swift | 177 ------------ .../Utilities/VatomObserverStore.swift | 252 ------------------ 4 files changed, 5 insertions(+), 514 deletions(-) delete mode 100644 BlockV/Face/Face Views/Utilities/VatomObserver.swift delete mode 100644 BlockV/Face/Face Views/Utilities/VatomObserverStore.swift diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 177bf993..e7b497e2 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -52,8 +52,8 @@ class ImageLayeredFaceView: FaceView { private var childLayers: [Layer] = [] private var childVatoms: [VatomModel] { - // observer store manages the child vatoms - return Array(vatomObserverStore.childVatoms) + // fetch cached children + return self.vatom.listCachedChildren() } // MARK: - Config @@ -83,25 +83,12 @@ class ImageLayeredFaceView: FaceView { private let config: Config - /* - NOTE - The `vatomChanged()` method called by `VatomView` does not handle child vatom updates. - The `VatomObserver` class is used to receive these events. This is required for the Child Count policy type. - */ - - /// Class responsible for observing changes related backing vAtom. - private var vatomObserverStore: VatomObserverStore - // MARK: - Init required init(vatom: VatomModel, faceModel: FaceModel) { // init face config self.config = Config(faceModel) - // create an observer for the backing vatom - self.vatomObserverStore = VatomObserverStore(vatomID: vatom.id) - super.init(vatom: vatom, faceModel: faceModel) - self.vatomObserverStore.delegate = self self.addSubview(baseLayer) @@ -129,39 +116,21 @@ class ImageLayeredFaceView: FaceView { */ self.loadBaseResource() - // continue loading by reloading all required data - self.refreshData() - } func vatomChanged(_ vatom: VatomModel) { self.vatom = vatom - if vatom.id != self.vatomObserverStore.rootVatomID { - // replace vAtom observer - printBV(info: "Image Layered: Vatom Changed. Replacing VatomObserverStore") - self.vatomObserverStore = VatomObserverStore(vatomID: vatom.id) - self.vatomObserverStore.refresh() - } - self.refreshUI() } /// Unload the face view (called when the VatomView must prepare for reuse). func unload() { self.baseLayer.image = nil - self.vatomObserverStore.cancel() } // MARK: - Refresh - /// Refresh the model layer (triggers a view layer update). - private func refreshData() { - self.vatomObserverStore.refresh(rootCompletion: nil) { _ in - self.refreshUI() - } - } - /// Refresh the view layer (does not refresh data layer). private func refreshUI() { self.loadBaseResource() @@ -297,27 +266,3 @@ class ImageLayeredFaceView: FaceView { } } - -extension ImageLayeredFaceView: VatomObserverStoreDelegate { - - func vatomObserver(_ observer: VatomObserverStore, rootVatomStateUpdated: VatomModel) { - // nothing to do - } - - func vatomObserver(_ observer: VatomObserverStore, childVatomStateUpdated: VatomModel) { - // nothing to do - } - - func vatomObserver(_ observer: VatomObserverStore, willAddChildVatom vatomID: String) { - // nothing to do - } - - func vatomObserver(_ observer: VatomObserverStore, didAddChildVatom childVatom: VatomModel) { - self.refreshUI() - } - - func vatomObserver(_ observer: VatomObserverStore, didRemoveChildVatom childVatom: VatomModel) { - self.refreshUI() - } - -} diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index 77f0cb58..1e42e6eb 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -45,15 +45,6 @@ class ImagePolicyFaceView: FaceView { /// struct immutalbe and is ONLY populated on init. private let config: Config - /* - NOTE - The `vatomChanged()` method called by `VatomView` does not handle child vatom updates. - The `VatomObserver` class is used to receive these events. This is required for the Child Count policy type. - */ - - /// Class responsible for observing changes related backing vAtom. - private var vatomObserver: VatomObserver - // MARK: - Initialization required init(vatom: VatomModel, faceModel: FaceModel) { @@ -67,11 +58,8 @@ class ImagePolicyFaceView: FaceView { self.config = Config() // default values } - // create an observer for the backing vatom - self.vatomObserver = VatomObserver(vatomID: vatom.id) super.init(vatom: vatom, faceModel: faceModel) - self.vatomObserver.delegate = self // add image view self.addSubview(animatedImageView) animatedImageView.frame = self.bounds @@ -107,14 +95,15 @@ class ImagePolicyFaceView: FaceView { /// Unload the face view (called when the VatomView must prepare for reuse). func unload() { - self.vatomObserver.cancel() + } // MARK: - Face Code /// Current count of child vAtoms. private var currentChildCount: Int { - return vatomObserver.childVatomIDs.count + // inspect cached children + return self.vatom.listCachedChildren().count } /// Updates the interface using local state. @@ -221,20 +210,6 @@ class ImagePolicyFaceView: FaceView { } -// MARK: - Vatom Observer Delegate - -extension ImagePolicyFaceView: VatomObserverDelegate { - - func vatomObserver(_ observer: VatomObserver, didAddChildVatom vatomID: String) { - self.updateUIDebounced() - } - - func vatomObserver(_ observer: VatomObserver, didRemoveChildVatom vatomID: String) { - self.updateUIDebounced() - } - -} - // MARK: - Image Policy /// Protocol image policies should conform to. diff --git a/BlockV/Face/Face Views/Utilities/VatomObserver.swift b/BlockV/Face/Face Views/Utilities/VatomObserver.swift deleted file mode 100644 index 0099a2ef..00000000 --- a/BlockV/Face/Face Views/Utilities/VatomObserver.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// - -import Foundation -import Signals - -protocol VatomObserverDelegate: class { - - /// Called after the observer has added a child vAtom (and it's state is available). - func vatomObserver(_ observer: VatomObserver, didAddChildVatom vatomID: String) - /// Called after the observer has removed a child vAtom. - func vatomObserver(_ observer: VatomObserver, didRemoveChildVatom vatomID: String) - -} - -//TODO: Add start and stop methods - -/// This class allows any user *owned* vAtom to be observed for state changes. This includes the adding and removing of -/// child vAtoms. -/// -/// On `init(vatomID:)`, this class will fetch the vAtoms remote state and then begin observing changes to the vAtom. -/// As such, soon after `init(vatomID:)` is called, the event closures may be exectured. -/// -/// This class provides a simple means of observing an *owned* vAtom and its immediate children. -/// -/// Handles fetching the root and child vatoms' state directly from the platform. Additionally, performs partial updates -/// using Web socket. -/// -/// Restrictions: -/// - Access level: Internal -/// - vAtom ownership: Owner only -/// -/// - Essentialy a wrapper around the Web socket and provides closure interfaces. This could also be a delegate... -/// - Only watches vAtom ids (does not store full vatoms - though that could be usefull). -/// - This class interfaces directly with the update stream. Note, you may receive state events before the internal -/// vatom management system (i.e. VatomInventory) has had a chance to update. -class VatomObserver { - - // MARK: - Properties - - /// Unique identifier of the root vAtom. - private(set) var rootVatomID: String - /// Set of unique identifiers of root's child vAtoms. - private(set) var childVatomIDs: Set = [] { - didSet { - let added = childVatomIDs.subtracting(oldValue) - let removed = oldValue.subtracting(childVatomIDs) - // notify delegate of changes - added.forEach { self.delegate?.vatomObserver(self, didAddChildVatom: $0) } - removed.forEach { self.delegate?.vatomObserver(self, didRemoveChildVatom: $0) } - } - } - - /// Delegate - weak var delegate: VatomObserverDelegate? - - // MARK: - Initializers - - /// Initialize using a vAtom ID. - init(vatomID: String) { - - self.rootVatomID = vatomID - - // move block out of init - DispatchQueue.main.async { - self.refresh() - } - - self.subscribeToUpdates() - - } - - // MARK: - Push State Update (Real-Time) - - /// Cancel observing the vAtom and it's children. - func cancel() { - // cancel observation updates - self.onConnected?.cancel() - self.onConnected = nil - self.onVatomStateUpdate?.cancel() - self.onVatomStateUpdate = nil - } - - private var onConnected: SignalSubscription? - private var onVatomStateUpdate: SignalSubscription? - - /// Subscribe to state updates - private func subscribeToUpdates() { - - // subscribe to web socket connect - self.onConnected = BLOCKv.socket.onConnected.subscribe(with: self) { [weak self] in - self?.refresh() - } - - // subscribe to state update - self.onVatomStateUpdate = BLOCKv.socket.onVatomStateUpdate.subscribe(with: self) { [weak self] stateUpdate in - - guard let `self` = self else { return } - - // - Parent ID Change - - // check for parent id changes - if let newParentID = stateUpdate.vatomProperties["vAtom::vAtomType"]?["parent_id"]?.stringValue { - - /* - Use the parent ID and the known list of children to determine if a child was added or removed. - */ - - if newParentID == self.rootVatomID { - // filter out duplicates - if !self.childVatomIDs.contains(stateUpdate.vatomId) { - // add - self.childVatomIDs.insert(stateUpdate.vatomId) - // notify delegate of imminent addition - self.delegate?.vatomObserver(self, didAddChildVatom: stateUpdate.vatomId) - } - } else { - /* - GOTCHA: - If for some reason, local children become out of sync with the remote there is no sensible way - for the client to know if a child vAtom was removed. For example, if the remote children are - [A, B, C] but locally the state is [A, B] and a "parent_id" change comes down for C. - - In this case, the removal of the child will not be noticied (since the child was "not" a local - child). To reduce the likelyhood of this, a remote state pull should be performed in cases - where the observer suspects remote-local sync issues, e.g. connetion drops. - */ - - // remove child id - if self.childVatomIDs.remove(stateUpdate.vatomId) != nil { - // notify delegate of removal - self.delegate?.vatomObserver(self, didRemoveChildVatom: stateUpdate.vatomId) - } - - } - - } - - } - - } - - // MARK: - Pull State Update - - /// Refresh root and child vatoms using remote state. - func refresh() { - self.updateChildVatoms() - } - - /// Replace all the root vAtom's direct children using remote state. - private func updateChildVatoms() { - - BLOCKv.getInventory(id: self.rootVatomID) { [weak self] result in - - switch result { - case .success(let vatoms): - // ensure correct parent ID - let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } - // replace the list of children - self?.childVatomIDs = Set(validChildren.map { $0.id }) - case .failure(let error): - printBV(error: "Unable to fetch children. Error: \(String(describing: error.localizedDescription))") - } - - } - - } - -} diff --git a/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift b/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift deleted file mode 100644 index cd07ab2a..00000000 --- a/BlockV/Face/Face Views/Utilities/VatomObserverStore.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// - -import Foundation -import Signals - -/// Aggregates updates from remote data pull and update stream push. -protocol VatomObserverStoreDelegate: class { - - /// Called when the root vAtom has experienced a property change. - func vatomObserver(_ observer: VatomObserverStore, rootVatomStateUpdated: VatomModel) - /// Called when a child vAtom has experienced a property change. - func vatomObserver(_ observer: VatomObserverStore, childVatomStateUpdated: VatomModel) - - /// Called when the observer is about to add a child vAtom. - /// - /// Use this event to be notified of an imminent child vAtom beign added to the root vAtom, e.g. beginning an - /// incomming animation. - /// - /// To know when the child's state is available, conform to: - /// `vatomObserver(:didAddChildVatom:)`. - func vatomObserver(_ observer: VatomObserverStore, willAddChildVatom vatomID: String) - /// Called after the observer has added a child vAtom (and it's state is available). - func vatomObserver(_ observer: VatomObserverStore, didAddChildVatom childVatom: VatomModel) - /// Called after the observer has removed a child vAtom. - func vatomObserver(_ observer: VatomObserverStore, didRemoveChildVatom childVatom: VatomModel) - -} - -/// This class provides a simple means of observing an *owned* vAtom and its immediate children. -/// -/// Handles fetching the root and child vatoms' state directly from the platform. Additionally, performs partial updates -/// using Web socket. -/// -/// Restrictions: -/// - Access level: Internal -/// - vAtom ownership: Owner only -class VatomObserverStore { - - // MARK: - Properties - - /// Unique identifier of the root vAtom. - public private(set) var rootVatomID: String - - public private(set) var rootVatom: VatomModel? - - /// Set of **direct** child vAtoms. - /// - /// Direct children are those vAtoms whose parent ID matches the root vAtom's ID. - public private(set) var childVatoms: Set = [] { - didSet { - let added = childVatoms.subtracting(oldValue) - let removed = oldValue.subtracting(childVatoms) - // notify delegate of changes - added.forEach { self.delegate?.vatomObserver(self, didAddChildVatom: $0) } - removed.forEach { self.delegate?.vatomObserver(self, didRemoveChildVatom: $0) } - } - } - - /// Delegate - weak var delegate: VatomObserverStoreDelegate? - - // MARK: - Initialization - - /// Initialize using a vAtom ID. - init(vatomID: String) { - self.rootVatomID = vatomID - self.subscribeToUpdates() - } - - // MARK: - Life Cycle - - /// Cancel observing the vAtom and it's children. - func cancel() { - // cancel observation updates - self.onConnected?.cancel() - self.onConnected = nil - self.onVatomStateUpdate?.cancel() - self.onVatomStateUpdate = nil - } - - private var onConnected: SignalSubscription? - private var onVatomStateUpdate: SignalSubscription? - - // MARK: - Push State Update (Real-Time) - - /// Subscribes to Web socket signals. - /// - /// - important: This method should only be called once. Signal subscription is additive. - private func subscribeToUpdates() { - - // subscribe to web socket connect - self.onConnected = BLOCKv.socket.onConnected.subscribe(with: self) { [weak self] in - self?.refresh() - } - - // subscribe to state update - self.onVatomStateUpdate = BLOCKv.socket.onVatomStateUpdate.subscribe(with: self) { [weak self] stateUpdate in - - guard let `self` = self else { return } - - // - Raw Update - - // check if root vatom - if stateUpdate.vatomId == self.rootVatomID, - let updatedVatom = self.rootVatom?.updated(applying: stateUpdate) { - self.delegate?.vatomObserver(self, rootVatomStateUpdated: updatedVatom) - } else { - // check if child vatom - if let childVatom = self.childVatoms.first(where: { $0.id == stateUpdate.vatomId }), - let updatedVatom = childVatom.updated(applying: stateUpdate) { - self.delegate?.vatomObserver(self, childVatomStateUpdated: updatedVatom) - } - } - - // - Parent ID Change - - // check for parent id changes - if let newParentID = stateUpdate.vatomProperties["vAtom::vAtomType"]?["parent_id"]?.stringValue { - - /* - Use the parent ID and the known list of children to determine if a child was added or removed. - */ - - if newParentID == self.rootVatomID { - // filter out duplicates - if !self.childVatoms.contains(where: { $0.id == stateUpdate.vatomId }) { - // notify delegate of imminent addition - self.delegate?.vatomObserver(self, willAddChildVatom: stateUpdate.vatomId) - // add child (async) - self.addChildVatom(withID: stateUpdate.vatomId) - } - } else { - // if the vatom's parentID is not the rootID and is in the list of children - if let index = self.childVatoms.firstIndex(where: { $0.id == stateUpdate.vatomId }) { - /* - GOTCHA: - If for some reason, local children become out of sync with the remote there is no sensible way - for the client to know if a child vAtom was removed. For example, if the remote children are - [A, B, C] but locally the state is [A, B] and a "parent_id" change comes down for C. - - In this case, the removal of the child will not be noticied (since the child was "not" a local - child). To reduce the likelyhood of this, a remote state pull should be performed in cases - where the observer suspects remote-local sync issues, e.g. connetion drops. - */ - - // remove child vatom - let removedVatom = self.childVatoms.remove(at: index) - // notify delegate of removal - self.delegate?.vatomObserver(self, didRemoveChildVatom: removedVatom) - } - } - - } - - } - - } - - // MARK: - Pull State Update - - typealias Completion = (Error?) -> Void - - /// Refresh root and child vatoms using remote state. - /// - /// - Parameters: - /// - rootCompletion: Completion handler that is called once the root vAtom has completed loading. - /// - childrenCompletion: Completion handler that is called once the children vAtoms has completed loading. - public func refresh(rootCompletion: Completion? = nil, childCompletion: Completion? = nil) { - // refresh - self.updateRootVatom(completion: rootCompletion) - self.updateChildVatoms(completion: childCompletion) - } - - /// Fetch root vAtom's remote state. - private func updateRootVatom(completion: Completion?) { - - BLOCKv.getVatoms(withIDs: [self.rootVatomID]) { [weak self] result in - - switch result { - case .success(let vatoms): - // ensure no error - guard let rootVatom = vatoms.first else { - return - } - // update root vAtom - self?.rootVatom = rootVatom - completion?(nil) - - case .failure(let error): - completion?(error) - } - - } - - } - - /// Fetch remote state for the specified child vAtom and adds it to the list of children. - private func addChildVatom(withID childID: String) { - - BLOCKv.getVatoms(withIDs: [childID]) { [weak self] result in - - switch result { - case .success(let vatoms): - - guard let childVatom = vatoms.first else { - return - } - - // ensure the vatom is still a child - // there is a case, due to the async arch, where the retrieved vAtom may no longer be a child - if childVatom.props.parentID == self?.rootVatomID { - // insert child (async) - self?.childVatoms.insert(childVatom) - } - case .failure(let error): - printBV(error: "Unable to add child. Error: \(String(describing: error.localizedDescription))") - } - - } - - } - - /// Replace all the root vAtom's direct children using remote state. - private func updateChildVatoms(completion: Completion?) { - - BLOCKv.getInventory(id: self.rootVatomID) { [weak self] result in - - switch result { - case .success(let vatoms): - // ensure correct parent ID - let validChildren = vatoms.filter { $0.props.parentID == self?.rootVatomID } - // replace the list of children - self?.childVatoms = Set(validChildren) - completion?(nil) - case .failure(let error): - completion?(error) - } - - } - - } - -} From c1bb4d02c1ec82eb2cbfcbbe7b1dd8a0f6227746 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 12:26:07 +0200 Subject: [PATCH 060/165] Add missing this.id parameter --- BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index 712b71e6..d3cf6233 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -124,6 +124,7 @@ extension VatomModel { completion: @escaping (Result<[String: Any], BVError>) -> Void) { let body = [ + "this.id": self.id, "geo.pos": [ "Lat": latitude, "Lon": longitude From 5a9b047b3f67f37b962b43827666e79304185057 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 13:31:53 +0200 Subject: [PATCH 061/165] Help complier with type --- BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index d3cf6233..a95a96b5 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -123,7 +123,7 @@ extension VatomModel { latitude: Double, completion: @escaping (Result<[String: Any], BVError>) -> Void) { - let body = [ + let body: [String: Any] = [ "this.id": self.id, "geo.pos": [ "Lat": latitude, From 559f321303fde2aa63dd93dc34f2e858ad7bf708 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Apr 2019 14:06:42 +0200 Subject: [PATCH 062/165] Update: Migrate to Swift 5.0 (#205) * Swift 5 migrator * Remove warnings for unused associated values * Remove Result in favour of std lib's Result * Disambiguate result type --- BLOCKv.podspec | 2 +- .../Data Pool/Helpers/Vatom+Containment.swift | 2 +- BlockV/Core/Extensions/Decodable+Ext.swift | 2 +- BlockV/Core/Helpers/Result.swift | 156 ------------------ BlockV/Core/Network/Stack/Client.swift | 8 +- .../Requests/BLOCKv+ActivityRequests.swift | 2 +- .../Core/Requests/BLOCKv+AuthRequests.swift | 2 +- .../Core/Requests/BLOCKv+UserRequests.swift | 6 +- .../Core/Requests/BLOCKv+VatomRequest.swift | 2 +- .../Image Layered/ImageLayeredFaceView.swift | 2 +- Example/BlockV.xcodeproj/project.pbxproj | 22 +-- .../xcschemes/BlockV-Example.xcscheme | 2 +- 12 files changed, 26 insertions(+), 182 deletions(-) delete mode 100644 BlockV/Core/Helpers/Result.swift diff --git a/BLOCKv.podspec b/BLOCKv.podspec index 8d95ab7a..e206bfa4 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/BLOCKvIO/ios-sdk.git', :tag => s.version.to_s } s.social_media_url = 'https://twitter.com/blockv_io' s.ios.deployment_target = '10.0' - s.swift_version = '4.2' + s.swift_version = '5.0' s.default_subspecs = 'Face' s.subspec 'Core' do |s| diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift index 08db246c..94a17d9f 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift @@ -25,7 +25,7 @@ extension VatomModel { /// - important: /// This method will inspect the 'inventory' and 'children' regions, if the regions are synchronized the vatoms /// are returned, if not, the regions are first synchronized. This means the method is potentially slow. - public func listChildren(completion: @escaping (Result<[VatomModel], BVError>) -> Void) { + public func listChildren(completion: @escaping (Swift.Result<[VatomModel], BVError>) -> Void) { // check if the vatom is in the owner's inventory region (and stabalized) DataPool.inventory().getStable(id: self.id).map { inventoryVatom -> Guarantee<[VatomModel]> in diff --git a/BlockV/Core/Extensions/Decodable+Ext.swift b/BlockV/Core/Extensions/Decodable+Ext.swift index fa2fe6e9..c93881ce 100644 --- a/BlockV/Core/Extensions/Decodable+Ext.swift +++ b/BlockV/Core/Extensions/Decodable+Ext.swift @@ -46,7 +46,7 @@ extension KeyedDecodingContainer { /// Returns `nil` if the key is absent, or if the type cannot be decoded func decodeSafelyIfPresent(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) -> T? { - let decoded = try? decodeIfPresent(Safe.self, forKey: key) + let decoded = ((try? decodeIfPresent(Safe.self, forKey: key)) as Safe??) return decoded??.value } diff --git a/BlockV/Core/Helpers/Result.swift b/BlockV/Core/Helpers/Result.swift deleted file mode 100644 index e0f824fe..00000000 --- a/BlockV/Core/Helpers/Result.swift +++ /dev/null @@ -1,156 +0,0 @@ -/* - TEMPORARY RESULT TYPE. ONCE SWIFT 5 IS RELEASED THIS CAN BE REMOVED. - */ - -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2018 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A value that represents either a success or a failure, including an -/// associated value in each case. -public enum Result { - /// A success, storing a `Success` value. - case success(Success) - - /// A failure, storing a `Failure` value. - case failure(Failure) - - /// Returns a new result, mapping any success value using the given - /// transformation. - /// - /// Use this method when you need to transform the value of a `Result` - /// instance when it represents a success. The following example transforms - /// the integer success value of a result into a string: - /// - /// func getNextInteger() -> Result { ... } - /// - /// let integerResult = getNextInteger() - /// // integerResult == .success(5) - /// let stringResult = integerResult.map({ String($0) }) - /// // stringResult == .success("5") - /// - /// - Parameter transform: A closure that takes the success value of this - /// instance. - /// - Returns: A `Result` instance with the result of evaluating `transform` - /// as the new success value if this instance represents a success. - public func map( - _ transform: (Success) -> NewSuccess - ) -> Result { - switch self { - case let .success(success): - return .success(transform(success)) - case let .failure(failure): - return .failure(failure) - } - } - - /// Returns a new result, mapping any failure value using the given - /// transformation. - /// - /// Use this method when you need to transform the value of a `Result` - /// instance when it represents a failure. The following example transforms - /// the error value of a result by wrapping it in a custom `Error` type: - /// - /// struct DatedError: Error { - /// var error: Error - /// var date: Date - /// - /// init(_ error: Error) { - /// self.error = error - /// self.date = Date() - /// } - /// } - /// - /// let result: Result = ... - /// // result == .failure() - /// let resultWithDatedError = result.mapError({ e in DatedError(e) }) - /// // result == .failure(DatedError(error: , date: )) - /// - /// - Parameter transform: A closure that takes the failure value of the - /// instance. - /// - Returns: A `Result` instance with the result of evaluating `transform` - /// as the new failure value if this instance represents a failure. - public func mapError( - _ transform: (Failure) -> NewFailure - ) -> Result { - switch self { - case let .success(success): - return .success(success) - case let .failure(failure): - return .failure(transform(failure)) - } - } - - /// Returns a new result, mapping any success value using the given - /// transformation and unwrapping the produced result. - /// - /// - Parameter transform: A closure that takes the success value of the - /// instance. - /// - Returns: A `Result` instance with the result of evaluating `transform` - /// as the new failure value if this instance represents a failure. - public func flatMap( - _ transform: (Success) -> Result - ) -> Result { - switch self { - case let .success(success): - return transform(success) - case let .failure(failure): - return .failure(failure) - } - } - - /// Returns a new result, mapping any failure value using the given - /// transformation and unwrapping the produced result. - /// - /// - Parameter transform: A closure that takes the failure value of the - /// instance. - /// - Returns: A `Result` instance, either from the closure or the previous - /// `.success`. - public func flatMapError( - _ transform: (Failure) -> Result - ) -> Result { - switch self { - case let .success(success): - return .success(success) - case let .failure(failure): - return transform(failure) - } - } - - /// Returns the success value as a throwing expression. - /// - /// Use this method to retrieve the value of this result if it represents a - /// success, or to catch the value if it represents a failure. - /// - /// let integerResult: Result = .success(5) - /// do { - /// let value = try integerResult.get() - /// print("The value is \(value).") - /// } catch error { - /// print("Error retrieving the value: \(error)") - /// } - /// // Prints "The value is 5." - /// - /// - Returns: The success value, if the instance represent a success. - /// - Throws: The failure value, if the instance represents a failure. - public func get() throws -> Success { - switch self { - case let .success(success): - return success - case let .failure(failure): - throw failure - } - } -} - -extension Result: Equatable where Success: Equatable, Failure: Equatable { } - -extension Result: Hashable where Success: Hashable, Failure: Hashable { } diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index e7e5e45f..38c4e33f 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -14,13 +14,13 @@ import Alamofire protocol ClientProtocol { - typealias RawCompletion = (Result) -> Void + typealias RawCompletion = (Swift.Result) -> Void /// Request that returns raw data. func request(_ endpoint: Endpoint, completion: @escaping RawCompletion) /// Request that returns native object (must conform to decodable). - func request(_ endpoint: Endpoint, completion: @escaping (Result) -> Void ) where T: Decodable + func request(_ endpoint: Endpoint, completion: @escaping (Swift.Result) -> Void ) where T: Decodable } @@ -153,7 +153,7 @@ final class Client: ClientProtocol { } /// JSON Completion handler. - typealias JSONCompletion = (Result) -> Void + typealias JSONCompletion = (Swift.Result) -> Void func requestJSON(_ endpoint: Endpoint, completion: @escaping JSONCompletion) { @@ -186,7 +186,7 @@ final class Client: ClientProtocol { /// - endpoint: Endpoint for the request /// - completion: The completion handler to call when the request is completed. func request(_ endpoint: Endpoint, - completion: @escaping (Result) -> Void ) where Response: Decodable { + completion: @escaping (Swift.Result) -> Void ) where Response: Decodable { // create request (starts immediately) let request = self.sessionManager.request( diff --git a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift index 9a910575..808dc45a 100644 --- a/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+ActivityRequests.swift @@ -99,7 +99,7 @@ extension BLOCKv { self.client.request(endpoint) { result in switch result { - case .success(let baseModel): + case .success: // model is available DispatchQueue.main.async { completion(nil) diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index d9f2f07a..295c8aac 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -182,7 +182,7 @@ extension BLOCKv { } switch result { - case .success(let model): + case .success: // model is available DispatchQueue.main.async { completion(nil) diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index f76f1fa8..babf9746 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -303,7 +303,7 @@ extension BLOCKv { self.client.request(endpoint) { result in switch result { - case .success(let baseModel): + case .success: // model is available DispatchQueue.main.async { completion(nil) @@ -338,7 +338,7 @@ extension BLOCKv { self.client.request(endpoint) { result in switch result { - case .success(let baseModel): + case .success: // model is available DispatchQueue.main.async { completion(nil) @@ -399,7 +399,7 @@ extension BLOCKv { self.client.request(endpoint) { result in switch result { - case .success(let baseModel): + case .success: // model is available DispatchQueue.main.async { completion(nil) diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index 841099bb..c370e377 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -113,7 +113,7 @@ extension BLOCKv { self.client.request(endpoint) { result in switch result { - case .success(let baseModel): + case .success: // model is available DispatchQueue.main.async { completion(nil) diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index e7b497e2..0bffdd83 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -221,7 +221,7 @@ class ImageLayeredFaceView: FaceView { }, completion: { _ in // remove it - if let index = self.childLayers.index(of: layer) { + if let index = self.childLayers.firstIndex(of: layer) { self.childLayers.remove(at: index) } layer.removeFromSuperview() diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index ce2fae81..89b7b239 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -453,12 +453,12 @@ 607FACCF1AFB9204008FA782 = { CreatedOnToolsVersion = 6.3.1; DevelopmentTeam = WA2R22UJPX; - LastSwiftMigration = 1010; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; D530759021119E0E00DE7FD0 = { CreatedOnToolsVersion = 9.4.1; - LastSwiftMigration = 1010; + LastSwiftMigration = 1020; ProvisioningStyle = Automatic; TestTargetID = 607FACCF1AFB9204008FA782; }; @@ -729,7 +729,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -777,7 +777,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Dev; @@ -801,7 +801,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.sdk.sample.${CONFIGURATION}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -824,7 +824,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.sdk.sample.${CONFIGURATION}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Dev; @@ -850,7 +850,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.BLOCKv-Unit-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BlockV_Example.app/BlockV_Example"; }; @@ -912,7 +912,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BlockV_Example.app/BlockV_Example"; VALIDATE_PRODUCT = YES; @@ -975,7 +975,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BlockV_Example.app/BlockV_Example"; VALIDATE_PRODUCT = YES; @@ -1026,7 +1026,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; VALIDATE_PRODUCT = YES; }; name = Prod; @@ -1049,7 +1049,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.sdk.sample.${CONFIGURATION}"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Prod; diff --git a/Example/BlockV.xcodeproj/xcshareddata/xcschemes/BlockV-Example.xcscheme b/Example/BlockV.xcodeproj/xcshareddata/xcschemes/BlockV-Example.xcscheme index 5feb9978..0fd3de7a 100644 --- a/Example/BlockV.xcodeproj/xcshareddata/xcschemes/BlockV-Example.xcscheme +++ b/Example/BlockV.xcodeproj/xcshareddata/xcschemes/BlockV-Example.xcscheme @@ -1,6 +1,6 @@ Date: Wed, 17 Apr 2019 12:06:37 +0200 Subject: [PATCH 063/165] Move completion handling to main queue --- BlockV/Core/Requests/BLOCKv+VatomRequest.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index c370e377..d07f105a 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -170,13 +170,16 @@ extension BLOCKv { // roll back only those failed containments let undosToRollback = undos.filter { !updateVatomModel.ids.contains($0.id) } undosToRollback.forEach { $0.undo() } - // complete - completion(.success(updateVatomModel)) + DispatchQueue.main.async { + completion(.success(updateVatomModel)) + } case .failure(let error): // roll back all containments undos.forEach { $0.undo() } - completion(.failure(error)) + DispatchQueue.main.async { + completion(.failure(error)) + } } } From cd5c73f0d784817d8a69f285ccac0a2eef089e27 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 12:22:55 +0200 Subject: [PATCH 064/165] Remove debouncer --- BlockV/Face/Extensions/Debouncer.swift | 79 ++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/BlockV/Face/Extensions/Debouncer.swift b/BlockV/Face/Extensions/Debouncer.swift index ee494f91..1cf07608 100644 --- a/BlockV/Face/Extensions/Debouncer.swift +++ b/BlockV/Face/Extensions/Debouncer.swift @@ -24,12 +24,12 @@ import Foundation /* -- https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function -- http://demo.nimius.net/debounce_throttle/ -*/ + - https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function + - http://demo.nimius.net/debounce_throttle/ + */ extension TimeInterval { - + /** Checks if the current time has passed the reference date `since` plus some delay `self`. @@ -48,7 +48,7 @@ extension TimeInterval { func hasPassed(since: TimeInterval) -> Bool { return Date().timeIntervalSinceReferenceDate - self > since } - + } /** @@ -112,3 +112,72 @@ func debounce(delay: DispatchTimeInterval, queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) } } + +/** + Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. + + - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. + - Parameter queue: The queue to perform the action on. Defaults to the main queue. + - Parameter action: A function to throttle. + + - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. + */ +func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { + var currentWorkItem: DispatchWorkItem? + var lastFire: TimeInterval = 0 + return { + guard currentWorkItem == nil else { return } + currentWorkItem = DispatchWorkItem { + action() + lastFire = Date().timeIntervalSinceReferenceDate + currentWorkItem = nil + } + delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) + } +} + +/** + Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. + + Accepts an `action` with one argument. + - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. + - Parameter queue: The queue to perform the action on. Defaults to the main queue. + - Parameter action: A function to throttle. Can accept one argument. + - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. + */ +func throttle1(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T) -> Void)) -> (T) -> Void { + var currentWorkItem: DispatchWorkItem? + var lastFire: TimeInterval = 0 + return { (p1: T) in + guard currentWorkItem == nil else { return } + currentWorkItem = DispatchWorkItem { + action(p1) + lastFire = Date().timeIntervalSinceReferenceDate + currentWorkItem = nil + } + delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) + } +} + +/** + Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. + Accepts an `action` with two arguments. + - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. + - Parameter queue: The queue to perform the action on. Defaults to the main queue. + - Parameter action: A function to throttle. Can accept two arguments. + - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. + */ + +func throttle2(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T, U) -> Void)) -> (T, U) -> Void { + var currentWorkItem: DispatchWorkItem? + var lastFire: TimeInterval = 0 + return { (p1: T, p2: U) in + guard currentWorkItem == nil else { return } + currentWorkItem = DispatchWorkItem { + action(p1, p2) + lastFire = Date().timeIntervalSinceReferenceDate + currentWorkItem = nil + } + delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) + } +} From a7c986f3a09e9733e0023424af8e75ead868b0a5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 13:15:53 +0200 Subject: [PATCH 065/165] Convert load from a taking a closure to taking no arguments. Instead, the FaceViewDelegate's didLoad method will be called. Improve documentation. --- BlockV/Face/Face Views/FaceView.swift | 51 ++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/BlockV/Face/Face Views/FaceView.swift b/BlockV/Face/Face Views/FaceView.swift index b2bf3775..9f5f91fb 100644 --- a/BlockV/Face/Face Views/FaceView.swift +++ b/BlockV/Face/Face Views/FaceView.swift @@ -12,16 +12,17 @@ import Foundation /* - Face Views should work for both owned and unowned vAtoms. + When implementing a Face View, it is worth considering how it will work for should work for both owned and unowned + (public) vAtoms. */ -/// FIXME: This operator is useful, but has a drawback in that it always makes an assignment. +/// TODO: This operator is useful, but has a drawback in that it always makes an assignment. infix operator ?= internal func ?= (lhs: inout T, rhs: T?) { lhs = rhs ?? lhs } -/// Composite type that all face views must derive from and conform. +/// Composite type that all face views must derive from and conform to. /// /// A face view is responsile for rendering a single face of a vAtom. public typealias FaceView = BaseFaceView & FaceViewLifecycle & FaceViewIdentifiable @@ -40,23 +41,46 @@ public protocol FaceViewIdentifiable { /// The protocol that face views must adopt to receive lifecycle events. public protocol FaceViewLifecycle: class { - /// Boolean value indicating whether the face view has loaded. + /// Boolean value indicating whether the face view has loaded. After load has completed that boolean must be + /// updated. var isLoaded: Bool { get } /// Called to initiate the loading of the face view. /// + /// All content and state should be reset on calling load. + /// /// - important: - /// This method is only called once per lifecyle. + /// This method will only be called once by the Vatom View Life Cycle (VVLC). This is the trigger for the face view + /// to gathering necessary resources and lay out its content. /// - /// Face views should call the completion handler at once the face view has displayable content. Displayable content - /// means a *minimum first view*. The face view may continue loading content after calling the completion handler. - func load(completion: ((Error?) -> Void)?) - + /// Face views should call the `FaceViewDelegate`'s `faceView(didLoad:)` method once the face view has completed + /// loading or encoutered and error during. + func load() + + /* + # NOTE + + The face model is set only on init. Vatom by comparison is may be updated over the lifetime of the face view. + This means if a change has been made to the template's actions and faces (after init) those changes will not + be incorporated. + + Perhaps vatomChanged(_ vatom: VatomModel) should be updated to accept the face model for the rare case it has + changed? That said, modifiying the faces after init is bad proactice. + */ + /// Called to inform the face view the specified vAtom should be rendered. /// - /// Face views should respond to this method by refreshing their content. + /// Face views should respond to this method by refreshing their content. Typically, this is achieved by internally + /// calling the load method. + /// + /// Animating changes: + /// If the vatom id has not changed, consider animating the state change. /// /// - important: + /// This method may be called multiple times by the Vatom View Life Cycle (VVLC). It is important to reset the + /// contents of the face view when so that the new state of the Vatom can be shown. + /// + /// - note: /// This method does not guarantee the same vAtom will be passed in. Rather, it guarantees that the vatom passed /// in will, at minimum, share the same template variation. This is typically encountered when VatomView is used /// inside a reuse pool such as those found in `UICollectionView`. @@ -66,13 +90,13 @@ public protocol FaceViewLifecycle: class { /// vAtom's root or private section. VatomView passes these updates on to the face view. func vatomChanged(_ vatom: VatomModel) - /// Called when the face view is no longer being displayed. + /// Called to reset the content of the face view. /// /// - important: /// This event may be called multiple times. /// /// The face view should perform a clean up operation, e.g. cancel all downloads, remove any listers, nil out any - /// references. Typical use cases include: 1. entering a reuse pool or 2. preparing for deallocation. + /// references. Call unload before dealloc. func unload() } @@ -90,6 +114,9 @@ open class BaseFaceView: UIView { weak var delegate: FaceViewDelegate? /// Initializes a BaseFaceView using a vAtom and a face model. + /// + /// Use the initializer purely to configure the face view. Use the `load` lifecycle method to begin heavy operations + /// to update the state of the face view, e.g. downloading resources. public required init(vatom: VatomModel, faceModel: FaceModel) { self.vatom = vatom self.faceModel = faceModel From b9a381b5aeac6c4510198bf25e7f8261245a01c0 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 13:16:48 +0200 Subject: [PATCH 066/165] Adopt standard pattern --- .../Image Layered/ImageLayeredFaceView.swift | 85 ++++++++----- .../Image Policy/ImagePolicyFaceView.swift | 69 ++++++---- .../ImageProgressFaceView.swift | 120 ++++++++++++------ .../Face/Face Views/Image/ImageFaceView.swift | 100 +++++++++++---- 4 files changed, 244 insertions(+), 130 deletions(-) diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 0bffdd83..3bcca445 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -100,41 +100,54 @@ class ImageLayeredFaceView: FaceView { // MARK: - Face View Lifecycle - /// Holds the completion to call when the face view has completed loading. - private var loadCompletion: ((Error?) -> Void)? - - /// Begin loading the face view's content. - func load(completion: ((Error?) -> Void)?) { - - // assign a single load completion closure - loadCompletion = { (error) in - completion?(error) + /// Begins loading the face view's content. + func load() { + + // reset content + self.reset() + // load required resources + self.loadResources { [weak self] error in + + guard let self = self else { return } + // update state and inform delegate of load completion + if let error = error { + self.isLoaded = false + self.updateLayers() + self.delegate?.faceView(self, didLoad: error) + } else { + self.isLoaded = true + self.updateLayers() + self.delegate?.faceView(self, didLoad: nil) + } } - /* - Business logic: - This face is considered to be 'loaded' once the base image has been downloaded and loaded into the view. - */ - self.loadBaseResource() } + /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - - self.vatom = vatom - self.refreshUI() + + if self.vatom.id == vatom.id { + // replace vatom, update UI + self.vatom = vatom + self.updateLayers() + } else { + // replace vatom, reset and update UI + self.vatom = vatom + self.reset() + self.updateLayers() + } + } + + /// Resets the contents of the face view. + private func reset() { + self.baseLayer.image = nil + self.removeAllLayers() } /// Unload the face view (called when the VatomView must prepare for reuse). func unload() { - self.baseLayer.image = nil - } - - // MARK: - Refresh - - /// Refresh the view layer (does not refresh data layer). - private func refreshUI() { - self.loadBaseResource() - self.updateLayers() + self.reset() + //TODO: Cancel downloads } // MARK: - Layer Management @@ -231,6 +244,12 @@ class ImageLayeredFaceView: FaceView { timeOffset += 0.2 } } + + /// Remove all child layers without animation. + private func removeAllLayers() { + childLayers.forEach { $0.removeFromSuperview() } + childLayers = [] + } // MARK: - Resources @@ -238,11 +257,11 @@ class ImageLayeredFaceView: FaceView { /// /// Calls the `loadCompletion` closure asynchronously. Note: the mechanics of `loadImage(with:into:)` mean only the /// latest completion handler will be executed since all previous tasks are cancelled. - private func loadBaseResource() { + private func loadResources(completion: @escaping (Error?) -> Void) { // extract resource model guard let resourceModel = vatom.props.resources.first(where: { $0.name == config.imageName }) else { - loadCompletion?(FaceError.missingVatomResource) + completion(FaceError.missingVatomResource) return } @@ -253,14 +272,14 @@ class ImageLayeredFaceView: FaceView { var request = ImageRequest(url: encodeURL) // use unencoded url as cache key request.cacheKey = resourceModel.url - // load image (automatically handles reuse) - // GOTCHA: Upon calling load, previous requests are cancelled allong with their completion handlers. - Nuke.loadImage(with: request, into: self.baseLayer) { (_, error) in + + // load image (auto cancel previous) + Nuke.loadImage(with: request, into: self.baseLayer) { (response, error) in self.isLoaded = true - self.loadCompletion?(error) + completion(error) } } catch { - loadCompletion?(error) + completion(error) } } diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index 1e42e6eb..4dcf0710 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -72,30 +72,55 @@ class ImagePolicyFaceView: FaceView { // MARK: - Face View Lifecycle - /// Begin loading the face view's content. - func load(completion: ((Error?) -> Void)?) { - // update resources - updateUI(completion: completion) + /// Begins loading the face view's content. + func load() { + // reset content + self.reset() + // update state + self.updateUI { [weak self] error in + + guard let self = self else { return } + // inform delegate of load completion + if let error = error { + self.isLoaded = false + self.delegate?.faceView(self, didLoad: error) + } else { + self.isLoaded = true + self.delegate?.faceView(self, didLoad: nil) + } + } + } - /// Respond to updates or replacement of the current vAtom. + /// Updates the backing Vatom and loads the new state. + /// + /// The VVLC ensures the vatom will share the same template variation. This means the vatom will have the same + /// resources but the state of the face (e.g. which recsources it is showing) may be different. func vatomChanged(_ vatom: VatomModel) { - /* - NOTE: - - Changes to to the backing vAtom must be reflected in the face view. - - Specifically, updates to the backing vAtom or it's children may afect the required image policy image. - */ - - // replace current vatom - self.vatom = vatom - self.updateUIDebounced() + if self.vatom.id == vatom.id { + // replace vatom, update UI + self.vatom = vatom + self.updateUI(completion: nil) + } else { + // replace vatom, reset and update UI + self.vatom = vatom + self.reset() + self.updateUI(completion: nil) + } } + + /// Resets the contents of the face view. + private func reset() { + self.animatedImageView.image = nil + self.animatedImageView.animatedImage = nil + } - /// Unload the face view (called when the VatomView must prepare for reuse). + /// Unload face view. Reset all content. func unload() { - + self.reset() + //TODO: Cancel all downloads } // MARK: - Face Code @@ -115,18 +140,6 @@ class ImagePolicyFaceView: FaceView { self.updateImageView(withResource: resourceName, completion: completion) } - /// Updates the interface using local state (debounced). - /// - /// This property debounces the UI update. This avoids the case where numerous parent ID state changes could cause - /// unecessary resource downloads. Essentially, it ensure the the vAtom is "settled" before updating the UI. - private lazy var updateUIDebounced = { - // NOTE: Debounce will cancel work items. - return debounce(delay: DispatchTimeInterval.milliseconds(500)) { - self.updateUI(completion: nil) - //printBV(info: "Debounced: updateUI called") - } - }() - /// Update the face view using *local* data. /// /// Do not call directly. Rather call `debouncedUpdateUI()`. diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 0fc88dff..37a7e588 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -139,24 +139,59 @@ class ImageProgressFaceView: FaceView { // MARK: - FaceView Lifecycle - func load(completion: ((Error?) -> Void)?) { - - self.updateResources { (error) in - self.setNeedsLayout() - self.updateUI() - completion?(error) + /// Begins loading the face view's content. + func load() { + + // reset content + self.reset() + /// load required resources + self.loadResources { [weak self] error in + + guard let self = self else { return } + // update state and inform delegate of load completion + if let error = error { + self.setNeedsLayout() + self.updateUI() + self.isLoaded = false + self.delegate?.faceView(self, didLoad: error) + } else { + self.setNeedsLayout() + self.updateUI() + self.isLoaded = true + self.delegate?.faceView(self, didLoad: nil) + } + } } + /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - // update vatom - self.vatom = vatom - updateUI() + if self.vatom.id == vatom.id { + // replace vatom, update UI + self.vatom = vatom + self.updateUI() + } else { + // replace vatom, reset and load + self.vatom = vatom + self.reset() + self.setNeedsLayout() + self.updateUI() + } + + } + + /// Resets the contents of the face view. + private func reset() { + emptyImageView.image = nil + fullImageView.image = nil } - func unload() { } + func unload() { + reset() + //TODO: Cancel all downloads + } // MARK: - View Lifecycle @@ -239,48 +274,49 @@ class ImageProgressFaceView: FaceView { /// Fetches required resources and populates the relevant `ImageView`s. The completion handler is called once all /// images are downloaded (or an error is encountered). - private func updateResources(completion: ((Error?) -> Void)?) { + private func loadResources(completion: @escaping (Error?) -> Void) { // ensure required resources are present guard let emptyImageResource = vatom.props.resources.first(where: { $0.name == self.config.emptyImageName }), let fullImageResource = vatom.props.resources.first(where: { $0.name == self.config.fullImageName }) else { - printBV(error: "\(#file) - failed to extract resources.") + completion(FaceError.missingVatomResource) return } // ensure encoding passes - guard - let encodedEmptyURL = try? BLOCKv.encodeURL(emptyImageResource.url), - let encodedFullURL = try? BLOCKv.encodeURL(fullImageResource.url) - else { - printBV(error: "\(#file) - failed to encode resources.") - return - } - - dispatchGroup.enter() - dispatchGroup.enter() - - var requestEmpty = ImageRequest(url: encodedEmptyURL) - // use unencoded url as cache key - requestEmpty.cacheKey = emptyImageResource.url - // load image (automatically handles reuse) - Nuke.loadImage(with: requestEmpty, into: self.emptyImageView) { (_, _) in - self.dispatchGroup.leave() - } - - var requestFull = ImageRequest(url: encodedFullURL) - // use unencoded url as cache key - requestFull.cacheKey = fullImageResource.url - // load image (automatically handles reuse) - Nuke.loadImage(with: requestFull, into: self.fullImageView) { (_, _) in - self.dispatchGroup.leave() - } - - dispatchGroup.notify(queue: .main) { - self.isLoaded = true - completion?(nil) + do { + + let encodedEmptyURL = try BLOCKv.encodeURL(emptyImageResource.url) + let encodedFullURL = try BLOCKv.encodeURL(fullImageResource.url) + + dispatchGroup.enter() + dispatchGroup.enter() + + var requestEmpty = ImageRequest(url: encodedEmptyURL) + // use unencoded url as cache key + requestEmpty.cacheKey = emptyImageResource.url + // load image (automatically handles reuse) + Nuke.loadImage(with: requestEmpty, into: self.emptyImageView) { (_, _) in + self.dispatchGroup.leave() + } + + var requestFull = ImageRequest(url: encodedFullURL) + // use unencoded url as cache key + requestFull.cacheKey = fullImageResource.url + // load image (automatically handles reuse) + Nuke.loadImage(with: requestFull, into: self.fullImageView) { (_, _) in + self.dispatchGroup.leave() + } + + dispatchGroup.notify(queue: .main) { + self.isLoaded = true + completion(nil) + } + + } catch { + completion(error) } } diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index f854f51d..b5b81cb0 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -123,12 +123,40 @@ class ImageFaceView: FaceView { // MARK: - Face View Lifecycle - /// Begin loading the face view's content. - func load(completion: ((Error?) -> Void)?) { - updateResources(completion: completion) + /// Begins loading the face view's content. + func load() { + + /* + # Pattern + 1. Call `reset` (which sets `isLoaded` to false) + >>> reset content, cancel downloads + 2. Update face state + >>> set `isLoaded` to true + >>> call the delegate + */ + + // reset content + self.reset() + // load required resources + self.loadResources { [weak self] error in + + guard let self = self else { return } + // update state and inform delegate of load completion + if let error = error { + self.isLoaded = false + self.delegate?.faceView(self, didLoad: error) + } else { + self.isLoaded = true + self.delegate?.faceView(self, didLoad: nil) + } + + } } - /// Respond to updates or replacement of the current vAtom. + /// Updates the backing Vatom and loads the new state. + /// + /// The VVLC ensures the vatom will share the same template variation. This means the vatom will have the same + /// resources but the state of the face (e.g. which recsources it is showing) may be different. func vatomChanged(_ vatom: VatomModel) { /* @@ -138,46 +166,64 @@ class ImageFaceView: FaceView { - Thus, no meaningful UI update can be made. */ + // reset content + self.reset() // replace current vatom self.vatom = vatom - updateResources(completion: nil) + self.load() } + + /// Resets the contents of the face view. + private func reset() { + self.animatedImageView.image = nil + self.animatedImageView.animatedImage = nil + } /// Unload the face view. /// - /// Also called before reuse (when used inside a reuse pool). + /// Unload should reset the face view contents *and* stop any expensive operations e.g. downloading resources. func unload() { - self.animatedImageView.image = nil - self.animatedImageView.animatedImage = nil + reset() + //TODO: Cancel resource downloading } // MARK: - Resources - private func updateResources(completion: ((Error?) -> Void)?) { + private func loadResources(completion: @escaping (Error?) -> Void) { // extract resource model guard let resourceModel = vatom.props.resources.first(where: { $0.name == config.imageName }) else { + completion(FaceError.missingVatomResource) return } - - // encode url - guard let encodeURL = try? BLOCKv.encodeURL(resourceModel.url) else { - return - } - - //FIXME: Where should this go? - ImagePipeline.Configuration.isAnimatedImageDataEnabled = true - - //TODO: Should the size of the VatomView be factoring in and the image be resized? - - var request = ImageRequest(url: encodeURL) - // use unencoded url as cache key - request.cacheKey = resourceModel.url - // load image (automatically handles reuse) - Nuke.loadImage(with: request, into: self.animatedImageView) { (_, error) in - self.isLoaded = true - completion?(error) + + do { + // encode url + let encodeURL = try BLOCKv.encodeURL(resourceModel.url) + + //FIXME: Where should this go? + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true + + //TODO: Should the size of the VatomView be factoring in and the image be resized? + + var request = ImageRequest(url: encodeURL) + // use unencoded url as cache key + request.cacheKey = resourceModel.url + + /* + Nuke's `loadImage` cancels any exisitng requests and nils out the old image. This takes care of the reuse-pool + use case where the same face view is used to display a vatom of the same template variation. + */ + + // load image (auto cancel previous) + Nuke.loadImage(with: request, into: self.animatedImageView) { (_, error) in + self.isLoaded = true + completion(error) + } + + } catch { + completion(error) } } From 96e5728ae4134a5b2bf796cffcd111a79c06907c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 13:16:58 +0200 Subject: [PATCH 067/165] Adopt standard pattern --- BlockV/Face/Face Views/Web/WebFaceView.swift | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index 5abf4f94..de6d79a8 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -80,17 +80,13 @@ class WebFaceView: FaceView { var isLoaded: Bool = false - var timer: Timer? - - /// Holds the completion handler. - private var completion: ((Error?) -> Void)? - - func load(completion: ((Error?) -> Void)?) { - // store the completion - self.completion = completion + /// Begins loading the face view's content. + func load() { + self.loadFace() } + /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { // if the vatom has changed, load the face url again if vatom.id != self.vatom.id { @@ -102,6 +98,11 @@ class WebFaceView: FaceView { let children = self.vatom.listCachedChildren() self.coreBridge?.sendVatomChildren(children) } + + /// Resets the contents of the face view. + private func reset() { + + } func unload() { self.webView.stopLoading() @@ -126,7 +127,8 @@ class WebFaceView: FaceView { extension WebFaceView: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - self.completion?(nil) + // inform delegate of load completion + self.delegate?.faceView(self, didLoad: nil) } func webView(_ webView: WKWebView, From e53ee3b28ba131dc0def9161e9be8afc54b7bbe5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 13:17:56 +0200 Subject: [PATCH 068/165] Improve documentation Move FaceViewDelegate conformance to VatomView --- .../Face/Vatom View/FaceMessageDelegate.swift | 39 ++++++------- BlockV/Face/Vatom View/VatomView.swift | 56 +++++++++++++------ 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/BlockV/Face/Vatom View/FaceMessageDelegate.swift b/BlockV/Face/Vatom View/FaceMessageDelegate.swift index b848a94a..3646e9b3 100644 --- a/BlockV/Face/Vatom View/FaceMessageDelegate.swift +++ b/BlockV/Face/Vatom View/FaceMessageDelegate.swift @@ -111,11 +111,27 @@ public struct FaceMessageError: Error { /// The protocol face views must conform to in order to communicate protocol FaceViewDelegate: class { - + + /* + Is this delegate method required? + OPtions: + 1. Callback in load + 2. Delegate methods informing the owner of vatom view that the load completed. + + 1. Have promblems cause it's hard to + */ + + /// Called when the face view completes loading. + /// + /// - Parameters: + /// - faceView: The face view from which this message was received. + /// - result: Outcome of the face view load operation. + func faceView(_ faceView: FaceView, didLoad outcome: Error?) + /// Called when the vatom view receives a message from the face. /// /// - Parameters: - /// - vatomView: The `VatomView` instance which the face message was received from. + /// - faceView: The face view from which this message was received. /// - message: The unique identifier of the message. /// - object: Companion object which addtional information relevant to the request. /// - completion: The completion handler to call once the request has been processed. @@ -125,22 +141,3 @@ protocol FaceViewDelegate: class { completion: ((Result) -> Void)?) } - -/// Extend VatomView to conform to `FaceViewDelegate`. -/// -/// This is the conduit of communication between the VatomView and it's Face View. -extension VatomView: FaceViewDelegate { - - func faceView(_ faceView: FaceView, - didSendMessage message: String, - withObject object: [String: JSON], - completion: ((Result) -> Void)?) { - - // forward the message to the vatom view delegate - self.vatomViewDelegate?.vatomView(self, - didRecevieFaceMessage: message, - withObject: object, - completion: completion) - } - -} diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 7fed36f3..c540f153 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -10,6 +10,7 @@ // import Foundation +import GenericJSON // MARK: - Protocols @@ -448,30 +449,49 @@ open class VatomView: UIView { self.insertSubview(newFaceView, at: 0) // 1. instruct face view to load its content (must be able to handle being called multiple times). - newFaceView.load { [weak self] (error) in + newFaceView.load() - ///printBV(info: "Face view load completion called.") - - guard let self = self else { return } - - // Error - Case 2 - Display error view if the face view encounters an error during its load operation. + } - // ensure no error - guard error == nil else { - // face view encountered an error - self.selectedFaceView?.removeFromSuperview() - self.state = .error - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) - return - } +} +/// Extend VatomView to conform to `FaceViewDelegate`. +/// +/// This is the conduit of communication between the VatomView and it's Face View. +extension VatomView: FaceViewDelegate { + + func faceView(_ faceView: FaceView, + didSendMessage message: String, + withObject object: [String: JSON], + completion: ((Result) -> Void)?) { + + // forward the message to the vatom view delegate + self.vatomViewDelegate?.vatomView(self, + didRecevieFaceMessage: message, + withObject: object, + completion: completion) + } + + func faceView(_ faceView: FaceView, didLoad error: Error?) { + + // forward the result to the vatom view delegate + if let error = error { + + // face view encountered an error + self.selectedFaceView?.removeFromSuperview() + self.state = .error + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) + + // inform delegate + //TODO: Error information is lost here. Does the face view need a specific error. + let vvlcError = VVLCError.faceViewLoadFailed + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: Result.failure(vvlcError)) + } else { // show face self.state = .completed // inform delegate - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(self.selectedFaceView!)) - + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(faceView)) } - } - + } From 354405a0e0d5804e29e8ba42070e9d74e95b7d22 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 13:25:27 +0200 Subject: [PATCH 069/165] Remove debouncer --- BlockV/Face/Extensions/Debouncer.swift | 183 ------------------------- 1 file changed, 183 deletions(-) delete mode 100644 BlockV/Face/Extensions/Debouncer.swift diff --git a/BlockV/Face/Extensions/Debouncer.swift b/BlockV/Face/Extensions/Debouncer.swift deleted file mode 100644 index 1cf07608..00000000 --- a/BlockV/Face/Extensions/Debouncer.swift +++ /dev/null @@ -1,183 +0,0 @@ -// MIT License -// -// Copyright (c) Simon Ljungberg -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Foundation - -/* - - https://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function - - http://demo.nimius.net/debounce_throttle/ - */ - -extension TimeInterval { - - /** - Checks if the current time has passed the reference date `since` plus some delay `self`. - - ``` - let lastRunDate = Date().timeIntervalSinceReferenceDate() - let delay: TimeInterval = 3 - - if delay.hasPassed(since: lastRunDate) { - // at least 3 seconds has passed - } - - ``` - - - Parameter since: The reference date from which - */ - func hasPassed(since: TimeInterval) -> Bool { - return Date().timeIntervalSinceReferenceDate - self > since - } - -} - -/** - Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this - function being called. - - - Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to debounce. Can't accept any arguments. - - - Returns: A new function that will only call `action` if `delay` time passes between invocations. - */ -func debounce(delay: DispatchTimeInterval, - queue: DispatchQueue = .main, - action: @escaping (() -> Void)) -> () -> Void { - var currentWorkItem: DispatchWorkItem? - return { - currentWorkItem?.cancel() - currentWorkItem = DispatchWorkItem { action() } - queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} - -/** - Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this - function being called. - - Accepsts an `action` with one argument. - - Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to debounce. Can accept one argument. - - Returns: A new function that will only call `action` if `delay` time passes between invocations. - */ -func debounce(delay: DispatchTimeInterval, - queue: DispatchQueue = .main, - action: @escaping ((T) -> Void)) -> (T) -> Void { - var currentWorkItem: DispatchWorkItem? - return { (param1: T) in - currentWorkItem?.cancel() - currentWorkItem = DispatchWorkItem { action(param1) } - queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} - -/** - Wraps a function in a new function that will only execute the wrapped function if `delay` has passed without this - function being called. - Accepsts an `action` with two arguments. - - Parameter delay: A `DispatchTimeInterval` to wait before executing the wrapped function after last invocation. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to debounce. Can accept two arguments. - - Returns: A new function that will only call `action` if `delay` time passes between invocations. - */ -func debounce(delay: DispatchTimeInterval, - queue: DispatchQueue = .main, - action: @escaping ((T, U) -> Void)) -> (T, U) -> Void { - var currentWorkItem: DispatchWorkItem? - return { (param1: T, param2: U) in - currentWorkItem?.cancel() - currentWorkItem = DispatchWorkItem { action(param1, param2) } - queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} - -/** - Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. - - - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to throttle. - - - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. - */ -func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { - var currentWorkItem: DispatchWorkItem? - var lastFire: TimeInterval = 0 - return { - guard currentWorkItem == nil else { return } - currentWorkItem = DispatchWorkItem { - action() - lastFire = Date().timeIntervalSinceReferenceDate - currentWorkItem = nil - } - delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} - -/** - Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. - - Accepts an `action` with one argument. - - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to throttle. Can accept one argument. - - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. - */ -func throttle1(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T) -> Void)) -> (T) -> Void { - var currentWorkItem: DispatchWorkItem? - var lastFire: TimeInterval = 0 - return { (p1: T) in - guard currentWorkItem == nil else { return } - currentWorkItem = DispatchWorkItem { - action(p1) - lastFire = Date().timeIntervalSinceReferenceDate - currentWorkItem = nil - } - delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} - -/** - Wraps a function in a new function that will throttle the execution to once in every `delay` seconds. - Accepts an `action` with two arguments. - - Parameter delay: A `TimeInterval` specifying the number of seconds that needst to pass between each execution of `action`. - - Parameter queue: The queue to perform the action on. Defaults to the main queue. - - Parameter action: A function to throttle. Can accept two arguments. - - Returns: A new function that will only call `action` once every `delay` seconds, regardless of how often it is called. - */ - -func throttle2(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping ((T, U) -> Void)) -> (T, U) -> Void { - var currentWorkItem: DispatchWorkItem? - var lastFire: TimeInterval = 0 - return { (p1: T, p2: U) in - guard currentWorkItem == nil else { return } - currentWorkItem = DispatchWorkItem { - action(p1, p2) - lastFire = Date().timeIntervalSinceReferenceDate - currentWorkItem = nil - } - delay.hasPassed(since: lastFire) ? queue.async(execute: currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: currentWorkItem!) - } -} From 75b9c47321786003473b1ab1554ce2ccf64ec57e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 14:19:24 +0200 Subject: [PATCH 070/165] Update load completion --- BlockV/Face/Face Views/FaceView.swift | 13 ++-- .../Image Layered/ImageLayeredFaceView.swift | 18 +++--- .../Image Policy/ImagePolicyFaceView.swift | 12 ++-- .../ImageProgressFaceView.swift | 26 ++++---- .../Face/Face Views/Image/ImageFaceView.swift | 41 +++++++------ BlockV/Face/Face Views/Web/WebFaceView.swift | 13 ++-- .../Face/Vatom View/FaceMessageDelegate.swift | 20 +------ BlockV/Face/Vatom View/VatomView.swift | 60 +++++++++---------- 8 files changed, 94 insertions(+), 109 deletions(-) diff --git a/BlockV/Face/Face Views/FaceView.swift b/BlockV/Face/Face Views/FaceView.swift index 9f5f91fb..6fe13844 100644 --- a/BlockV/Face/Face Views/FaceView.swift +++ b/BlockV/Face/Face Views/FaceView.swift @@ -50,12 +50,11 @@ public protocol FaceViewLifecycle: class { /// All content and state should be reset on calling load. /// /// - important: - /// This method will only be called once by the Vatom View Life Cycle (VVLC). This is the trigger for the face view - /// to gathering necessary resources and lay out its content. + /// This method will only be called only once by the Vatom View Life Cycle (VVLC). This is the trigger for the face + /// view to gathering necessary resources and lay out its content. /// - /// Face views should call the `FaceViewDelegate`'s `faceView(didLoad:)` method once the face view has completed - /// loading or encoutered and error during. - func load() + /// Face views *must* call the completion handler once loading has completed or errored out. + func load(completion: ((Error?) -> Void)?) /* # NOTE @@ -67,7 +66,7 @@ public protocol FaceViewLifecycle: class { Perhaps vatomChanged(_ vatom: VatomModel) should be updated to accept the face model for the rare case it has changed? That said, modifiying the faces after init is bad proactice. */ - + /// Called to inform the face view the specified vAtom should be rendered. /// /// Face views should respond to this method by refreshing their content. Typically, this is achieved by internally @@ -111,7 +110,7 @@ open class BaseFaceView: UIView { public internal(set) var faceModel: FaceModel /// Face view delegate. - weak var delegate: FaceViewDelegate? + weak public internal(set) var delegate: FaceViewDelegate? /// Initializes a BaseFaceView using a vAtom and a face model. /// diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 3bcca445..ceea7da5 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -101,23 +101,23 @@ class ImageLayeredFaceView: FaceView { // MARK: - Face View Lifecycle /// Begins loading the face view's content. - func load() { + func load(completion: ((Error?) -> Void)?) { // reset content self.reset() // load required resources self.loadResources { [weak self] error in - + guard let self = self else { return } // update state and inform delegate of load completion if let error = error { self.isLoaded = false self.updateLayers() - self.delegate?.faceView(self, didLoad: error) + completion?(error) } else { self.isLoaded = true self.updateLayers() - self.delegate?.faceView(self, didLoad: nil) + completion?(nil) } } @@ -125,7 +125,7 @@ class ImageLayeredFaceView: FaceView { /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - + if self.vatom.id == vatom.id { // replace vatom, update UI self.vatom = vatom @@ -137,7 +137,7 @@ class ImageLayeredFaceView: FaceView { self.updateLayers() } } - + /// Resets the contents of the face view. private func reset() { self.baseLayer.image = nil @@ -244,7 +244,7 @@ class ImageLayeredFaceView: FaceView { timeOffset += 0.2 } } - + /// Remove all child layers without animation. private func removeAllLayers() { childLayers.forEach { $0.removeFromSuperview() } @@ -272,9 +272,9 @@ class ImageLayeredFaceView: FaceView { var request = ImageRequest(url: encodeURL) // use unencoded url as cache key request.cacheKey = resourceModel.url - + // load image (auto cancel previous) - Nuke.loadImage(with: request, into: self.baseLayer) { (response, error) in + Nuke.loadImage(with: request, into: self.baseLayer) { (_, error) in self.isLoaded = true completion(error) } diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index 4dcf0710..d0bd9742 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -73,23 +73,23 @@ class ImagePolicyFaceView: FaceView { // MARK: - Face View Lifecycle /// Begins loading the face view's content. - func load() { + func load(completion: ((Error?) -> Void)?) { // reset content self.reset() // update state self.updateUI { [weak self] error in - + guard let self = self else { return } // inform delegate of load completion if let error = error { self.isLoaded = false - self.delegate?.faceView(self, didLoad: error) + completion?(error) } else { self.isLoaded = true - self.delegate?.faceView(self, didLoad: nil) + completion?(nil) } } - + } /// Updates the backing Vatom and loads the new state. @@ -110,7 +110,7 @@ class ImagePolicyFaceView: FaceView { } } - + /// Resets the contents of the face view. private func reset() { self.animatedImageView.image = nil diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 37a7e588..987986f1 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -140,27 +140,27 @@ class ImageProgressFaceView: FaceView { // MARK: - FaceView Lifecycle /// Begins loading the face view's content. - func load() { + func load(completion: ((Error?) -> Void)?) { // reset content self.reset() /// load required resources self.loadResources { [weak self] error in - + guard let self = self else { return } // update state and inform delegate of load completion if let error = error { self.setNeedsLayout() self.updateUI() self.isLoaded = false - self.delegate?.faceView(self, didLoad: error) + completion?(error) } else { self.setNeedsLayout() self.updateUI() self.isLoaded = true - self.delegate?.faceView(self, didLoad: nil) + completion?(nil) } - + } } @@ -179,9 +179,9 @@ class ImageProgressFaceView: FaceView { self.setNeedsLayout() self.updateUI() } - + } - + /// Resets the contents of the face view. private func reset() { emptyImageView.image = nil @@ -287,13 +287,13 @@ class ImageProgressFaceView: FaceView { // ensure encoding passes do { - + let encodedEmptyURL = try BLOCKv.encodeURL(emptyImageResource.url) let encodedFullURL = try BLOCKv.encodeURL(fullImageResource.url) - + dispatchGroup.enter() dispatchGroup.enter() - + var requestEmpty = ImageRequest(url: encodedEmptyURL) // use unencoded url as cache key requestEmpty.cacheKey = emptyImageResource.url @@ -301,7 +301,7 @@ class ImageProgressFaceView: FaceView { Nuke.loadImage(with: requestEmpty, into: self.emptyImageView) { (_, _) in self.dispatchGroup.leave() } - + var requestFull = ImageRequest(url: encodedFullURL) // use unencoded url as cache key requestFull.cacheKey = fullImageResource.url @@ -309,12 +309,12 @@ class ImageProgressFaceView: FaceView { Nuke.loadImage(with: requestFull, into: self.fullImageView) { (_, _) in self.dispatchGroup.leave() } - + dispatchGroup.notify(queue: .main) { self.isLoaded = true completion(nil) } - + } catch { completion(error) } diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index b5b81cb0..1ee43eb1 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -124,8 +124,8 @@ class ImageFaceView: FaceView { // MARK: - Face View Lifecycle /// Begins loading the face view's content. - func load() { - + func load(completion: ((Error?) -> Void)?) { + /* # Pattern 1. Call `reset` (which sets `isLoaded` to false) @@ -134,22 +134,22 @@ class ImageFaceView: FaceView { >>> set `isLoaded` to true >>> call the delegate */ - + // reset content self.reset() // load required resources self.loadResources { [weak self] error in - + guard let self = self else { return } // update state and inform delegate of load completion if let error = error { self.isLoaded = false - self.delegate?.faceView(self, didLoad: error) + completion?(error) } else { self.isLoaded = true - self.delegate?.faceView(self, didLoad: nil) + completion?(nil) } - + } } @@ -168,12 +168,11 @@ class ImageFaceView: FaceView { // reset content self.reset() - // replace current vatom self.vatom = vatom - self.load() + self.loadResources(completion: nil) } - + /// Resets the contents of the face view. private func reset() { self.animatedImageView.image = nil @@ -190,40 +189,40 @@ class ImageFaceView: FaceView { // MARK: - Resources - private func loadResources(completion: @escaping (Error?) -> Void) { + private func loadResources(completion: ((Error?) -> Void)?) { // extract resource model guard let resourceModel = vatom.props.resources.first(where: { $0.name == config.imageName }) else { - completion(FaceError.missingVatomResource) + completion?(FaceError.missingVatomResource) return } - + do { // encode url let encodeURL = try BLOCKv.encodeURL(resourceModel.url) - + //FIXME: Where should this go? ImagePipeline.Configuration.isAnimatedImageDataEnabled = true - + //TODO: Should the size of the VatomView be factoring in and the image be resized? - + var request = ImageRequest(url: encodeURL) // use unencoded url as cache key request.cacheKey = resourceModel.url - + /* Nuke's `loadImage` cancels any exisitng requests and nils out the old image. This takes care of the reuse-pool use case where the same face view is used to display a vatom of the same template variation. */ - + // load image (auto cancel previous) Nuke.loadImage(with: request, into: self.animatedImageView) { (_, error) in self.isLoaded = true - completion(error) + completion?(error) } - + } catch { - completion(error) + completion?(error) } } diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index de6d79a8..dcc4aa21 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -80,9 +80,13 @@ class WebFaceView: FaceView { var isLoaded: Bool = false + /// Holds the completion handler. + private var completion: ((Error?) -> Void)? + /// Begins loading the face view's content. - func load() { - + func load(completion: ((Error?) -> Void)?) { + // store the completion + self.completion = completion self.loadFace() } @@ -98,7 +102,7 @@ class WebFaceView: FaceView { let children = self.vatom.listCachedChildren() self.coreBridge?.sendVatomChildren(children) } - + /// Resets the contents of the face view. private func reset() { @@ -127,8 +131,7 @@ class WebFaceView: FaceView { extension WebFaceView: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // inform delegate of load completion - self.delegate?.faceView(self, didLoad: nil) + self.completion?(nil) } func webView(_ webView: WKWebView, diff --git a/BlockV/Face/Vatom View/FaceMessageDelegate.swift b/BlockV/Face/Vatom View/FaceMessageDelegate.swift index 3646e9b3..2b4690ed 100644 --- a/BlockV/Face/Vatom View/FaceMessageDelegate.swift +++ b/BlockV/Face/Vatom View/FaceMessageDelegate.swift @@ -110,24 +110,8 @@ public struct FaceMessageError: Error { } /// The protocol face views must conform to in order to communicate -protocol FaceViewDelegate: class { - - /* - Is this delegate method required? - OPtions: - 1. Callback in load - 2. Delegate methods informing the owner of vatom view that the load completed. - - 1. Have promblems cause it's hard to - */ - - /// Called when the face view completes loading. - /// - /// - Parameters: - /// - faceView: The face view from which this message was received. - /// - result: Outcome of the face view load operation. - func faceView(_ faceView: FaceView, didLoad outcome: Error?) - +public protocol FaceViewDelegate: class { + /// Called when the vatom view receives a message from the face. /// /// - Parameters: diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index c540f153..19a5015b 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -449,7 +449,29 @@ open class VatomView: UIView { self.insertSubview(newFaceView, at: 0) // 1. instruct face view to load its content (must be able to handle being called multiple times). - newFaceView.load() + newFaceView.load { [weak self] (error) in + + ///printBV(info: "Face view load completion called.") + + guard let self = self else { return } + + // Error - Case 2 - Display error view if the face view encounters an error during its load operation. + + // ensure no error + guard error == nil else { + // face view encountered an error + self.selectedFaceView?.removeFromSuperview() + self.state = .error + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) + return + } + + // show face + self.state = .completed + // inform delegate + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(self.selectedFaceView!)) + + } } @@ -459,39 +481,17 @@ open class VatomView: UIView { /// /// This is the conduit of communication between the VatomView and it's Face View. extension VatomView: FaceViewDelegate { - - func faceView(_ faceView: FaceView, - didSendMessage message: String, - withObject object: [String: JSON], - completion: ((Result) -> Void)?) { - + + public func faceView(_ faceView: FaceView, + didSendMessage message: String, + withObject object: [String: JSON], + completion: ((Result) -> Void)?) { + // forward the message to the vatom view delegate self.vatomViewDelegate?.vatomView(self, didRecevieFaceMessage: message, withObject: object, completion: completion) } - - func faceView(_ faceView: FaceView, didLoad error: Error?) { - - // forward the result to the vatom view delegate - if let error = error { - - // face view encountered an error - self.selectedFaceView?.removeFromSuperview() - self.state = .error - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) - - // inform delegate - //TODO: Error information is lost here. Does the face view need a specific error. - let vvlcError = VVLCError.faceViewLoadFailed - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: Result.failure(vvlcError)) - } else { - // show face - self.state = .completed - // inform delegate - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(faceView)) - } - } - + } From 3a323ccff6dac2bea7684fb7f7f7806c557c7efe Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 14:46:08 +0200 Subject: [PATCH 071/165] Fix stray comment --- BlockV/Face/Face Views/FaceView.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/BlockV/Face/Face Views/FaceView.swift b/BlockV/Face/Face Views/FaceView.swift index 6fe13844..e224b748 100644 --- a/BlockV/Face/Face Views/FaceView.swift +++ b/BlockV/Face/Face Views/FaceView.swift @@ -11,11 +11,6 @@ import Foundation -/* - When implementing a Face View, it is worth considering how it will work for should work for both owned and unowned - (public) vAtoms. - */ - /// TODO: This operator is useful, but has a drawback in that it always makes an assignment. infix operator ?= internal func ?= (lhs: inout T, rhs: T?) { @@ -39,6 +34,8 @@ public protocol FaceViewIdentifiable { } /// The protocol that face views must adopt to receive lifecycle events. +/// +/// When implementing a Face View, it is worth considering how it will work for both owned and unowned (public) vAtoms. public protocol FaceViewLifecycle: class { /// Boolean value indicating whether the face view has loaded. After load has completed that boolean must be From 4b9d22b56abc26a7d76fc70a19303024cf08ff0c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 17 Apr 2019 14:46:44 +0200 Subject: [PATCH 072/165] Call UI update once --- .../Image Layered/ImageLayeredFaceView.swift | 11 +++++++---- .../Image Policy/ImagePolicyFaceView.swift | 5 +++-- .../Image Progress/ImageProgressFaceView.swift | 12 ++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index ceea7da5..5451c644 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -109,14 +109,16 @@ class ImageLayeredFaceView: FaceView { self.loadResources { [weak self] error in guard let self = self else { return } + + // update ui + self.updateLayers() + // update state and inform delegate of load completion if let error = error { self.isLoaded = false - self.updateLayers() completion?(error) } else { self.isLoaded = true - self.updateLayers() completion?(nil) } } @@ -129,13 +131,14 @@ class ImageLayeredFaceView: FaceView { if self.vatom.id == vatom.id { // replace vatom, update UI self.vatom = vatom - self.updateLayers() } else { // replace vatom, reset and update UI self.vatom = vatom self.reset() - self.updateLayers() } + // update ui + self.updateLayers() + } /// Resets the contents of the face view. diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index d0bd9742..396628f0 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -101,13 +101,14 @@ class ImagePolicyFaceView: FaceView { if self.vatom.id == vatom.id { // replace vatom, update UI self.vatom = vatom - self.updateUI(completion: nil) + } else { // replace vatom, reset and update UI self.vatom = vatom self.reset() - self.updateUI(completion: nil) } + // update ui + self.updateUI(completion: nil) } diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 987986f1..ae72bce8 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -168,17 +168,17 @@ class ImageProgressFaceView: FaceView { /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - if self.vatom.id == vatom.id { + if self.vatom.id == vatom.id { // replace vatom, update UI self.vatom = vatom - self.updateUI() - } else { + } else { // replace vatom, reset and load self.vatom = vatom self.reset() - self.setNeedsLayout() - self.updateUI() - } + } + // update ui + self.setNeedsLayout() + self.updateUI() } From 5c31e3ebefe8b37a48149fd92c11231f0eb900a5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 18 Apr 2019 10:23:06 +0200 Subject: [PATCH 073/165] Move roll back onto main queue --- BlockV/Core/Requests/BLOCKv+VatomRequest.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index d07f105a..aea95068 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -167,17 +167,17 @@ extension BLOCKv { not enforce child policy rules so this always succeed (using the current API). */ let updateVatomModel = baseModel.payload - // roll back only those failed containments - let undosToRollback = undos.filter { !updateVatomModel.ids.contains($0.id) } - undosToRollback.forEach { $0.undo() } DispatchQueue.main.async { + // roll back only those failed containments + let undosToRollback = undos.filter { !updateVatomModel.ids.contains($0.id) } + undosToRollback.forEach { $0.undo() } completion(.success(updateVatomModel)) } case .failure(let error): - // roll back all containments - undos.forEach { $0.undo() } DispatchQueue.main.async { + // roll back all containments + undos.forEach { $0.undo() } completion(.failure(error)) } } From ee3a86ba1ecbf35cc69927b51928a35d56ab2812 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 18 Apr 2019 15:25:40 +0200 Subject: [PATCH 074/165] Core: Feature - Geopos region (#209) * Add geo pos regin getter to DataPool * Add geopos region * Add region command to web socket manager --- BlockV/Core/Data Pool/DataPool.swift | 19 +- .../Core/Data Pool/Regions/GeoPosRegion.swift | 215 ++++++++++++++++++ BlockV/Core/Web Socket/WebSocketManager.swift | 32 +++ 3 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 BlockV/Core/Data Pool/Regions/GeoPosRegion.swift diff --git a/BlockV/Core/Data Pool/DataPool.swift b/BlockV/Core/Data Pool/DataPool.swift index da4f95a9..2a76e573 100644 --- a/BlockV/Core/Data Pool/DataPool.swift +++ b/BlockV/Core/Data Pool/DataPool.swift @@ -11,6 +11,7 @@ import Foundation import PromiseKit +import MapKit /* # Notes: @@ -30,7 +31,10 @@ public final class DataPool { /// List of available plugins, i.e. region classes. internal static let plugins: [Region.Type] = [ - InventoryRegion.self + InventoryRegion.self, + VatomChildrenRegion.self, + VatomIDRegion.self, + GeoPosRegion.self ] /// List of active regions. @@ -113,19 +117,28 @@ public final class DataPool { extension DataPool { + /* + Convenience functions for fetching/creating regions. Only the abstract `Region` type is returned. + */ + /// Returns the global inventory region. public static func inventory() -> Region { return DataPool.region(id: InventoryRegion.id, descriptor: "") } - /// Returns the global vatom regions for the specified identifier. + /// Returns the vatom region for the specified identifier. public static func vatom(id: String) -> Region { return DataPool.region(id: VatomIDRegion.id, descriptor: [id]) } - /// Returns the global children region for the specifed parent identifier. + /// Returns the children region for the specifed parent identifier. public static func children(parentID: String) -> Region { return DataPool.region(id: VatomChildrenRegion.id, descriptor: parentID) } + /// Returns the geo pos region for the specifed coordinate region. + public static func geoPos(region: MKCoordinateRegion) -> Region { + return DataPool.region(id: GeoPosRegion.id, descriptor: region) + } + } diff --git a/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift new file mode 100644 index 00000000..17e1a3f8 --- /dev/null +++ b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift @@ -0,0 +1,215 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import PromiseKit +import CoreLocation +import MapKit + +/// This region plugin provides access to a collection of vatoms that has been dropped within the specified region on +/// the map. +//// +/// To get an instance, call `DataPool.region(id: "geopos", descriptor: MKCoordinateRegion)` +/// +/// Responsibilities +/// - Monitor a region. +/// - Automatically subcribe to premptive brian updates. +class GeoPosRegion: BLOCKvRegion { + + /// Plugin identifier. + override class var id: String { return "geopos" } + + /// The monitored region. + let region: MKCoordinateRegion + + /// Constructor. + required init(descriptor: Any) throws { + + // check descriptor type + guard let region = descriptor as? MKCoordinateRegion else { + throw NSError("Region descriptor must be a GeoPosRegionCoordinates object!") + } + + // store region + self.region = region + + // setup base class + try super.init(descriptor: descriptor) + + // send region command + self.sendRegionCommand() + + } + + /// Our state key is the top-right and bottom-left geopos coordinates + override var stateKey: String { + + var hasher = Hasher() + hasher.combine(self.region.topRight.latitude) + hasher.combine(self.region.topRight.longitude) + hasher.combine(self.region.bottomLeft.latitude) + hasher.combine(self.region.bottomLeft.longitude) + let hash = hasher.finalize() // not guaranteed to be equal across different executions of your program + + return "geopos:\(hash)" + } + + /// Check if a region request matches our region. + override func matches(id: String, descriptor: Any) -> Bool { + + // make sure we got passed a proper region object + guard let region = descriptor as? MKCoordinateRegion else { + return false + } + + // check if matches ours + if region.topRight.latitude == self.region.topRight.latitude && + region.topRight.longitude == self.region.topRight.longitude && + region.bottomLeft.latitude == self.region.bottomLeft.latitude && + region.bottomLeft.longitude == self.region.bottomLeft.longitude { + return true + } + + // did not match + return false + + } + + /// Load current state from the server. + override func load() -> Promise<[String]?> { + + // pause websocket events + self.pauseMessages() + + let endpoint: Endpoint = API.Generic.geoDiscover( + bottomLeftLat: self.region.bottomLeft.latitude, + bottomLeftLon: self.region.bottomLeft.longitude, + topRightLat: self.region.topRight.latitude, + topRightLon: self.region.topRight.longitude, + filter: "all") + + // execute request + return BLOCKv.client.requestJSON(endpoint).map { json -> [String]? in + + // parse items + guard + let json = json as? [String: Any], + let payload = json["payload"] as? [String: Any], + let items = self.parseDataObject(from: payload) else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + // add all objects + self.add(objects: items) + // return IDs + return items.map { $0.id } + + }.ensure { + + // resume websocket events + self.resumeMessages() + + } + + } + + /// Override save with a blank implementation. Regions change too often. + override func save() {} + + /// Override map so we can exclude vatoms which are no longer dropped. + override func map(_ object: DataObject) -> Any? { + + // check if dropped + guard + let vatom = object.data, + let props = vatom["vAtom::vAtomType"] as? [String: Any], + let dropped = props["dropped"] as? Bool, dropped + else { return nil } + + // it is, continue + return super.map(object) + + } + + /// Called when the Web socket reconnects. + override func onWebSocketConnect() { + super.onWebSocketConnect() + + // send region command + self.sendRegionCommand() + + } + + /// Sends the monitor command to the backend. This allows this client to receive preemptive brain updates over the + /// Web socket. + func sendRegionCommand() { + // write region command + BLOCKv.socket.writeRegionCommand(region.toDictionary()) + } + +} + +private extension MKCoordinateRegion { + + /// Returns a dictionary in data pool format. + func toDictionary() -> [String: Any] { + let payload: [String: [String: Any]] = [ + "top_left": [ + "lat": self.topLeft.latitude, + "lon": self.topLeft.longitude + ], + "bottom_right": [ + "lat": self.bottomRight.latitude, + "lon": self.bottomRight.longitude + ] + ] + return payload + } + +} + +public extension MKCoordinateRegion { + + /* + Things to check: + 1. Behaviour around the poles and international date line. + 2. Potentially better way: + > https://stackoverflow.com/questions/8496551/how-to-fit-a-certain-bounds-consisting-of-ne-and-sw-coordinates-into-the-visible + */ + + /// Computes the coordinate of the bottom left point. + var bottomLeft: CLLocationCoordinate2D { + return CLLocationCoordinate2DMake( + self.center.latitude - self.span.latitudeDelta/2, self.center.longitude - self.span.longitudeDelta/2 + ) + } + + /// Computes the coordinate of the bottom right point. + var bottomRight: CLLocationCoordinate2D { + return CLLocationCoordinate2DMake( + self.center.latitude - self.span.latitudeDelta/2, self.center.longitude + self.span.longitudeDelta/2 + ) + } + + /// Computes the coordinate of the top right point. + var topRight: CLLocationCoordinate2D { + return CLLocationCoordinate2DMake( + self.center.latitude + self.span.latitudeDelta/2, self.center.longitude + self.span.longitudeDelta/2 + ) + } + + /// Computes the coordinate of the top left point. + var topLeft: CLLocationCoordinate2D { + return CLLocationCoordinate2DMake( + self.center.latitude + self.span.latitudeDelta/2, self.center.longitude - self.span.longitudeDelta/2 + ) + } + +} diff --git a/BlockV/Core/Web Socket/WebSocketManager.swift b/BlockV/Core/Web Socket/WebSocketManager.swift index 67e87d5a..766ba1c3 100644 --- a/BlockV/Core/Web Socket/WebSocketManager.swift +++ b/BlockV/Core/Web Socket/WebSocketManager.swift @@ -217,6 +217,38 @@ public class WebSocketManager { self.connect() } + // MARK: - Commands + + /// Writes a raw payload to the socket. + func write(_ payload: [String: Any]) { + + DispatchQueue.global(qos: .userInitiated).async { + // serialize data + guard let data = try? JSONSerialization.data(withJSONObject: payload) else { + return + } + // write + self.socket?.write(data: data) + } + + } + + /// Writes a region command using the specified payload to the socket. + func writeRegionCommand(_ payload: [String: Any]) { + // command package + let commandPackage: [String: Any] = [ + "cmd": "monitor", + "id": "1", + "version": "1", + "type": "command", + "payload": payload + ] + print(#function, payload) + // write + self.write(commandPackage) + + } + } // MARK: - Extension WebSocket Delegate From 0d1d02f84cd2acb30322e8686b0ebdf069c183b5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:50:10 +0200 Subject: [PATCH 075/165] Improve message handling Restrict paylaod to vatoms only --- .../Core/Data Pool/Regions/GeoPosRegion.swift | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift index 17e1a3f8..bb5116de 100644 --- a/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift +++ b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift @@ -14,6 +14,14 @@ import PromiseKit import CoreLocation import MapKit +/* + - Map must have it's own array of on-screen vatom array model (which is only those vatoms for the visible region). + - When visialble regions changes, the previous region must close, and a new region created. + - Once the new region is created, the region's vatoms must be diffed with the in-memory model. + This means the in-memory model only holds the on-screen vatoms, but as the region changes, remaining vatoms + arn't removed and re-added. This could be achived with the map's annotation model. + */ + /// This region plugin provides access to a collection of vatoms that has been dropped within the specified region on /// the map. //// @@ -29,9 +37,12 @@ class GeoPosRegion: BLOCKvRegion { /// The monitored region. let region: MKCoordinateRegion + + /// Current user ID. + let currentUserID = DataPool.sessionInfo["userID"] as? String ?? "" /// Constructor. - required init(descriptor: Any) throws { + required init(descriptor: Any) throws { //TODO: Add filter "all" "avatar" // check descriptor type guard let region = descriptor as? MKCoordinateRegion else { @@ -94,7 +105,7 @@ class GeoPosRegion: BLOCKvRegion { bottomLeftLon: self.region.bottomLeft.longitude, topRightLat: self.region.topRight.latitude, topRightLon: self.region.topRight.longitude, - filter: "all") + filter: "vatoms") // execute request return BLOCKv.client.requestJSON(endpoint).map { json -> [String]? in @@ -153,6 +164,100 @@ class GeoPosRegion: BLOCKvRegion { // write region command BLOCKv.socket.writeRegionCommand(region.toDictionary()) } + + /// Called on Web socket message. + /// + /// Allows super to handle 'state_update', then goes on to process 'inventory' events. + /// Message process is paused for 'inventory' events which indicate a vatom was added. Since the vatom must + /// be fetched from the server. + override func processMessage(_ msg: [String: Any]) { + + // - Look at state update + + // get info + guard let msgType = msg["msg_type"] as? String else { return } + guard let payload = msg["payload"] as? [String: Any] else { return } + guard let newData = payload["new_object"] as? [String: Any] else { return } + guard let vatomID = payload["id"] as? String else { return } + + if msgType == "state_update" { + + // check update is related to drop + guard let properties = newData["vAtom::vAtomType"] as? [String: Any], + let dropped = properties["dropped"] as? Bool + else { return } + + // check if vatom was picked up + if !dropped { + // remove vatom from this region + self.remove(ids: [vatomID]) + return + } + + // check if we have the vatom + if self.get(id: vatomID) != nil { + // ask super to process the update to the object (i.e. setting dropped to true) + super.processMessage(msg) + } else { + + // pause this instance's message processing and fetch vatom payload + self.pauseMessages() + + // create endpoint over void + let endpoint: Endpoint = API.Generic.getVatoms(withIDs: [vatomID]) + BLOCKv.client.request(endpoint).done { data in + + // convert + guard + let object = try? JSONSerialization.jsonObject(with: data), + let json = object as? [String: Any], + let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + // parse out objects + guard let items = self.parseDataObject(from: payload) else { + throw NSError.init("Unable to parse data") //FIXME: Create a better error + } + + // add new objects + self.add(objects: items) + + }.catch { error in + printBV(error: "[InventoryRegion] Unable to fetch vatom. \(error.localizedDescription)") + }.finally { + // resume WebSocket processing + self.resumeMessages() + } + + } + + } + + // inspect inventory events + guard let oldOwner = payload["old_owner"] as? String else { return } + guard let newOwner = payload["new_owner"] as? String else { return } + + if msgType == "inventory" { + + /* + Iventory events indicate a vatom has entered or exited the user's inventory. It is unlikely that dropped vatoms + will experience inventory events, but it is possible. + Here we check only for outgoing inventory events (e.g. transfer). This gives the listener (e.g. map) the + opportinity to remove the vatom. + Incomming events don't need to be processed, since the user will subsequently need to drop the vatom. This + state-update event will be caught by the superclass `BLOCKvRegion`. + */ + + // check if this is an incoming or outgoing vatom + if oldOwner == self.currentUserID && newOwner != self.currentUserID { + // vatom is no longer owned by us + self.remove(ids: [vatomID]) + } + + } + + } } From facf4d0cf1eae4a983ab7ec280c55725874f61fa Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:50:22 +0200 Subject: [PATCH 076/165] Expose groups --- BlockV/Core/Network/Models/Geo Group/GeoGroupModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Network/Models/Geo Group/GeoGroupModel.swift b/BlockV/Core/Network/Models/Geo Group/GeoGroupModel.swift index 0944a397..4e6a1597 100644 --- a/BlockV/Core/Network/Models/Geo Group/GeoGroupModel.swift +++ b/BlockV/Core/Network/Models/Geo Group/GeoGroupModel.swift @@ -14,7 +14,7 @@ import CoreLocation /// Represents a geo discover groups response. public struct GeoModel: Decodable { - let groups: [GeoGroupModel] + public let groups: [GeoGroupModel] enum CodingKeys: String, CodingKey { case groups From d454c84d9f67bdd87f886d492cbbe8854b71f031 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:50:51 +0200 Subject: [PATCH 077/165] Add acquire pub variation convenience actinon --- .../Core/Requests/BLOCKv+VatomRequest.swift | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift index aea95068..dd3a6ae3 100644 --- a/BlockV/Core/Requests/BLOCKv+VatomRequest.swift +++ b/BlockV/Core/Requests/BLOCKv+VatomRequest.swift @@ -438,7 +438,7 @@ extension BLOCKv { // MARK: - Common Actions for Unowned vAtoms - /// Performs an acquire action on a vAtom. + /// Performs an acquire action on the specified vatom id. /// /// Often, only a vAtom's ID is known, e.g. scanning a QR code with an embeded vAtom /// ID. This call is useful is such circumstances. @@ -451,15 +451,28 @@ extension BLOCKv { completion: @escaping (Result<[String: Any], BVError>) -> Void) { let body = ["this.id": id] - // perform the action self.performAction(name: "Acquire", payload: body) { result in - - //FIXME: Call into Data Pool - completion(result) } } + + /// Performs an acquire pub variation action on the specified vatom id. + /// + /// - Parameters: + /// - id: The id of the vAtom to acquire. + /// - completion: The completion handler to call when the action is completed. + /// This handler is executed on the main queue. + public static func acquirePubVariation(withID id: String, + completion: @escaping (Result<[String: Any], BVError>) -> Void) { + + let body = ["this.id": id] + // perform the action + self.performAction(name: "AcquirePubVariation", payload: body) { result in + completion(result) + } + + } } From 31182a4657a6d230cb0aa25542ccf3cd5561ea38 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:55:01 +0200 Subject: [PATCH 078/165] Add gif support to error view --- BlockV/Face/Vatom View/DefaultErrorView.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index 5803fa10..ab586771 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -10,6 +10,7 @@ // import UIKit +import FLAnimatedImage import Nuke /// Default error view. @@ -47,10 +48,11 @@ internal final class DefaultErrorView: UIView & VatomViewError { button.tintColor = UIColor.orange return button }() - - private let activatedImageView: UIImageView = { - let imageView = UIImageView() + + private let activatedImageView: FLAnimatedImageView = { + let imageView = FLAnimatedImageView() imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.clipsToBounds = true imageView.contentMode = .scaleAspectFit return imageView }() @@ -112,6 +114,8 @@ internal final class DefaultErrorView: UIView & VatomViewError { guard let encodeURL = try? BLOCKv.encodeURL(resourceModel.url) else { return } + + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true var request = ImageRequest(url: encodeURL) // use unencoded url as cache key From ac69d9c23e9014b0bd1c045184965f58705b4738 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:55:21 +0200 Subject: [PATCH 079/165] fix missing common setup path --- BlockV/Face/Vatom View/VatomView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 19a5015b..390b15a9 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -172,6 +172,8 @@ open class VatomView: UIView { self.loaderView = VatomView.defaultLoaderView.init() self.errorView = VatomView.defaultErrorView.init() super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) + + commonSetup() } /// Intializes using a vatom and optional procedure. @@ -338,6 +340,7 @@ open class VatomView: UIView { */ guard let vatom = vatom else { + self.state = .error self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) assertionFailure("Developer error: vatom must not be nil.") return From 8ef71d2fe3f6c0b1dde50f30ffa0d8d926a0845c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 17:58:19 +0200 Subject: [PATCH 080/165] Lint --- BlockV/Face/Vatom View/DefaultErrorView.swift | 4 ++-- BlockV/Face/Vatom View/VatomView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index ab586771..1474bea0 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -48,7 +48,7 @@ internal final class DefaultErrorView: UIView & VatomViewError { button.tintColor = UIColor.orange return button }() - + private let activatedImageView: FLAnimatedImageView = { let imageView = FLAnimatedImageView() imageView.translatesAutoresizingMaskIntoConstraints = false @@ -114,7 +114,7 @@ internal final class DefaultErrorView: UIView & VatomViewError { guard let encodeURL = try? BLOCKv.encodeURL(resourceModel.url) else { return } - + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true var request = ImageRequest(url: encodeURL) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 390b15a9..6b0e2fee 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -172,7 +172,7 @@ open class VatomView: UIView { self.loaderView = VatomView.defaultLoaderView.init() self.errorView = VatomView.defaultErrorView.init() super.init(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) - + commonSetup() } From ed8df7c5338d316ae50b02f08a7a85e0e836ec30 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 28 Apr 2019 18:02:02 +0200 Subject: [PATCH 081/165] Set deployment target as 11.0 --- BLOCKv.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BLOCKv.podspec b/BLOCKv.podspec index e206bfa4..a0bf6d74 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -12,7 +12,7 @@ Pod::Spec.new do |s| s.author = { 'BLOCKv' => 'developer.blockv.io' } s.source = { :git => 'https://github.com/BLOCKvIO/ios-sdk.git', :tag => s.version.to_s } s.social_media_url = 'https://twitter.com/blockv_io' - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.swift_version = '5.0' s.default_subspecs = 'Face' From 5e41a5e0c825ccb8f13dd0618b09a9637c6bd827 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 29 Apr 2019 09:49:25 +0200 Subject: [PATCH 082/165] Core: Feature - OAuth (#210) * Expose appID and environment getters internally Configure environment when appID is configured * Add oauth web app domain * Session errors * Split API into generic and typed responses * Add oauth login/register * Remove expiresIn * Token exchange model * AssetProviderRefreshModel * Add headers to endpoint configuration * Remove setEnvironment * Expand token interceptor * Clean up --- BlockV/Core/BLOCKv.swift | 80 ++-- BlockV/Core/BVEnvironment.swift | 7 + BlockV/Core/BVError.swift | 12 +- .../Helpers/OAuth/AuthorizationServer.swift | 163 +++++++ .../Network/Models/AssetProviderModel.swift | 8 + BlockV/Core/Network/Models/BVToken.swift | 2 - .../Models/OAuthTokenExchangeModel.swift | 40 ++ .../Network/Stack/API+TypedResponses.swift | 404 ++++++++++++++++ BlockV/Core/Network/Stack/API.swift | 430 ++---------------- BlockV/Core/Network/Stack/Client.swift | 47 +- BlockV/Core/Network/Stack/Endpoint.swift | 5 +- .../Core/Requests/BLOCKv+AuthRequests.swift | 70 +++ 12 files changed, 783 insertions(+), 485 deletions(-) create mode 100644 BlockV/Core/Helpers/OAuth/AuthorizationServer.swift create mode 100644 BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift create mode 100644 BlockV/Core/Network/Stack/API+TypedResponses.swift diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index 47ac3b2a..ef7d2f42 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -27,7 +27,7 @@ public final class BLOCKv { /// The App ID to be passed to the BLOCKv platform. /// /// Must be set once by the host app. - fileprivate static var appID: String? { + internal fileprivate(set) static var appID: String? { // willSet is only called outside of the initialisation context, i.e. // setting the appID after its init will cause a fatal error. willSet { @@ -42,7 +42,7 @@ public final class BLOCKv { /// The BLOCKv platform environment to use. /// /// Must be set by the host app. - fileprivate static var environment: BVEnvironment? { + internal fileprivate(set) static var environment: BVEnvironment? { willSet { if environment != nil { reset() } } @@ -60,37 +60,6 @@ public final class BLOCKv { public static func configure(appID: String) { self.appID = appID - // NOTE: Since `configure` is called only once in the app's lifecycle. We do not - // need to worry about multiple registrations. - NotificationCenter.default.addObserver(BLOCKv.self, - selector: #selector(handleUserAuthorisationRequired), - name: Notification.Name.BVInternal.UserAuthorizationRequried, - object: nil) - - // handle session launch - if self.isLoggedIn { - self.onSessionLaunch() - } - - } - - // MARK: - Client - - // FIXME: Should this be nil on logout? - // FIXME: This MUST become a singleton (since only a single instance should ever exist). - private static let oauthHandler = OAuth2Handler(appID: BLOCKv.appID!, - baseURLString: BLOCKv.environment!.apiServerURLString, - refreshToken: CredentialStore.refreshToken?.token ?? "") - - /// Computes the configuration object needed to initialise clients and sockets. - fileprivate static var clientConfiguration: Client.Configuration { - // ensure host app has set an app id - let warning = """ - Please call 'BLOCKv.configure(appID:)' with your issued app ID before making network - requests. - """ - precondition(BLOCKv.appID != nil, warning) - // - CONFIGURE ENVIRONMENT // only modify if not set @@ -123,6 +92,37 @@ public final class BLOCKv { } + // NOTE: Since `configure` is called only once in the app's lifecycle. We do not + // need to worry about multiple registrations. + NotificationCenter.default.addObserver(BLOCKv.self, + selector: #selector(handleUserAuthorisationRequired), + name: Notification.Name.BVInternal.UserAuthorizationRequried, + object: nil) + + // handle session launch + if self.isLoggedIn { + self.onSessionLaunch() + } + + } + + // MARK: - Client + + // FIXME: Should this be nil on logout? + // FIXME: This MUST become a singleton (since only a single instance should ever exist). + private static let oauthHandler = OAuth2Handler(appID: BLOCKv.appID!, + baseURLString: BLOCKv.environment!.apiServerURLString, + refreshToken: CredentialStore.refreshToken?.token ?? "") + + /// Computes the configuration object needed to initialise clients and sockets. + fileprivate static var clientConfiguration: Client.Configuration { + // ensure host app has set an app id + let warning = """ + Please call 'BLOCKv.configure(appID:)' with your issued app ID before making network + requests. + """ + precondition(BLOCKv.appID != nil, warning) + // return the configuration (inexpensive object) return Client.Configuration(baseURLString: BLOCKv.environment!.apiServerURLString, appID: BLOCKv.appID!) @@ -267,20 +267,6 @@ public final class BLOCKv { /// Holds a closure to call on logout public static var onLogout: (() -> Void)? - /// Sets the BLOCKv platform environment. - /// - /// By setting the environment you are informing the SDK which BLOCKv - /// platform environment to interact with. - /// - /// Typically, you would call `setEnvironment` in `application(_:didFinishLaunchingWithOptions:)`. - @available(*, deprecated, message: "BLOCKv now defaults to production. You may remove this call.") - public static func setEnvironment(_ environment: BVEnvironment) { - self.environment = environment - - //FIXME: *Changing* the environment should nil out the client and access credentials. - - } - /// This function is called everytime a user session is launched. /// /// A 'session launch' means the user has logged in (received a new refresh token), or the app has been cold diff --git a/BlockV/Core/BVEnvironment.swift b/BlockV/Core/BVEnvironment.swift index b91df37b..d3a8c7d8 100644 --- a/BlockV/Core/BVEnvironment.swift +++ b/BlockV/Core/BVEnvironment.swift @@ -34,4 +34,11 @@ public enum BVEnvironment: String { } } + var oauthWebApp: String { + switch self { + case .production: return "https://login.blockv.io" + case .development: return "https:/login.blockv.net" + } + } + } diff --git a/BlockV/Core/BVError.swift b/BlockV/Core/BVError.swift index 0e9afce9..fc615c48 100644 --- a/BlockV/Core/BVError.swift +++ b/BlockV/Core/BVError.swift @@ -34,14 +34,20 @@ public enum BVError: Error { case networking(error: Error) /// Models a Web socket error. case webSocket(error: WebSocketErrorReason) - - //FIXME: REMOVE AT SOME POINT + /// Models a session error. + case session(reason: SessionErrorReason) /// Models a custom error. This should be used in very limited circumstances. /// A more defined error is preferred. case custom(reason: String) // MARK: Reasons + public enum SessionErrorReason: Equatable { + case invalidAuthoriationCode + case nonMatchingStates + + } + /// Platform error. Associated values: `code` and `message`. public enum PlatformErrorReason: Equatable { @@ -202,6 +208,8 @@ extension BVError: LocalizedError { return reason.localizedDescription case .modelDecoding(let reason): return "Model decoding failed with error: \(reason)" + case .session(let reason): + return "Session error: \(reason)" case .custom(reason: let reason): return reason } diff --git a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift new file mode 100644 index 00000000..43ffe2f3 --- /dev/null +++ b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift @@ -0,0 +1,163 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import SafariServices + +//TODO: Update for iOS 12 ASWebAuthenticationSession + +public final class AuthorizationServer { + + // MARK: - Properties + + // viewer config + let clientID: String // viewer app-id + let domain: String // login web app domain + let scope: String // requested scope + let redirectURI: String // publisher registered redirect url + + var receivedCode: String? + var receivedState: String? + + private var authSession: SFAuthenticationSession? + private var savedState: String? + + // MARK: - Initialization + + public init(clientID: String, domain: String, scope: String, redirectURI: String) { + self.clientID = clientID + self.domain = domain + self.scope = scope + self.redirectURI = redirectURI + } + + // MARK: - Methods + + /// Begins delegated authorization. + public func authorize(handler: @escaping (Bool) -> Void) { + + savedState = generateState(withLength: 20) + + var urlComp = URLComponents(string: domain)! + + urlComp.queryItems = [ + URLQueryItem(name: "response_type", value: "code"), + URLQueryItem(name: "client_id", value: clientID), + URLQueryItem(name: "redirect_uri", value: redirectURI), + URLQueryItem(name: "state", value: savedState!), + URLQueryItem(name: "scope", value: scope) + ] + + //TODO: Should the `callbackURLScheme` include more than the redirectURL, e.g. bundle identifier? + // init an auth session + authSession = SFAuthenticationSession(url: urlComp.url!, + callbackURLScheme: redirectURI, + completionHandler: { (url, error) in + guard error == nil else { + return handler(false) + } + + handler(url != nil && self.parseAuthorizeRedirectURL(url!)) + + }) + // start the authentication session + authSession?.start() + + } + + /// Parse authorised redirect URL. + /// + /// `code` and `state` instance properties are updated. + /// + /// - Parameter url: Parses out the information in the redirect URL. + /// - Returns: Boolean value indicating the outcome of parsing the authorize redirect url. + func parseAuthorizeRedirectURL(_ url: URL) -> Bool { + + // decompose into components + guard let urlComp = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + authSession?.cancel() + return false + } + // find query items + guard let items = urlComp.queryItems else { + authSession?.cancel() + return false + } + // extract code and state + receivedCode = items.first(where: { $0.name == "code" })?.value + receivedState = items.first(where: { $0.name == "state" })?.value + + // dismiss + authSession?.cancel() + return receivedCode != nil && receivedState != nil + + } + + /// Exchanges authorization code for tokens. + /// + /// - Parameter completion: Completion handler that is called once the request has been processed. + func getToken(completion: @escaping (Result) -> Void) { + // sanity checks + guard let code = receivedCode else { + let error = BVError.session(reason: .invalidAuthoriationCode) + completion(.failure(error)) + return + } + // security: scheck state match + guard savedState == receivedState else { + let error = BVError.session(reason: .nonMatchingStates) + completion(.failure(error)) + return + } + + // build token exchange endpoint + let endpoint = API.Session.tokenExchange(grantType: "authorization_code", + clientID: self.clientID, + code: code, + redirectURI: redirectURI) + + // perform request + BLOCKv.client.request(endpoint) { result in + switch result { + case .success(let model): + //FIXME: Temporary converstion between temporary token model + let oauthTokenExchangeModel = OAuthTokenExchangeModel(accessToken: model.payload.accessToken.token, + refreshToken: model.payload.refreshToken.token, + tokenType: model.payload.accessToken.tokenType, + expriesIn: nil, + scope: nil) + + completion(.success(oauthTokenExchangeModel)) + + case .failure(let error): + completion(.failure(error)) + } + } + + } + + // MARK: Helpers + + private func generateState(withLength len: Int) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + let length = UInt32(letters.count) + + var randomString = "" + for _ in 0.. URL struct AssetProviderModel: Codable, Equatable { diff --git a/BlockV/Core/Network/Models/BVToken.swift b/BlockV/Core/Network/Models/BVToken.swift index 4e99cece..fd79810e 100644 --- a/BlockV/Core/Network/Models/BVToken.swift +++ b/BlockV/Core/Network/Models/BVToken.swift @@ -17,12 +17,10 @@ import Foundation struct BVToken: Codable, Equatable { let token: String let tokenType: String - let expiresIn: Int enum CodingKeys: String, CodingKey { case token = "token" case tokenType = "token_type" - case expiresIn = "expires_in" } } diff --git a/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift b/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift new file mode 100644 index 00000000..e10ef5af --- /dev/null +++ b/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift @@ -0,0 +1,40 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +public struct TemporaryOAuthTokenExchangeModel: Decodable, Equatable { + + let accessToken: BVToken + let refreshToken: BVToken + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case refreshToken = "refresh_token" + } + +} + +public struct OAuthTokenExchangeModel: Decodable, Equatable { + let accessToken: String + let refreshToken: String + let tokenType: String? //FIXME: Make non-optional when server is updated + let expriesIn: Double? //FIXME: Make non-optional when server is updated + let scope: String? //FIXME: Make non-optional when server is updated + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case refreshToken = "refresh_token" + case tokenType = "token_type" + case expriesIn = "expires_in" + case scope = "scope" + } +} diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift new file mode 100644 index 00000000..294c11eb --- /dev/null +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -0,0 +1,404 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import Alamofire + +/// Extension on API which provides endpoints whcih return typed models. +extension API { + + /// Namespace for session related endpoints with a typed reponse model. + enum Session { + + // MARK: - OAuth + + /// Builds the endpoint to exchange an auth code for session tokens. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func tokenExchange(grantType: String, clientID: String, code: String, redirectURI: String) + -> Endpoint> { + return API.Generic.token(grantType: grantType, clientID: clientID, code: code, redirectURI: redirectURI) + } + + // MARK: Asset Providers + + /// Builds the endpoint to refresh asset provider credentials. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getAssetProviders() -> Endpoint> { + return API.Generic.getAssetProviders() + } + + // MARK: Register + + private static let registerPath = "/v1/users" + + /// Builds the endpoint for new user registration. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func register(tokens: [RegisterTokenParams], userInfo: UserInfo? = nil) -> + Endpoint> { + + precondition(!tokens.isEmpty, "One or more tokens must be supplied for this endpoint.") + + // dictionary of user information + var params = [String: Any]() + if let userInfo = userInfo { + params = userInfo.toDictionary() + } + + // create an array of tokens in their dictionary representation + let tokens = tokens.map { $0.toDictionary() } + params["user_tokens"] = tokens + + return Endpoint(method: .post, + path: registerPath, + parameters: params) + + } + + // MARK: Login + + private static let loginPath = "/v1/user/login" + + /// Builds the endpoint for user login. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func login(tokenParams: LoginTokenParams) -> Endpoint> { + return Endpoint(method: .post, + path: loginPath, + parameters: tokenParams.toDictionary()) + } + + } + + /// Namespace for current user related endpoints with a typed reponse model. + enum CurrentUser { + + private static let currentUserPath = "/v1/user" + + /// Builds the endpoint to get the current user's properties. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func get() -> Endpoint> { + return Endpoint(path: currentUserPath) + } + + /// Builds the endpoint to get the current user's tokens. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getTokens() -> Endpoint> { + return Endpoint(path: currentUserPath + "/tokens") + } + + /// Builds the endpoint to log out the current user. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func logOut() -> Endpoint> { + return Endpoint(method: .post, path: currentUserPath + "/logout") + } + + /// Builds the endpoint to update current user's information. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func update(userInfo: UserInfo) -> Endpoint> { + return Endpoint(method: .patch, + path: currentUserPath, + parameters: userInfo.toSafeDictionary() + ) + } + + /// Builds the endpoint to verify a token with an OTP code. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func verifyToken(_ token: UserToken, code: String) -> Endpoint> { + return Endpoint(method: .post, + path: currentUserPath + "/verify_token", + parameters: [ + "token": token.value, + "token_type": token.type.rawValue, + "verify_code": code + ] + ) + } + + /// Builds the endpoint to reset a user token. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func resetToken(_ token: UserToken) -> Endpoint> { + return Endpoint(method: .post, + path: currentUserPath + "/reset_token", + parameters: token.toDictionary() + ) + } + + /// Builds the endpoint to send a verification request for a specific token. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func resetTokenVerification(forToken token: UserToken) -> Endpoint> { + return Endpoint(method: .post, + path: currentUserPath + "/reset_token_verification", + parameters: token.toDictionary() + ) + } + + /// Builds the endpoint to add a token to the current user. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func addToken(_ token: UserToken, isPrimary: Bool) -> Endpoint> { + return Endpoint(method: .post, + path: currentUserPath + "/tokens", + parameters: [ + "token": token.value, + "token_type": token.type.rawValue, + "is_primary": isPrimary + ] + ) + } + + /// Builds the endpoint to delete a token. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func deleteToken(id: String) -> Endpoint> { + return Endpoint(method: .delete, + path: currentUserPath + "/tokens/\(id)") + } + + /// Builds the endpoint to set a default token. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func setDefaultToken(id: String) -> Endpoint> { + return Endpoint(method: .put, + path: currentUserPath + "/tokens/\(id)/default") + } + + // MARK: Avatar + + /// Builds the endpoint for the user's avatar. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func uploadAvatar(_ imageData: Data) -> UploadEndpoint> { + + let bodyPart = MultiformBodyPart(data: imageData, + name: "avatar", + fileName: "avatar.png", + mimeType: "image/png") + return UploadEndpoint(path: "/v1/user/avatar", + bodyPart: bodyPart) + + } + + // MARK: Messaging + + /// Builds the endpoint to allow the current user to send a message to a user token. + /// + /// - Parameters: + /// - message: Content of the message. + /// - userID: Unique identifier of the recipient user. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func sendMessage(_ message: String, toUserId userId: String) -> Endpoint> { + return Endpoint(method: .post, + path: currentUserPath + "/message", + parameters: [ + "message": message, + "id": userId]) + } + + // MARK: DEBUG + + /// DO NOT EXPOSE. ONLY USE FOR TESTING. + /// + /// Builds the endpoint to allow the current user to be deleted. + static func deleteCurrentUser() -> Endpoint> { + return Endpoint(method: .delete, path: "/v1/user") + } + + } + + /// Namespace for public user related endpoints with a typed reponse model. + enum PublicUser { + + /// Builds the endpoint to get a public user's details. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func get(id: String) -> Endpoint> { + return Endpoint(path: "/v1/users/\(id)") + } + + } + + /// Namespace for vatom related endpoints with a typed reponse model. + enum Vatom { + + /// Builds an endpoint to get the current user's inventory. + /// + /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's + /// perspetive. That is, we specify the id of the parent who's children are to be retunred. + /// + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func getInventory(parentID: String, + page: Int = 0, + limit: Int = 0) -> Endpoint> { + return API.Generic.getInventory(parentID: parentID, page: page, limit: limit) + + } + + /// Builds an endpoint to get a vAtom by its unique identifier. + /// + /// - Parameter ids: Unique identifier of the vatom. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func getVatoms(withIDs ids: [String]) -> Endpoint> { + return API.Generic.getVatoms(withIDs: ids) + } + + /// Builds the endpoint to trash a vAtom specified by its id. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func trashVatom(_ id: String) -> Endpoint> { + return Endpoint(method: .post, + path: "/v1/user/vatom/trash", + parameters: ["this.id": id]) + + } + + /// Builds an endpoint to update a vAtom. + /// + /// - Parameter payload: Raw payload. + /// - Returns: Constructed endpoint specialized to parse out a `VatomUpdateModel`. + static func updateVatom(payload: [String: Any]) -> Endpoint> { + return API.Generic.updateVatom(payload: payload) + } + + /// Builds an endpoint to search for vAtoms. + /// + /// - Parameter payload: Raw request payload. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func discover(_ payload: [String: Any]) -> Endpoint> { + return API.Generic.discover(payload) + } + + /// Builds an endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). + /// + /// Use this endpoint to fetch a collection of vAtoms. + /// + /// - Parameters: + /// - bottomLeftLat: Bottom left latitude coordinate. + /// - bottomLeftLon: Bottom left longitude coordinate. + /// - topRightLat: Top right latitude coordinate. + /// - topRightLon: Top right longitude coordinte. + /// - filter: The vAtom filter option to apply. + /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. + static func geoDiscover(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + filter: String) -> Endpoint> { + + return API.Generic.geoDiscover(bottomLeftLat: bottomLeftLat, + bottomLeftLon: bottomLeftLon, + topRightLat: topRightLat, + topRightLon: topRightLon, + filter: filter) + } + + /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). + /// + /// Use this endpoint to fetch an collection of groups/annotation indicating the count + /// of vAtoms at a particular location. + /// + /// - Parameters: + /// - bottomLeftLat: Bottom left latitude coordinate. + /// - bottomLeftLon: Bottom left longitude coordinate. + /// - topRightLat: Top right latitude coordinate. + /// - topRightLon: Top right longitude coordinte. + /// - precision: The grouping precision applied when computing the groups. + /// - filter: The vAtom filter option to apply. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func geoDiscoverGroups(bottomLeftLat: Double, + bottomLeftLon: Double, + topRightLat: Double, + topRightLon: Double, + precision: Int, + filter: String) -> Endpoint> { + + return API.Generic.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, + bottomLeftLon: bottomLeftLon, + topRightLat: topRightLat, + topRightLon: topRightLon, + precision: precision, + filter: filter) + + } + + } + + /// Namespace for action related endpoints with a typed reponse model. + enum VatomAction { + + /* + Each action's reactor returns it's own json payload. This does not need to be mapped as yet. + */ + + /// Builds the endpoint to perform and action on a vAtom. + /// + /// - Parameters: + /// - name: Action name. + /// - payload: Raw payload for the action. + /// - Returns: Constructed endpoint generic over `Void` that may be passed to a request. + static func custom(name: String, payload: [String: Any]) -> Endpoint { + return API.Generic.perform(name: name, payload: payload) + } + + } + + /// Namespace for user action related endpoints with a typed reponse model. + enum UserActions { + + /// Builds the endpoint for fetching the actions configured for a template ID. + /// + /// - Parameter id: Uniquie identifier of the template. + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getActions(forTemplateID id: String) -> Endpoint> { + return API.Generic.getActions(forTemplateID: id) + } + + } + + /// Namespace for user activity related endpoints with a typed reponse model. + enum UserActivity { + + /// Builds the endpoint for fetching the threads involving the current user. + /// + /// - Parameters: + /// - cursor: Filters out all threads more recent than the cursor (useful for paging). + /// If omitted or set as zero, the most recent threads are returned. + /// - count: Defines the number of messages to return (after the cursor). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getThreads(cursor: String, count: Int) -> Endpoint> { + return API.Generic.getThreads(cursor: cursor, count: count) + } + + /// Builds the endpoint for fetching the message for a specified thread involving the current user. + /// + /// - Parameters: + /// - id: Unique identifier of the thread (a.k.a thread `name`). + /// - cursor: Filters out all message more recent than the cursor (useful for paging). + /// If omitted or set as zero, the most recent threads are returned. + /// - count: Defines the number of messages to return (after the cursor). + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> + Endpoint> { + return API.Generic.getMessages(forThreadId: threadId, cursor: cursor, count: count) + } + + } + +} diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 6c249162..108bf96e 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -12,427 +12,49 @@ import Foundation import Alamofire -// swiftlint:disable file_length - -/// Consolidates all BLOCKv API endpoints. -/// -/// Endpoints are namespaced to furture proof. -/// +/// Consolidates BLOCKv API endpoints. /// Endpoints closely match their server counterparts where possible. -/// -/// The goal is to abstract endpoint specific details. The networking client should -/// not need to taylor requests for the specific of an endpoint. +/// The goal is to abstract endpoint specific details. The networking client should not need to taylor requests for the +/// specific of an endpoint. + enum API { } extension API { - /* - Notes: - All Session, Current User, and Public User endpoints are wrapped in a container object. This is modelled as - BaseModel. - */ - - // MARK: - - - /// Consolidates all session related endpoints. - enum Session { + /// Namespace for endpoints which are generic over their response type. + enum Generic { - // MARK: Register + private static let userVatomPath = "/v1/user/vatom" + private static let userActionsPath = "/v1/user/actions" + private static let actionPath = "/v1/user/vatom/action" + private static let userActivityPath = "/v1/activity" - private static let registerPath = "/v1/users" + // MARK: OAuth - /// Builds the endpoint for new user registration. + /// Builds the generic endpoint to exchange a code for tokens. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func register(tokens: [RegisterTokenParams], userInfo: UserInfo? = nil) -> - Endpoint> { - - precondition(!tokens.isEmpty, "One or more tokens must be supplied for this endpoint.") - - // dictionary of user information - var params = [String: Any]() - if let userInfo = userInfo { - params = userInfo.toDictionary() - } + static func token(grantType: String, clientID: String, code: String, redirectURI: String) -> Endpoint { - // create an array of tokens in their dictionary representation - let tokens = tokens.map { $0.toDictionary() } - params["user_tokens"] = tokens + let params: [String: Any] = [ + "grant_type": grantType, + "client_id": clientID, + "code": code, + "redirect_uri": redirectURI + ] return Endpoint(method: .post, - path: registerPath, + path: "/v1/oauth/token", parameters: params) - - } - - // MARK: Login - - private static let loginPath = "/v1/user/login" - - /// Builds the endpoint for user login. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func login(tokenParams: LoginTokenParams) -> Endpoint> { - return Endpoint(method: .post, - path: loginPath, - parameters: tokenParams.toDictionary()) - } - - } - - // MARK: - - - /// Consolidates all current user endpoints. - enum CurrentUser { - - private static let currentUserPath = "/v1/user" - - /// Builds the endpoint to get the current user's properties. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func get() -> Endpoint> { - return Endpoint(path: currentUserPath) - } - - /// Builds the endpoint to get the current user's tokens. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getTokens() -> Endpoint> { - return Endpoint(path: currentUserPath + "/tokens") - } - - /// Builds the endpoint to log out the current user. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func logOut() -> Endpoint> { - return Endpoint(method: .post, path: currentUserPath + "/logout") - } - - /// Builds the endpoint to update current user's information. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func update(userInfo: UserInfo) -> Endpoint> { - return Endpoint(method: .patch, - path: currentUserPath, - parameters: userInfo.toSafeDictionary() - ) - } - - /// Builds the endpoint to verify a token with an OTP code. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func verifyToken(_ token: UserToken, code: String) -> Endpoint> { - return Endpoint(method: .post, - path: currentUserPath + "/verify_token", - parameters: [ - "token": token.value, - "token_type": token.type.rawValue, - "verify_code": code - ] - ) - } - - /// Builds the endpoint to reset a user token. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func resetToken(_ token: UserToken) -> Endpoint> { - return Endpoint(method: .post, - path: currentUserPath + "/reset_token", - parameters: token.toDictionary() - ) - } - - /// Builds the endpoint to send a verification request for a specific token. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func resetTokenVerification(forToken token: UserToken) -> Endpoint> { - return Endpoint(method: .post, - path: currentUserPath + "/reset_token_verification", - parameters: token.toDictionary() - ) - } - - /// Builds the endpoint to add a token to the current user. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func addToken(_ token: UserToken, isPrimary: Bool) -> Endpoint> { - return Endpoint(method: .post, - path: currentUserPath + "/tokens", - parameters: [ - "token": token.value, - "token_type": token.type.rawValue, - "is_primary": isPrimary - ] - ) - } - - /// Builds the endpoint to delete a token. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func deleteToken(id: String) -> Endpoint> { - return Endpoint(method: .delete, - path: currentUserPath + "/tokens/\(id)") - } - - /// Builds the endpoint to set a default token. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func setDefaultToken(id: String) -> Endpoint> { - return Endpoint(method: .put, - path: currentUserPath + "/tokens/\(id)/default") - } - - // MARK: Avatar - - /// Builds the endpoint for the user's avatar. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func uploadAvatar(_ imageData: Data) -> UploadEndpoint> { - - let bodyPart = MultiformBodyPart(data: imageData, - name: "avatar", - fileName: "avatar.png", - mimeType: "image/png") - return UploadEndpoint(path: "/v1/user/avatar", - bodyPart: bodyPart) - - } - - // MARK: Messaging - - /// Builds the endpoint to allow the current user to send a message to a user token. - /// - /// - Parameters: - /// - message: Content of the message. - /// - userID: Unique identifier of the recipient user. - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func sendMessage(_ message: String, toUserId userId: String) -> Endpoint> { - return Endpoint(method: .post, - path: currentUserPath + "/message", - parameters: [ - "message": message, - "id": userId]) - } - - // MARK: Redemption - - /* - /// Endpoint to fetch redeemables. - public static func getRedeemables() -> Endpoint { - return Endpoint(path: currentUserPath + "/redeemables") - } - */ - - // MARK: - DEBUG - - /// DO NOT EXPOSE. ONLY USE FOR TESTING. - /// - /// Builds the endpoint to allow the current user to be deleted. - static func deleteCurrentUser() -> Endpoint> { - return Endpoint(method: .delete, path: "/v1/user") - } - - } - - // MARK: - - - /// Consolidates all public user endpoints. - enum PublicUser { - - /// Builds the endpoint to get a public user's details. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func get(id: String) -> Endpoint> { - return Endpoint(path: "/v1/users/\(id)") - } - - } - - // MARK: - - - /// Consolidates all user vatom endpoints. - enum Vatom { - - /// Builds an endpoint to get the current user's inventory. - /// - /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's - /// perspetive. That is, we specify the id of the parent who's children are to be retunred. - /// - /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. - static func getInventory(parentID: String, - page: Int = 0, - limit: Int = 0) -> Endpoint> { - return API.Generic.getInventory(parentID: parentID, page: page, limit: limit) - - } - - /// Builds an endpoint to get a vAtom by its unique identifier. - /// - /// - Parameter ids: Unique identifier of the vatom. - /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. - static func getVatoms(withIDs ids: [String]) -> Endpoint> { - return API.Generic.getVatoms(withIDs: ids) - } - - /// Builds the endpoint to trash a vAtom specified by its id. - /// - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func trashVatom(_ id: String) -> Endpoint> { - return Endpoint(method: .post, - path: "/v1/user/vatom/trash", - parameters: ["this.id": id]) - - } - - /// Builds an endpoint to update a vAtom. - /// - /// - Parameter payload: Raw payload. - /// - Returns: Constructed endpoint specialized to parse out a `VatomUpdateModel`. - static func updateVatom(payload: [String: Any]) -> Endpoint> { - return API.Generic.updateVatom(payload: payload) } - /// Builds an endpoint to search for vAtoms. - /// - /// - Parameter payload: Raw request payload. - /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. - static func discover(_ payload: [String: Any]) -> Endpoint> { - return API.Generic.discover(payload) - } - - /// Builds an endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). - /// - /// Use this endpoint to fetch a collection of vAtoms. - /// - /// - Parameters: - /// - bottomLeftLat: Bottom left latitude coordinate. - /// - bottomLeftLon: Bottom left longitude coordinate. - /// - topRightLat: Top right latitude coordinate. - /// - topRightLon: Top right longitude coordinte. - /// - filter: The vAtom filter option to apply. - /// - Returns: Constructed endpoint specialized to parse out a `UnpackedModel`. - static func geoDiscover(bottomLeftLat: Double, - bottomLeftLon: Double, - topRightLat: Double, - topRightLon: Double, - filter: String) -> Endpoint> { - - return API.Generic.geoDiscover(bottomLeftLat: bottomLeftLat, - bottomLeftLon: bottomLeftLon, - topRightLat: topRightLat, - topRightLon: topRightLon, - filter: filter) - } - - /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). - /// - /// Use this endpoint to fetch an collection of groups/annotation indicating the count - /// of vAtoms at a particular location. - /// - /// - Parameters: - /// - bottomLeftLat: Bottom left latitude coordinate. - /// - bottomLeftLon: Bottom left longitude coordinate. - /// - topRightLat: Top right latitude coordinate. - /// - topRightLon: Top right longitude coordinte. - /// - precision: The grouping precision applied when computing the groups. - /// - filter: The vAtom filter option to apply. - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func geoDiscoverGroups(bottomLeftLat: Double, - bottomLeftLon: Double, - topRightLat: Double, - topRightLon: Double, - precision: Int, - filter: String) -> Endpoint> { - - return API.Generic.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, - bottomLeftLon: bottomLeftLon, - topRightLat: topRightLat, - topRightLon: topRightLon, - precision: precision, - filter: filter) - - } - - } - - // MARK: - + // MARK: Asset Providers - /// Consolidates all action endpoints. - enum VatomAction { - - /* - Each action's reactor returns it's own json payload. This does not need to be mapped as yet. - */ - - /// Builds the endpoint to perform and action on a vAtom. - /// - /// - Parameters: - /// - name: Action name. - /// - payload: Raw payload for the action. - /// - Returns: Constructed endpoint generic over `Void` that may be passed to a request. - static func custom(name: String, payload: [String: Any]) -> Endpoint { - return API.Generic.perform(name: name, payload: payload) - } - - } - - // MARK: - - - /// Consolidates all the user actions. - enum UserActions { - - /// Builds the endpoint for fetching the actions configured for a template ID. - /// - /// - Parameter id: Uniquie identifier of the template. - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getActions(forTemplateID id: String) -> Endpoint> { - return API.Generic.getActions(forTemplateID: id) - } - - } - - // MARK: - - - /// Consolidtes all the user activity endpoints. - enum UserActivity { - - /// Builds the endpoint for fetching the threads involving the current user. - /// - /// - Parameters: - /// - cursor: Filters out all threads more recent than the cursor (useful for paging). - /// If omitted or set as zero, the most recent threads are returned. - /// - count: Defines the number of messages to return (after the cursor). - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getThreads(cursor: String, count: Int) -> Endpoint> { - return API.Generic.getThreads(cursor: cursor, count: count) - } - - /// Builds the endpoint for fetching the message for a specified thread involving the current user. - /// - /// - Parameters: - /// - id: Unique identifier of the thread (a.k.a thread `name`). - /// - cursor: Filters out all message more recent than the cursor (useful for paging). - /// If omitted or set as zero, the most recent threads are returned. - /// - count: Defines the number of messages to return (after the cursor). - /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getMessages(forThreadId threadId: String, cursor: String, count: Int) -> - Endpoint> { - return API.Generic.getMessages(forThreadId: threadId, cursor: cursor, count: count) + static func getAssetProviders() -> Endpoint { + return Endpoint(method: .get, + path: "/v1/user/asset_providers") } - } - -} - -extension API { - - enum Generic { - - private static let userVatomPath = "/v1/user/vatom" - private static let userActionsPath = "/v1/user/actions" - private static let actionPath = "/v1/user/vatom/action" - private static let userActivityPath = "/v1/activity" - // MARK: Vatoms /// Builds the generic endpoint to get the current user's inventory. @@ -567,7 +189,7 @@ extension API { } - // MARK: - Perform Actions + // MARK: Perform Actions /// Builds the endpoint to perform and action on a vAtom. /// @@ -591,7 +213,7 @@ extension API { path: userActionsPath + "/\(id)") } - // MARK: - User Activity + // MARK: User Activity /// Builds the endpoint for fetching the threads involving the current user. /// diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index 38c4e33f..ec85bd00 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -125,7 +125,8 @@ final class Client: ClientProtocol { url(path: endpoint.path), method: endpoint.method, parameters: endpoint.parameters, - encoding: endpoint.encoding + encoding: endpoint.encoding, + headers: endpoint.headers ) // configure validation @@ -162,7 +163,8 @@ final class Client: ClientProtocol { url(path: endpoint.path), method: endpoint.method, parameters: endpoint.parameters, - encoding: endpoint.encoding + encoding: endpoint.encoding, + headers: endpoint.headers ) // configure validation @@ -193,7 +195,8 @@ final class Client: ClientProtocol { url(path: endpoint.path), method: endpoint.method, parameters: endpoint.parameters, - encoding: endpoint.encoding + encoding: endpoint.encoding, + headers: endpoint.headers ) // configure validation - will cause an error to be generated for unacceptable status code or MIME type. @@ -203,32 +206,29 @@ final class Client: ClientProtocol { request.validate().responseJSONDecodable(queue: self.queue, decoder: blockvJSONDecoder) { (dataResponse: DataResponse) in - // DEBUG - // let json = try? JSONSerialization.jsonObject(with: dataResponse.data!, options: []) - // dump(json) - switch dataResponse.result { case let .success(val): /* - Not all responses (even in the 200 range) are wrapped in the `BaseModel`. Endpoints must be treated - on a per-endpoint basis. + Certain endpoints return session tokens which need to be persisted (currently further up the chain) + and injected into the oauth session handler. */ // extract auth tokens if available if let model = val as? BaseModel { + // inject token into session's oauth handler + self.oauthHandler.set(accessToken: model.payload.accessToken.token, + refreshToken: model.payload.refreshToken.token) + } else if let model = val as? BaseModel { + // inject token into session's oauth handler + self.oauthHandler.set(accessToken: model.payload.accessToken, + refreshToken: model.payload.refreshToken) + } else if let model = val as? BaseModel { //FIXME: Remove this! + // inject token into session's oauth handler self.oauthHandler.set(accessToken: model.payload.accessToken.token, refreshToken: model.payload.refreshToken.token) } - // ensure the payload was parsed correctly - // on success, the payload should alway have a value - // guard let payload = val.payload else { - // let error = BVError.modelDecoding(reason: "Payload model not parsed correctly.") - // completion(nil, error) - // return - // } - completion(.success(val)) //TODO: Add some thing like this to pull back to a completion thread? @@ -239,13 +239,7 @@ final class Client: ClientProtocol { case let .failure(err): - // DEBUG - // if let data = dataResponse.data { - // let json = String(data: data, encoding: String.Encoding.utf8) - // print("Failure Response: \(json)") - // } - - //FIXME: Can this error casting be done away with? + //TODO: Can this error casting be done away with? if let err = err as? BVError { completion(.failure(err)) } else { @@ -285,7 +279,6 @@ final class Client: ClientProtocol { switch encodingResult { case .success(let upload, _, _): - //print("Upload response: \(upload.response.debugDescription)") // upload progress upload.uploadProgress { progress in @@ -294,12 +287,10 @@ final class Client: ClientProtocol { upload.validate() // parse out a native model (within the base model) - //TODO: If is fine to capture self here? upload.responseJSONDecodable(queue: self.queue, decoder: self.blockvJSONDecoder) { (dataResponse: DataResponse) in - /// switch dataResponse.result { case let .success(val): completion(val, nil) @@ -318,8 +309,6 @@ final class Client: ClientProtocol { } case .failure(let encodingError): - //print(encodingError) - let error = BVError.networking(error: encodingError) completion(nil, error) } diff --git a/BlockV/Core/Network/Stack/Endpoint.swift b/BlockV/Core/Network/Stack/Endpoint.swift index 0a10cdcc..5083df9d 100644 --- a/BlockV/Core/Network/Stack/Endpoint.swift +++ b/BlockV/Core/Network/Stack/Endpoint.swift @@ -29,17 +29,20 @@ final class Endpoint { let path: Path let parameters: Parameters? let encoding: ParameterEncoding + let headers: HTTPHeaders? //TODO: Does it make sense for json to be the default encoding? init(method: HTTPMethod = .get, path: Path, parameters: Parameters? = nil, - encoding: ParameterEncoding = JSONEncoding.default) { + encoding: ParameterEncoding = JSONEncoding.default, + headers: HTTPHeaders? = nil) { self.method = method self.path = path self.parameters = parameters self.encoding = encoding + self.headers = headers } diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index 295c8aac..8f79065c 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -16,6 +16,76 @@ extension BLOCKv { // MARK: - Register + //FIXME: callbackURLScheme - where does this get set? + public static func oauth(scope: String, redirectURI: String, completion: @escaping (BVError?) -> Void) { + + // ensure host app has set an app id + let warning = """ + Please call 'BLOCKv.configure(appID:)' with your issued app ID before making network + requests. + """ + precondition(BLOCKv.appID != nil, warning) + + // extract config variables + let appID = BLOCKv.appID! + let webAppDomain = BLOCKv.environment!.oauthWebApp + + let authServer = AuthorizationServer(clientID: appID, + domain: webAppDomain, + scope: scope, + redirectURI: redirectURI) + + // start delegated authorization + authServer.authorize { success in + + guard success else { + printBV(error: ("OAuth Authorize error.")) + return + } + // exchange code for tokens + authServer.getToken { result in + + switch result { + case .success(let tokens): + /* + At this point, accces and refresh tokens have been injected into the oauthhandler by the client + response inspector. + */ + + // build endpoint + let endpoint = API.Session.getAssetProviders() + // perform api call + BLOCKv.client.request(endpoint) { result in + switch result { + case .success(let model): + + // pull back to main queue + DispatchQueue.main.async { + + let refreshToken = BVToken(token: tokens.refreshToken, tokenType: tokens.tokenType!) + // persist refresh token and credential + CredentialStore.saveRefreshToken(refreshToken) + CredentialStore.saveAssetProviders(model.payload.assetProviders) + // noifty on login process + self.onLogin() + completion(nil) + } + + case .failure(let error): + completion(error) + } + } + + case .failure(let error): + completion(error) + } + + } + + } + + } + /// Registers a user on the BLOCKv platform. Accepts a user token (phone or email). /// /// - Parameters: From 668fc7477a25b7c88acfb186e1a813166647a1bf Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 30 Apr 2019 12:34:21 +0200 Subject: [PATCH 083/165] Invoke viewer's logout closure --- BlockV/Core/Requests/BLOCKv+AuthRequests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index 8f79065c..ddd153a7 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -246,9 +246,11 @@ extension BLOCKv { self.client.request(endpoint) { result in - // reset DispatchQueue.main.async { + // reset sdk state reset() + // give viewer opportunity to reset their state + onLogout?() } switch result { From 0205273ee083ffb83b25c20dbdfaca28cbd280cf Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 30 Apr 2019 15:38:45 +0200 Subject: [PATCH 084/165] Core: Fix - OAuth V2 (#215) * Replace TemporaryOAuthTokenExchangeModel with OAuthTokenExchangeModel --- .../Helpers/OAuth/AuthorizationServer.swift | 14 ++------------ .../Models/OAuthTokenExchangeModel.swift | 18 +++--------------- .../Network/Stack/API+TypedResponses.swift | 2 +- BlockV/Core/Network/Stack/Client.swift | 10 +++------- BlockV/Core/Requests/BLOCKv+AuthRequests.swift | 2 +- 5 files changed, 10 insertions(+), 36 deletions(-) diff --git a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift index 43ffe2f3..d6f83c8d 100644 --- a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift +++ b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift @@ -127,18 +127,8 @@ public final class AuthorizationServer { // perform request BLOCKv.client.request(endpoint) { result in switch result { - case .success(let model): - //FIXME: Temporary converstion between temporary token model - let oauthTokenExchangeModel = OAuthTokenExchangeModel(accessToken: model.payload.accessToken.token, - refreshToken: model.payload.refreshToken.token, - tokenType: model.payload.accessToken.tokenType, - expriesIn: nil, - scope: nil) - - completion(.success(oauthTokenExchangeModel)) - - case .failure(let error): - completion(.failure(error)) + case .success(let model): completion(.success(model)) + case .failure(let error): completion(.failure(error)) } } diff --git a/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift b/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift index e10ef5af..ee4f449f 100644 --- a/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift +++ b/BlockV/Core/Network/Models/OAuthTokenExchangeModel.swift @@ -11,24 +11,12 @@ import Foundation -public struct TemporaryOAuthTokenExchangeModel: Decodable, Equatable { - - let accessToken: BVToken - let refreshToken: BVToken - - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case refreshToken = "refresh_token" - } - -} - public struct OAuthTokenExchangeModel: Decodable, Equatable { let accessToken: String let refreshToken: String - let tokenType: String? //FIXME: Make non-optional when server is updated - let expriesIn: Double? //FIXME: Make non-optional when server is updated - let scope: String? //FIXME: Make non-optional when server is updated + let tokenType: String + let expriesIn: Double + let scope: String enum CodingKeys: String, CodingKey { case accessToken = "access_token" diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 294c11eb..252bff7d 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -24,7 +24,7 @@ extension API { /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func tokenExchange(grantType: String, clientID: String, code: String, redirectURI: String) - -> Endpoint> { + -> Endpoint { return API.Generic.token(grantType: grantType, clientID: clientID, code: code, redirectURI: redirectURI) } diff --git a/BlockV/Core/Network/Stack/Client.swift b/BlockV/Core/Network/Stack/Client.swift index ec85bd00..a69f12c0 100644 --- a/BlockV/Core/Network/Stack/Client.swift +++ b/BlockV/Core/Network/Stack/Client.swift @@ -219,14 +219,10 @@ final class Client: ClientProtocol { // inject token into session's oauth handler self.oauthHandler.set(accessToken: model.payload.accessToken.token, refreshToken: model.payload.refreshToken.token) - } else if let model = val as? BaseModel { + } else if let model = val as? OAuthTokenExchangeModel { // inject token into session's oauth handler - self.oauthHandler.set(accessToken: model.payload.accessToken, - refreshToken: model.payload.refreshToken) - } else if let model = val as? BaseModel { //FIXME: Remove this! - // inject token into session's oauth handler - self.oauthHandler.set(accessToken: model.payload.accessToken.token, - refreshToken: model.payload.refreshToken.token) + self.oauthHandler.set(accessToken: model.accessToken, + refreshToken: model.refreshToken) } completion(.success(val)) diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index ddd153a7..d7745b5b 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -62,7 +62,7 @@ extension BLOCKv { // pull back to main queue DispatchQueue.main.async { - let refreshToken = BVToken(token: tokens.refreshToken, tokenType: tokens.tokenType!) + let refreshToken = BVToken(token: tokens.refreshToken, tokenType: tokens.tokenType) // persist refresh token and credential CredentialStore.saveRefreshToken(refreshToken) CredentialStore.saveAssetProviders(model.payload.assetProviders) From 70a5e2cb56a06758c932af1d1484309bbf3a0304 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 30 Apr 2019 18:58:27 +0200 Subject: [PATCH 085/165] Add member-wise initializers to vatom, face, and action Convert blockv region to use member-wise initializers --- .../Core/Data Pool/Regions/BLOCKvRegion.swift | 31 +-- .../Action/ActionModel+Descriptor.swift | 48 +++++ .../Package/{ => Action}/ActionModel.swift | 0 .../Package/Face/FaceModel+Descriptable.swift | 92 ++++++++ .../Models/Package/{ => Face}/FaceModel.swift | 0 .../{ => Vatom}/VatomModel+Descriptor.swift | 197 ++++-------------- .../{ => Vatom}/VatomModel+Update.swift | 0 .../Package/{ => Vatom}/VatomModel.swift | 0 .../{ => Vatom}/VatomResourceModel.swift | 0 .../{ => Vatom}/VatomUpdateModel.swift | 0 10 files changed, 179 insertions(+), 189 deletions(-) create mode 100644 BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift rename BlockV/Core/Network/Models/Package/{ => Action}/ActionModel.swift (100%) create mode 100644 BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift rename BlockV/Core/Network/Models/Package/{ => Face}/FaceModel.swift (100%) rename BlockV/Core/Network/Models/Package/{ => Vatom}/VatomModel+Descriptor.swift (66%) rename BlockV/Core/Network/Models/Package/{ => Vatom}/VatomModel+Update.swift (100%) rename BlockV/Core/Network/Models/Package/{ => Vatom}/VatomModel.swift (100%) rename BlockV/Core/Network/Models/Package/{ => Vatom}/VatomResourceModel.swift (100%) rename BlockV/Core/Network/Models/Package/{ => Vatom}/VatomUpdateModel.swift (100%) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index 05fdb72d..691dc3ae 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -218,8 +218,7 @@ class BLOCKvRegion: Region { .starts(with: actionNamePrefix) == true } objectData["actions"] = actions.map { $0.data } - // Experiment 1: Descriptor initialiser - + // create vatoms, face, anf actions using member-wise initilializer do { let faces = faces.compactMap { $0.data }.compactMap { try? FaceModel(from: $0) } let actions = actions.compactMap { $0.data }.compactMap { try? ActionModel(from: $0) } @@ -231,34 +230,6 @@ class BLOCKvRegion: Region { printBV(error: error.localizedDescription) return nil } - - // Experiemnt 2: Dictionary decoder (dictionary > vatom model) - -// let decoder = DictionaryDecoder() -// decoder.dateDecodingStrategy = .iso8601 -// -// do { -// let vatoms = try decoder.decode(VatomModel.self, from: objectData) -// return vatoms -// } catch { -// printBV(error: error.localizedDescription) -// return nil -// } - - // Experiment 3: Data decoder (json decoder) - -// do { -// if JSONSerialization.isValidJSONObject(objectData) { -// let rawData = try JSONSerialization.data(withJSONObject: objectData) -// let vatoms = try JSONDecoder.blockv.decode(VatomModel.self, from: rawData) -// return vatoms -// } else { -// throw NSError.init("Invalid JSON for Vatom: \(object.id)") -// } -// } catch { -// printBV(error: error.localizedDescription) -// return nil -// } } diff --git a/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift new file mode 100644 index 00000000..650cead0 --- /dev/null +++ b/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift @@ -0,0 +1,48 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +extension ActionModel: Descriptable { + + init(from descriptor: [String: Any]) throws { + + guard + let _compoundName = descriptor["name"] as? String, + let _metaDescriptor = descriptor["meta"] as? [String: Any], + let _propertiesDescriptor = descriptor["properties"] as? [String: Any] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + let (templateID, actionName) = try ActionModel.splitCompoundName(_compoundName) + let meta = try MetaModel(from: _metaDescriptor) + let properties = try Properties(from: _propertiesDescriptor) + + self.init(compoundName: _compoundName, + name: actionName, + templateID: templateID, + meta: meta, + properties: properties) + + } + +} + +extension ActionModel.Properties: Descriptable { + + init(from descriptor: [String: Any]) throws { + guard + let _reactor = descriptor["reactor"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.init(reactor: _reactor) + } + +} diff --git a/BlockV/Core/Network/Models/Package/ActionModel.swift b/BlockV/Core/Network/Models/Package/Action/ActionModel.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/ActionModel.swift rename to BlockV/Core/Network/Models/Package/Action/ActionModel.swift diff --git a/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift b/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift new file mode 100644 index 00000000..6666fda3 --- /dev/null +++ b/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift @@ -0,0 +1,92 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import GenericJSON + +extension FaceModel: Descriptable { + + init(from descriptor: [String: Any]) throws { + + guard + let _id = descriptor["id"] as? String, + let _templateID = descriptor["template"] as? String, + let _metaDescriptor = descriptor["meta"] as? [String: Any], + let _propertiesDescriptor = descriptor["properties"] as? [String: Any] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.id = _id + self.templateID = _templateID + self.properties = try Properties(from: _propertiesDescriptor) + self.meta = try MetaModel(from: _metaDescriptor) + // convenience + isNative = properties.displayURL.hasPrefix("native://") + isWeb = properties.displayURL.hasPrefix("https://") + + } + +} + +extension FaceModel.Properties: Descriptable { + + init(from descriptor: [String: Any]) throws { + guard + let _displayURL = descriptor["display_url"] as? String, + let _constraintsDescriptor = descriptor["constraints"] as? [String: Any], + let _resources = descriptor["resources"] as? [String] + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + let _config = descriptor["config"] as? [String: Any] + + self.displayURL = _displayURL + self.constraints = try FaceModel.Properties.Constraints(from: _constraintsDescriptor) + self.resources = _resources + self.config = try? JSON(_config) + + } + +} + +extension FaceModel.Properties.Constraints: Descriptable { + + init(from descriptor: [String: Any]) throws { + guard + let _viewMode = descriptor["view_mode"] as? String, + let _platform = descriptor["platform"] as? String + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.viewMode = _viewMode + self.platform = _platform + + } + +} + +extension MetaModel: Descriptable { + + init(from descriptor: [String: Any]) throws { + guard + let _createdBy = descriptor["created_by"] as? String, + let _dataType = descriptor["data_type"] as? String, + let _whenCreated = descriptor["when_created"] as? String, + let _whenModified = descriptor["when_modified"] as? String, + let _whenCreatedDate = DateFormatter.blockvDateFormatter.date(from: _whenCreated), + let _whenModifiedDate = DateFormatter.blockvDateFormatter.date(from: _whenModified) + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } + + self.init(createdBy: _createdBy, + dataType: _dataType, + whenCreated: _whenCreatedDate, + whenModified: _whenModifiedDate) + + } + +} diff --git a/BlockV/Core/Network/Models/Package/FaceModel.swift b/BlockV/Core/Network/Models/Package/Face/FaceModel.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/FaceModel.swift rename to BlockV/Core/Network/Models/Package/Face/FaceModel.swift diff --git a/BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift similarity index 66% rename from BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift rename to BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift index dcbe37cc..47abffe2 100644 --- a/BlockV/Core/Network/Models/Package/VatomModel+Descriptor.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift @@ -17,7 +17,7 @@ protocol Descriptable { } extension VatomModel: Descriptable { - + init(from descriptor: [String: Any]) throws { guard @@ -25,17 +25,15 @@ extension VatomModel: Descriptable { let _version = descriptor["version"] as? String, let _whenCreated = descriptor["when_created"] as? String, let _whenModified = descriptor["when_modified"] as? String, - let _isUnpublished = descriptor["unpublished"] as? Bool, let _private = descriptor["private"] as? [String: Any], let _rootDescriptor = descriptor["vAtom::vAtomType"] as? [String: Any] - + else { throw BVError.modelDecoding(reason: "Model decoding failed: \(type(of: self))") } self.id = _id self.version = _version self.whenCreated = DateFormatter.blockvDateFormatter.date(from: _whenCreated)! //FIXME: force self.whenModified = DateFormatter.blockvDateFormatter.date(from: _whenModified)! //FIXME: force - self.isUnpublished = _isUnpublished self.private = try? JSON.init(_private) self.props = try RootProperties(from: _rootDescriptor) @@ -45,14 +43,16 @@ extension VatomModel: Descriptable { self.eth = nil self.eos = nil + self.isUnpublished = (descriptor["unpublished"] as? Bool) ?? false + } } extension RootProperties: Descriptable { - - init(from descriptor: [String : Any]) throws { - + + init(from descriptor: [String: Any]) throws { + guard let _author = descriptor["author"] as? String, let _rootType = descriptor["category"] as? String, @@ -61,7 +61,7 @@ extension RootProperties: Descriptable { let _publisherFQDN = descriptor["publisher_fqdn"] as? String, let _title = descriptor["title"] as? String, let _description = descriptor["description"] as? String, - + let _category = descriptor["category"] as? String, let _clonedFrom = descriptor["cloned_from"] as? String, let _cloningScore = descriptor["cloning_score"] as? Double, @@ -74,19 +74,19 @@ extension RootProperties: Descriptable { let _parentID = descriptor["parent_id"] as? String, let _transferredBy = descriptor["transferred_by"] as? String, let _visibilityDescriptor = descriptor["visibility"] as? [String: Any], - + let _isAcquirable = descriptor["acquirable"] as? Bool, let _isRedeemable = descriptor["redeemable"] as? Bool, let _isDisabled = descriptor["disabled"] as? Bool, let _isDropped = descriptor["dropped"] as? Bool, let _isTradeable = descriptor["tradeable"] as? Bool, let _isTransferable = descriptor["transferable"] as? Bool, - + let _geoPositionDescriptor = descriptor["geo_pos"] as? [String: Any], let _resourceDescriptor = descriptor["resources"] as? [[String: Any]] - + else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - + // decode if present let _childPolicyDescriptor = descriptor["child_policy"] as? [[String: Any]] ?? [] let _tags = descriptor["tags"] as? [String] ?? [] @@ -98,7 +98,7 @@ extension RootProperties: Descriptable { self.publisherFQDN = _publisherFQDN self.title = _title self.description = _description - + self.category = _category self.childPolicy = _childPolicyDescriptor.compactMap { try? VatomChildPolicy(from: $0) } self.clonedFrom = _clonedFrom @@ -120,46 +120,46 @@ extension RootProperties: Descriptable { self.isDropped = _isDropped self.isTradeable = _isTradeable self.isTransferable = _isTransferable - + self.geoPosition = try GeoPosition(from: _geoPositionDescriptor) self.resources = _resourceDescriptor.compactMap { try? VatomResourceModel(from: $0) } - + } - + } extension RootProperties.Visibility: Descriptable { - - init(from descriptor: [String : Any]) throws { + + init(from descriptor: [String: Any]) throws { guard let _type = descriptor["type"] as? String, let _value = descriptor["value"] as? String else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - + self.type = _type self.value = _value - + } - + } extension RootProperties.Commerce: Descriptable { - - init(from descriptor: [String : Any]) throws { + + init(from descriptor: [String: Any]) throws { guard let _pricingDescriptor = descriptor["pricing"] as? [String: Any], let _vatomPricing = try? VatomPricing(from: _pricingDescriptor) else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - + self.pricing = _vatomPricing } - + } extension VatomPricing: Descriptable { - init(from descriptor: [String : Any]) throws { + init(from descriptor: [String: Any]) throws { guard let _pricingType = descriptor["pricingType"] as? String, let _valueDescriptor = descriptor["value"] as? [String: Any], @@ -182,8 +182,8 @@ extension VatomPricing: Descriptable { extension VatomChildPolicy: Descriptable { - init(from descriptor: [String : Any]) throws { - + init(from descriptor: [String: Any]) throws { + guard let _count = descriptor["count"] as? Int, let _creationPolicyDescriptor = descriptor["creation_policy"] as? [String: Any], @@ -198,8 +198,8 @@ extension VatomChildPolicy: Descriptable { } extension VatomChildPolicy.CreationPolicy: Descriptable { - - init(from descriptor: [String : Any]) throws { + + init(from descriptor: [String: Any]) throws { guard let _autoCreate = descriptor["auto_create"] as? String, let _autoCreateCount = descriptor["auto_create_count"] as? Int, @@ -217,14 +217,14 @@ extension VatomChildPolicy.CreationPolicy: Descriptable { self.enforcePolicyCountMin = _enforcePolicyCountMin self.policyCountMax = _policyCountMax self.policyCountMin = _policyCountMin - + } - + } extension RootProperties.GeoPosition: Descriptable { - - init(from descriptor: [String : Any]) throws { + + init(from descriptor: [String: Any]) throws { guard let _type = descriptor["type"] as? String, let _coordinates = descriptor["coordinates"] as? [Double] @@ -233,12 +233,12 @@ extension RootProperties.GeoPosition: Descriptable { self.type = _type self.coordinates = _coordinates } - + } extension VatomResourceModel: Descriptable { - - init(from descriptor: [String : Any]) throws { + + init(from descriptor: [String: Any]) throws { guard let _name = descriptor["name"] as? String, let _type = descriptor["resourceType"] as? String, @@ -249,128 +249,7 @@ extension VatomResourceModel: Descriptable { self.name = _name self.type = _type self.url = _url - - } - -} - -// MARK: - FaceModel + Descriptable - -extension FaceModel: Descriptable { - - init(from descriptor: [String : Any]) throws { - - guard - let _id = descriptor["id"] as? String, - let _templateID = descriptor["template"] as? String, - let _metaDescriptor = descriptor["meta"] as? [String: Any], - let _propertiesDescriptor = descriptor["properties"] as? [String: Any] - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - self.id = _id - self.templateID = _templateID - self.properties = try Properties(from: _propertiesDescriptor) - self.meta = try MetaModel(from: _metaDescriptor) - // convenience - isNative = properties.displayURL.hasPrefix("native://") - isWeb = properties.displayURL.hasPrefix("https://") - - } - - -} - -extension FaceModel.Properties: Descriptable { - - init(from descriptor: [String : Any]) throws { - guard - let _displayURL = descriptor["display_url"] as? String, - let _constraintsDescriptor = descriptor["constraints"] as? [String: Any], - let _resources = descriptor["resources"] as? [String] - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - let _config = descriptor["config"] as? [String: Any] - - self.displayURL = _displayURL - self.constraints = try FaceModel.Properties.Constraints(from: _constraintsDescriptor) - self.resources = _resources - self.config = try? JSON(_config) - - } - -} - -extension FaceModel.Properties.Constraints: Descriptable { - - init(from descriptor: [String : Any]) throws { - guard - let _viewMode = descriptor["view_mode"] as? String, - let _platform = descriptor["platform"] as? String - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - self.viewMode = _viewMode - self.platform = _platform - - } - -} -extension MetaModel: Descriptable { - - init(from descriptor: [String : Any]) throws { - guard - let _createdBy = descriptor["created_by"] as? String, - let _dataType = descriptor["data_type"] as? String, - let _whenCreated = descriptor["when_created"] as? String, - let _whenModified = descriptor["when_modified"] as? String, - let _whenCreatedDate = DateFormatter.blockvDateFormatter.date(from: _whenCreated), - let _whenModifiedDate = DateFormatter.blockvDateFormatter.date(from: _whenModified) - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - self.init(createdBy: _createdBy, - dataType: _dataType, - whenCreated: _whenCreatedDate, - whenModified: _whenModifiedDate) - - } - -} - -// MARK: - FaceModel + Descriptable - -extension ActionModel: Descriptable { - - init(from descriptor: [String : Any]) throws { - - guard - let _compoundName = descriptor["name"] as? String, - let _metaDescriptor = descriptor["meta"] as? [String: Any], - let _propertiesDescriptor = descriptor["properties"] as? [String: Any] - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - let (templateID, actionName) = try ActionModel.splitCompoundName(_compoundName) - let meta = try MetaModel(from: _metaDescriptor) - let properties = try Properties(from: _propertiesDescriptor) - - self.init(compoundName: _compoundName, - name: actionName, - templateID: templateID, - meta: meta, - properties: properties) - - } - - -} + } -extension ActionModel.Properties: Descriptable { - - init(from descriptor: [String : Any]) throws { - guard - let _reactor = descriptor["reactor"] as? String - else { throw BVError.modelDecoding(reason: "Model decoding failed.") } - - self.init(reactor: _reactor) - } - } diff --git a/BlockV/Core/Network/Models/Package/VatomModel+Update.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/VatomModel+Update.swift rename to BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift diff --git a/BlockV/Core/Network/Models/Package/VatomModel.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/VatomModel.swift rename to BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift diff --git a/BlockV/Core/Network/Models/Package/VatomResourceModel.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomResourceModel.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/VatomResourceModel.swift rename to BlockV/Core/Network/Models/Package/Vatom/VatomResourceModel.swift diff --git a/BlockV/Core/Network/Models/Package/VatomUpdateModel.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomUpdateModel.swift similarity index 100% rename from BlockV/Core/Network/Models/Package/VatomUpdateModel.swift rename to BlockV/Core/Network/Models/Package/Vatom/VatomUpdateModel.swift From 78c5c3466c9062f4f0f24c3e33b187597527b0ff Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 1 May 2019 21:52:06 +0200 Subject: [PATCH 086/165] Fix folder containment --- BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift index 94a17d9f..d008e348 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift @@ -140,13 +140,13 @@ extension VatomModel { if self.props.rootType == "vAtom::vAtomType" { return .standard } else { - if self.props.rootType.hasSuffix("FolderContainerType") { + if self.props.rootType.hasSuffix("::FolderContainerType") { return .container(.folder) - } else if self.props.rootType.hasSuffix("PackageContainerType") { + } else if self.props.rootType.hasSuffix("::PackageContainerType") { return .container(.package) - } else if self.props.rootType.hasSuffix("DiscoverContainerType") { + } else if self.props.rootType.hasSuffix("::DiscoverContainerType") { return .container(.discover) - } else if self.props.rootType.hasSuffix("DefinedFolderContainerType") { + } else if self.props.rootType.hasSuffix("::DefinedFolderContainerType") { return .container(.defined) } else { return .unknown From b3ccedd830a460fb382fb3ae73e295b390bab8ff Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 1 May 2019 21:52:20 +0200 Subject: [PATCH 087/165] Fix action name --- BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift index a95a96b5..479fc5b3 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+PreemptiveActions.swift @@ -79,7 +79,7 @@ extension VatomModel { let undo = DataPool.inventory().preemptiveRemove(id: self.id) // perform the action - self.performAction("Redeem", payload: body, undos: [undo], completion: completion) + self.performAction("Activate", payload: body, undos: [undo], completion: completion) } From 36b4fc167a338e9a34aad86a6c27804291a7059e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 2 May 2019 09:33:58 +0200 Subject: [PATCH 088/165] Fix issue with reuse pool where the vatom of the error view was not updated Remove matching face model check --- BlockV/Face/Vatom View/VatomView.swift | 100 ++++++++++--------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 6b0e2fee..2f5ffa9d 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -74,6 +74,7 @@ open class VatomView: UIView { self.loaderView.startAnimating() self.errorView.isHidden = true case .error: + self.errorView.vatom = vatom self.loaderView.isHidden = true self.loaderView.stopAnimating() self.errorView.isHidden = false @@ -273,6 +274,7 @@ open class VatomView: UIView { errorView.autoresizingMask = [.flexibleHeight, .flexibleWidth] self.state = .loading + } // MARK: - State Management @@ -340,6 +342,8 @@ open class VatomView: UIView { */ guard let vatom = vatom else { + self.selectedFaceView?.removeFromSuperview() + self.selectedFaceView = nil self.state = .error self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) assertionFailure("Developer error: vatom must not be nil.") @@ -349,94 +353,66 @@ open class VatomView: UIView { //TODO: Offload FSP to a background queue. // 1. select the best face model - guard let selectedFaceModel = procedure(vatom, Set(roster.keys)) else { + guard let newSelectedFaceModel = procedure(vatom, Set(roster.keys)) else { /* Error - Case 1 - Show the error view if the FSP fails to select a face view. */ //printBV(error: "Face Selection Procedure (FSP) returned without selecting a face model.") + self.selectedFaceView?.removeFromSuperview() + self.selectedFaceView = nil self.state = .error self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) return } + + //TODO: Add equal face model, view reuse back - //printBV(info: "Face Selection Procedure (FSP) selected face model: \(selectedFaceModel)") + //printBV(info: "Face model changed - Replacing face view.") - /* - Here we check: - - A. If selected face model is still equal to the current. This is necessary since the face may - change as a result of the publisher modifying the face (typically via a delete/add operation). - - B. If the new vatom and the previous vatom share a template variation. This is needed since resources are - defined at the template variation level. - */ + // replace currently selected face model + self.selectedFaceModel = newSelectedFaceModel - // 2. check if the face model has not changed - if (vatom.props.templateVariationID == oldVatom?.props.templateVariationID) && - (selectedFaceModel == self.selectedFaceModel) { - - //printBV(info: "Face model unchanged - Updating face view.") - - /* - Although the selected face model has not changed, other items in the vatom may have, these updates - must be passed to the face view to give it a change to update its state. The VVLC should not be re-run - (since the selected face view does not need replacing). - */ - - self.state = .completed - // update currently selected face view (without replacement) - self.selectedFaceView?.vatomChanged(vatom) - // inform delegate - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(self.selectedFaceView!)) + // 3. find face view type + var faceViewType: FaceView.Type? + if newSelectedFaceModel.isWeb { + faceViewType = roster["https://*"] } else { + faceViewType = roster[newSelectedFaceModel.properties.displayURL] + } - //printBV(info: "Face model changed - Replacing face view.") - - // replace currently selected face model - self.selectedFaceModel = selectedFaceModel - - // 3. find face view type - var faceViewType: FaceView.Type? - - if selectedFaceModel.isWeb { - faceViewType = roster["https://*"] - } else { - faceViewType = roster[selectedFaceModel.properties.displayURL] - } - - guard let viewType = faceViewType else { - // viewer developer MUST have registered the face view with the face registry - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) - assertionFailure( - """ + guard let viewType = faceViewType else { + // viewer developer MUST have registered the face view with the face registry + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) + assertionFailure( + """ Developer error: Face Selection Procedure (FSP) selected a face model without an eligible face view being registered. Your FSP MUST check if the face view has been registered with the FaceRegistry. """) - return - } - - //printBV(info: "Face view for face model: \(faceViewType)") - - //let selectedFaceView: FaceView = ImageFaceView(vatom: vatom, faceModel: selectedFace, host: self) - let selectedFaceView: FaceView = viewType.init(vatom: vatom, - faceModel: selectedFaceModel) - selectedFaceView.delegate = self + return + } + + //printBV(info: "Face view for face model: \(faceViewType)") - // replace currently selected face view with newly selected - self.replaceFaceView(with: selectedFaceView) - // inform delegate - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(selectedFaceView)) + //let selectedFaceView: FaceView = ImageFaceView(vatom: vatom, faceModel: selectedFace, host: self) + let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, + faceModel: newSelectedFaceModel) + newSelectedFaceView.delegate = self - } + // replace currently selected face view with newly selected + self.replaceFaceView(with: newSelectedFaceView) + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(newSelectedFaceView)) } /// Replaces the current face view (if any) with the specified face view and starts the FVLC. private func replaceFaceView(with newFaceView: (FaceView)) { + DispatchQueue.mainThreadPrecondition() + // update current state self.state = .loading @@ -454,7 +430,7 @@ open class VatomView: UIView { // 1. instruct face view to load its content (must be able to handle being called multiple times). newFaceView.load { [weak self] (error) in - ///printBV(info: "Face view load completion called.") + //printBV(info: "Face view load completion called.") guard let self = self else { return } From 8487f8eee4412f704a5ae6b98ace6f6e41e6e7ed Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 2 May 2019 09:41:13 +0200 Subject: [PATCH 089/165] Add image scaling to lower memory impact --- BlockV/Face/Extensions/UIImageView+Ext.swift | 12 +++++++ .../Image Layered/ImageLayeredFaceView.swift | 18 +++++----- .../Image Policy/ImagePolicyFaceView.swift | 7 +++- .../ImageProgressFaceView.swift | 9 +---- .../Face/Face Views/Image/ImageFaceView.swift | 36 +++++++++++-------- BlockV/Face/Vatom View/DefaultErrorView.swift | 28 +++++++++++---- 6 files changed, 70 insertions(+), 40 deletions(-) diff --git a/BlockV/Face/Extensions/UIImageView+Ext.swift b/BlockV/Face/Extensions/UIImageView+Ext.swift index c1c3a243..1531b92a 100644 --- a/BlockV/Face/Extensions/UIImageView+Ext.swift +++ b/BlockV/Face/Extensions/UIImageView+Ext.swift @@ -21,3 +21,15 @@ extension UIImageView { } } + +extension BaseFaceView { + + /// Size of the bounds of the view in pixels. + public var pixelSize: CGSize { + get { + return CGSize(width: self.bounds.size.width * UIScreen.main.scale, + height: self.bounds.size.height * UIScreen.main.scale) + } + } + +} diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 5451c644..6ddbfb44 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -128,14 +128,8 @@ class ImageLayeredFaceView: FaceView { /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - if self.vatom.id == vatom.id { - // replace vatom, update UI - self.vatom = vatom - } else { - // replace vatom, reset and update UI - self.vatom = vatom - self.reset() - } + // replace vatom + self.vatom = vatom // update ui self.updateLayers() @@ -206,7 +200,9 @@ class ImageLayeredFaceView: FaceView { return layer } - var request = ImageRequest(url: encodeURL) + var request = ImageRequest(url: encodeURL, + targetSize: pixelSize, + contentMode: .aspectFit) // use unencoded url as cache key request.cacheKey = resourceModel.url // load image (automatically handles reuse) @@ -272,7 +268,9 @@ class ImageLayeredFaceView: FaceView { // encode url let encodeURL = try BLOCKv.encodeURL(resourceModel.url) - var request = ImageRequest(url: encodeURL) + var request = ImageRequest(url: encodeURL, + targetSize: pixelSize, + contentMode: .aspectFit) // use unencoded url as cache key request.cacheKey = resourceModel.url diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index 396628f0..217a8ca4 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -59,6 +59,9 @@ class ImagePolicyFaceView: FaceView { } super.init(vatom: vatom, faceModel: faceModel) + + // enable animated images + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true // add image view self.addSubview(animatedImageView) @@ -208,7 +211,9 @@ class ImagePolicyFaceView: FaceView { // encode url let encodeURL = try BLOCKv.encodeURL(resourceModel.url) - var request = ImageRequest(url: encodeURL) + var request = ImageRequest(url: encodeURL, + targetSize: pixelSize, + contentMode: .aspectFit) // use unencoded url as cache key request.cacheKey = resourceModel.url // load image (automatically handles reuse) diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index ae72bce8..d4b9420e 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -168,14 +168,7 @@ class ImageProgressFaceView: FaceView { /// Updates the backing Vatom and loads the new state. func vatomChanged(_ vatom: VatomModel) { - if self.vatom.id == vatom.id { - // replace vatom, update UI - self.vatom = vatom - } else { - // replace vatom, reset and load - self.vatom = vatom - self.reset() - } + self.vatom = vatom // update ui self.setNeedsLayout() self.updateUI() diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index 1ee43eb1..2bb34abb 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -50,6 +50,9 @@ class ImageFaceView: FaceView { /// ### Legacy Support /// The first resource name in the resources array (if present) is used in place of the activate image. init(_ faceModel: FaceModel) { + + // enable animated images + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true // legacy: overwrite fallback if needed self.imageName ?= faceModel.properties.resources.first @@ -112,13 +115,15 @@ class ImageFaceView: FaceView { /// Inspects the face config first and uses the scale if available. If no face config is found, a simple heuristic /// is used to choose the best content mode. private func updateContentMode() { - + self.animatedImageView.contentMode = configuredContentMode + } + + var configuredContentMode: UIView.ContentMode { // check face config switch config.scale { - case .fill: animatedImageView.contentMode = .scaleAspectFill - case .fit: animatedImageView.contentMode = .scaleAspectFit + case .fill: return .scaleAspectFill + case .fit: return .scaleAspectFit } - } // MARK: - Face View Lifecycle @@ -166,10 +171,7 @@ class ImageFaceView: FaceView { - Thus, no meaningful UI update can be made. */ - // reset content - self.reset() self.vatom = vatom - self.loadResources(completion: nil) } @@ -188,6 +190,14 @@ class ImageFaceView: FaceView { } // MARK: - Resources + + var nukeContentMode: ImageDecompressor.ContentMode { + // check face config, convert to nuke content mode + switch config.scale { + case .fill: return .aspectFill + case .fit: return .aspectFit + } + } private func loadResources(completion: ((Error?) -> Void)?) { @@ -200,13 +210,11 @@ class ImageFaceView: FaceView { do { // encode url let encodeURL = try BLOCKv.encodeURL(resourceModel.url) - - //FIXME: Where should this go? - ImagePipeline.Configuration.isAnimatedImageDataEnabled = true - - //TODO: Should the size of the VatomView be factoring in and the image be resized? - - var request = ImageRequest(url: encodeURL) + // create request + var request = ImageRequest(url: encodeURL, + targetSize: pixelSize, + contentMode: nukeContentMode) + // use unencoded url as cache key request.cacheKey = resourceModel.url diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index 1474bea0..3bbe7d1c 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -75,13 +75,15 @@ internal final class DefaultErrorView: UIView & VatomViewError { activityIndicator.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true - infoButton.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 12).isActive = true - infoButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true + infoButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -12).isActive = true + infoButton.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -12).isActive = true activatedImageView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true activatedImageView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true activatedImageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true activatedImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true + + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true } @@ -97,11 +99,10 @@ internal final class DefaultErrorView: UIView & VatomViewError { private func loadResources() { activityIndicator.startAnimating() - defer { self.activityIndicator.stopAnimating() } // extract error guard let vatom = vatom else { - assertionFailure("vatom must not be nil.") + assertionFailure("Vatom must not be nil.") return } @@ -115,9 +116,10 @@ internal final class DefaultErrorView: UIView & VatomViewError { return } - ImagePipeline.Configuration.isAnimatedImageDataEnabled = true - - var request = ImageRequest(url: encodeURL) + // create request + var request = ImageRequest(url: encodeURL, + targetSize: pixelSize, + contentMode: .aspectFit) // use unencoded url as cache key request.cacheKey = resourceModel.url // load the image (reuse pool is automatically handled) @@ -128,3 +130,15 @@ internal final class DefaultErrorView: UIView & VatomViewError { } } + +extension DefaultErrorView { + + /// Size of the bounds of the view in pixels. + public var pixelSize: CGSize { + get { + return CGSize(width: self.bounds.size.width * UIScreen.main.scale, + height: self.bounds.size.height * UIScreen.main.scale) + } + } + +} From 60b989c883fdd49f6029be4d8d400d421ae6e552 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 2 May 2019 10:19:53 +0200 Subject: [PATCH 090/165] Remove redundant assignment --- BlockV/Face/Vatom View/VatomView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 2f5ffa9d..2d898332 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -74,7 +74,6 @@ open class VatomView: UIView { self.loaderView.startAnimating() self.errorView.isHidden = true case .error: - self.errorView.vatom = vatom self.loaderView.isHidden = true self.loaderView.stopAnimating() self.errorView.isHidden = false From b8cb4475decba466e0a67c0c11794b95fcab68af Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 15 May 2019 15:18:55 +0200 Subject: [PATCH 091/165] Fix child template variation filter --- BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift index d008e348..7c7f0092 100644 --- a/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift +++ b/BlockV/Core/Data Pool/Helpers/Vatom+Containment.swift @@ -112,7 +112,10 @@ extension VatomModel { // check if there is a maximum number of children if policy.creationPolicy.enforcePolicyCountMax { // check if current child count is less then policy max - if policy.creationPolicy.policyCountMax > self.listCachedChildren().count { + let children = self.listCachedChildren().filter { + $0.props.templateVariationID == policy.templateVariationID + } + if policy.creationPolicy.policyCountMax > children.count { return true } } else { From 545dfa1435764fb0fd8fd1c9cbd1fee30ca26426 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 16 May 2019 12:15:02 +0200 Subject: [PATCH 092/165] Add view reuse for vatom-view with matching face model and vatom template variations Fix vatom-view reuse issue cause by load closure updating a vatom view whose vatom has changed --- BlockV/Face/Vatom View/VatomView.swift | 115 ++++++++++++++++++------- 1 file changed, 84 insertions(+), 31 deletions(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 2d898332..405cca0a 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -343,6 +343,7 @@ open class VatomView: UIView { guard let vatom = vatom else { self.selectedFaceView?.removeFromSuperview() self.selectedFaceView = nil + self.selectedFaceModel = nil self.state = .error self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) assertionFailure("Developer error: vatom must not be nil.") @@ -361,50 +362,84 @@ open class VatomView: UIView { //printBV(error: "Face Selection Procedure (FSP) returned without selecting a face model.") self.selectedFaceView?.removeFromSuperview() self.selectedFaceView = nil + self.selectedFaceModel = nil self.state = .error self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) return } - - //TODO: Add equal face model, view reuse back - //printBV(info: "Face model changed - Replacing face view.") + /* + Here we check: + + A. If selected face model is still equal to the current. This is necessary since the face may + change as a result of the publisher modifying the face (typically via a delete/add operation). + + B. If the new vatom and the previous vatom share a template variation. This is needed since resources are + defined at the template variation level. + */ - // replace currently selected face model - self.selectedFaceModel = newSelectedFaceModel +// printBV(info: "Face model changed - Replacing face view.") - // 3. find face view type - var faceViewType: FaceView.Type? + // 2. check if the face model has not changed + + if (vatom.props.templateVariationID == oldVatom?.props.templateVariationID) && + (newSelectedFaceModel == self.selectedFaceModel) { + + //printBV(info: "Face model unchanged - Updating face view.") + + /* + Although the selected face model has not changed, other items in the vatom may have, these updates + must be passed to the face view to give it a change to update its state. The VVLC should not be re-run + (since the selected face view does not need replacing). + */ + + // update currently selected face view (without replacement) + self.selectedFaceView?.vatomChanged(vatom) + // complete + self.state = .completed + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(self.selectedFaceView!)) - if newSelectedFaceModel.isWeb { - faceViewType = roster["https://*"] } else { - faceViewType = roster[newSelectedFaceModel.properties.displayURL] - } - guard let viewType = faceViewType else { - // viewer developer MUST have registered the face view with the face registry - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) - assertionFailure( - """ + // replace the selected face model + self.selectedFaceModel = newSelectedFaceModel + + // 3. find face view type + var faceViewType: FaceView.Type? + + if newSelectedFaceModel.isWeb { + faceViewType = roster["https://*"] + } else { + faceViewType = roster[newSelectedFaceModel.properties.displayURL] + } + + guard let viewType = faceViewType else { + // viewer developer MUST have registered the face view with the face registry + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) + assertionFailure( + """ Developer error: Face Selection Procedure (FSP) selected a face model without an eligible face view being registered. Your FSP MUST check if the face view has been registered with the FaceRegistry. """) - return - } - - //printBV(info: "Face view for face model: \(faceViewType)") + return + } - //let selectedFaceView: FaceView = ImageFaceView(vatom: vatom, faceModel: selectedFace, host: self) - let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, - faceModel: newSelectedFaceModel) - newSelectedFaceView.delegate = self +// printBV(info: "Face view for face model: \(faceViewType)") - // replace currently selected face view with newly selected - self.replaceFaceView(with: newSelectedFaceView) - // inform delegate - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(newSelectedFaceView)) + //let selectedFaceView: FaceView = ImageFaceView(vatom: vatom, faceModel: selectedFace, host: self) + let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, + faceModel: newSelectedFaceModel) + newSelectedFaceView.delegate = self + // replace currently selected face view with newly selected + self.replaceFaceView(with: newSelectedFaceView) + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(newSelectedFaceView)) + + } + + } /// Replaces the current face view (if any) with the specified face view and starts the FVLC. @@ -412,6 +447,9 @@ open class VatomView: UIView { DispatchQueue.mainThreadPrecondition() + // vatom id currectly associated with the vatom view (important for reuse pool) + guard let contextID = self.vatom?.id else { return } + // update current state self.state = .loading @@ -419,8 +457,9 @@ open class VatomView: UIView { self.selectedFaceView?.unload() self.selectedFaceView?.removeFromSuperview() self.selectedFaceView = nil - self.selectedFaceView = newFaceView + // replace with new + self.selectedFaceView = newFaceView // insert face view into the view hierarcy newFaceView.frame = self.bounds newFaceView.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -429,16 +468,30 @@ open class VatomView: UIView { // 1. instruct face view to load its content (must be able to handle being called multiple times). newFaceView.load { [weak self] (error) in - //printBV(info: "Face view load completion called.") - guard let self = self else { return } + /* + Important: + - Since vatom view may be in a reuse pool, and load is async, we must check the underlying vatom has not + changed. + - As the vatom-view comes out of the reuse pool, `update(usingVatom:procedure:)` is called. Since `load` is + async, by the time load's closure executes the underlying vatom may have changed. + */ + guard self.vatom!.id == contextID else { + // vatom-view is no longer displaying the original vatom + printBV(info: "Load completed, but original vatom has changed.") + return + } + // Error - Case 2 - Display error view if the face view encounters an error during its load operation. // ensure no error guard error == nil else { + // face view encountered an error + self.selectedFaceView?.unload() self.selectedFaceView?.removeFromSuperview() + self.selectedFaceView = nil self.state = .error self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) return From 5ac061f9a6487e9504ad24b55b7a4b1755959468 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 16 May 2019 12:23:32 +0200 Subject: [PATCH 093/165] Lint --- BlockV/Face/Vatom View/DefaultErrorView.swift | 6 +++--- BlockV/Face/Vatom View/VatomView.swift | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index 3bbe7d1c..ff2c2c29 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -82,7 +82,7 @@ internal final class DefaultErrorView: UIView & VatomViewError { activatedImageView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true activatedImageView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true activatedImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true - + ImagePipeline.Configuration.isAnimatedImageDataEnabled = true } @@ -132,7 +132,7 @@ internal final class DefaultErrorView: UIView & VatomViewError { } extension DefaultErrorView { - + /// Size of the bounds of the view in pixels. public var pixelSize: CGSize { get { @@ -140,5 +140,5 @@ extension DefaultErrorView { height: self.bounds.size.height * UIScreen.main.scale) } } - + } diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 405cca0a..c10fd90d 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -273,7 +273,7 @@ open class VatomView: UIView { errorView.autoresizingMask = [.flexibleHeight, .flexibleWidth] self.state = .loading - + } // MARK: - State Management @@ -381,7 +381,7 @@ open class VatomView: UIView { // printBV(info: "Face model changed - Replacing face view.") // 2. check if the face model has not changed - + if (vatom.props.templateVariationID == oldVatom?.props.templateVariationID) && (newSelectedFaceModel == self.selectedFaceModel) { @@ -413,7 +413,7 @@ open class VatomView: UIView { } else { faceViewType = roster[newSelectedFaceModel.properties.displayURL] } - + guard let viewType = faceViewType else { // viewer developer MUST have registered the face view with the face registry self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) @@ -436,10 +436,9 @@ open class VatomView: UIView { self.replaceFaceView(with: newSelectedFaceView) // inform delegate self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(newSelectedFaceView)) - + } - } /// Replaces the current face view (if any) with the specified face view and starts the FVLC. From b2dd9e0c9779fd9d14ae7783046df0aab47fefab Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 16 May 2019 14:41:04 +0200 Subject: [PATCH 094/165] remove comment --- BlockV/Face/Vatom View/VatomView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index c10fd90d..2c26ba7d 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -427,7 +427,6 @@ open class VatomView: UIView { // printBV(info: "Face view for face model: \(faceViewType)") - //let selectedFaceView: FaceView = ImageFaceView(vatom: vatom, faceModel: selectedFace, host: self) let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, faceModel: newSelectedFaceModel) newSelectedFaceView.delegate = self From a62a8afd41d4e3c38cdefd06acfef675681334bc Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 17 May 2019 10:21:57 +0200 Subject: [PATCH 095/165] Add AppUpdateModel --- BlockV/Core/Network/Models/AppUpdateModel.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 BlockV/Core/Network/Models/AppUpdateModel.swift diff --git a/BlockV/Core/Network/Models/AppUpdateModel.swift b/BlockV/Core/Network/Models/AppUpdateModel.swift new file mode 100644 index 00000000..37d6e90a --- /dev/null +++ b/BlockV/Core/Network/Models/AppUpdateModel.swift @@ -0,0 +1,8 @@ +// +// AppUpdateModel.swift +// BLOCKv +// +// Created by Cameron McOnie on 2019/05/17. +// + +import Foundation From 88f9877dcd148f19dfcddc5a66573eb00a090ad5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 17 May 2019 10:22:30 +0200 Subject: [PATCH 096/165] Expose getSupportedVersion endpoint --- .../Core/Network/Models/AppUpdateModel.swift | 36 +++++++++++++++++-- .../Network/Stack/API+TypedResponses.swift | 6 ++++ .../Core/Requests/BLOCKv+AuthRequests.swift | 26 ++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Network/Models/AppUpdateModel.swift b/BlockV/Core/Network/Models/AppUpdateModel.swift index 37d6e90a..d8055181 100644 --- a/BlockV/Core/Network/Models/AppUpdateModel.swift +++ b/BlockV/Core/Network/Models/AppUpdateModel.swift @@ -1,8 +1,38 @@ // -// AppUpdateModel.swift -// BLOCKv +// BlockV AG. Copyright (c) 2018, all rights reserved. // -// Created by Cameron McOnie on 2019/05/17. +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. // import Foundation + +public struct AppUpdateModel: Decodable { + + /// Latest supported app version. + public var supportedVersion: String = "0" + + /// Update URL. + public var updateURL: String + + /// Update text. + public var updateText: String? + + enum CodingKeys: String, CodingKey { + case supportedVersion = "supported_version" + case updateURL = "update_url" + case updateText = "update_text" + } + + public init(from decoder: Decoder) throws { + let items = try decoder.container(keyedBy: CodingKeys.self) + supportedVersion = try items.decode(String.self, forKey: .supportedVersion) + updateURL = try items.decode(String.self, forKey: .updateURL) + updateText = try items.decodeIfPresent(String.self, forKey: .updateText) + } + +} diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 252bff7d..5ce9e3fe 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -18,6 +18,12 @@ extension API { /// Namespace for session related endpoints with a typed reponse model. enum Session { + // MARK: - Version & Support + + static func getSupportedVersion() -> Endpoint> { + return Endpoint(path: "/v1/general/app/version") + } + // MARK: - OAuth /// Builds the endpoint to exchange an auth code for session tokens. diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index d7745b5b..57d91496 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -269,5 +269,31 @@ extension BLOCKv { } } + + /// Fetches information regarding app versioning and support. + /// + /// - Parameter result: Complettion handler that is called when the + public static func getSupportedVersion(result: @escaping (Result) -> Void) { + + let endpoint = API.Session.getSupportedVersion() + // send request + self.client.request(endpoint) { innerResult in + + switch innerResult { + case .success(let model): + // model is available + DispatchQueue.main.async { + result(.success(model.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + result(.failure(error)) + } + } + + } + + } } From 4209d7bd904a6fdcc1d6b8020f7064fc0dd05816 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 22 May 2019 17:36:06 +0200 Subject: [PATCH 097/165] Add push token endpoint Lint --- .../Network/Stack/API+TypedResponses.swift | 123 ++++++++++-------- .../Core/Requests/BLOCKv+AuthRequests.swift | 44 ++++++- 2 files changed, 108 insertions(+), 59 deletions(-) diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 5ce9e3fe..9d083e13 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -17,15 +17,32 @@ extension API { /// Namespace for session related endpoints with a typed reponse model. enum Session { - + + // MARK: - Push Notifications + + /// Builds the endpoint to update the push notification settings. + static func updatePushNotification(fcmToken: String, + platformID: String = "ios", + enabled: Bool = true) -> Endpoint> { + return Endpoint(method: .post, + path: "v1/user/pushnotification", + parameters: [ + "fcm_token": fcmToken, + "platform_id": platformID, + "on": enabled + ]) + + } + // MARK: - Version & Support - + + /// Builds the endpoint to fetch the support application version and update metadata. static func getSupportedVersion() -> Endpoint> { return Endpoint(path: "/v1/general/app/version") } - + // MARK: - OAuth - + /// Builds the endpoint to exchange an auth code for session tokens. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -88,30 +105,30 @@ extension API { /// Namespace for current user related endpoints with a typed reponse model. enum CurrentUser { - + private static let currentUserPath = "/v1/user" - + /// Builds the endpoint to get the current user's properties. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func get() -> Endpoint> { return Endpoint(path: currentUserPath) } - + /// Builds the endpoint to get the current user's tokens. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getTokens() -> Endpoint> { return Endpoint(path: currentUserPath + "/tokens") } - + /// Builds the endpoint to log out the current user. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func logOut() -> Endpoint> { return Endpoint(method: .post, path: currentUserPath + "/logout") } - + /// Builds the endpoint to update current user's information. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -121,7 +138,7 @@ extension API { parameters: userInfo.toSafeDictionary() ) } - + /// Builds the endpoint to verify a token with an OTP code. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -135,7 +152,7 @@ extension API { ] ) } - + /// Builds the endpoint to reset a user token. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -145,7 +162,7 @@ extension API { parameters: token.toDictionary() ) } - + /// Builds the endpoint to send a verification request for a specific token. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -155,7 +172,7 @@ extension API { parameters: token.toDictionary() ) } - + /// Builds the endpoint to add a token to the current user. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -169,7 +186,7 @@ extension API { ] ) } - + /// Builds the endpoint to delete a token. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -177,7 +194,7 @@ extension API { return Endpoint(method: .delete, path: currentUserPath + "/tokens/\(id)") } - + /// Builds the endpoint to set a default token. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -185,25 +202,25 @@ extension API { return Endpoint(method: .put, path: currentUserPath + "/tokens/\(id)/default") } - + // MARK: Avatar - + /// Builds the endpoint for the user's avatar. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func uploadAvatar(_ imageData: Data) -> UploadEndpoint> { - + let bodyPart = MultiformBodyPart(data: imageData, name: "avatar", fileName: "avatar.png", mimeType: "image/png") return UploadEndpoint(path: "/v1/user/avatar", bodyPart: bodyPart) - + } - + // MARK: Messaging - + /// Builds the endpoint to allow the current user to send a message to a user token. /// /// - Parameters: @@ -217,33 +234,33 @@ extension API { "message": message, "id": userId]) } - + // MARK: DEBUG - + /// DO NOT EXPOSE. ONLY USE FOR TESTING. /// /// Builds the endpoint to allow the current user to be deleted. static func deleteCurrentUser() -> Endpoint> { return Endpoint(method: .delete, path: "/v1/user") } - + } - + /// Namespace for public user related endpoints with a typed reponse model. enum PublicUser { - + /// Builds the endpoint to get a public user's details. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func get(id: String) -> Endpoint> { return Endpoint(path: "/v1/users/\(id)") } - + } - + /// Namespace for vatom related endpoints with a typed reponse model. enum Vatom { - + /// Builds an endpoint to get the current user's inventory. /// /// The inventory call is essentially an optimized discover call. The server-pattern is from the child's @@ -254,9 +271,9 @@ extension API { page: Int = 0, limit: Int = 0) -> Endpoint> { return API.Generic.getInventory(parentID: parentID, page: page, limit: limit) - + } - + /// Builds an endpoint to get a vAtom by its unique identifier. /// /// - Parameter ids: Unique identifier of the vatom. @@ -264,7 +281,7 @@ extension API { static func getVatoms(withIDs ids: [String]) -> Endpoint> { return API.Generic.getVatoms(withIDs: ids) } - + /// Builds the endpoint to trash a vAtom specified by its id. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. @@ -272,9 +289,9 @@ extension API { return Endpoint(method: .post, path: "/v1/user/vatom/trash", parameters: ["this.id": id]) - + } - + /// Builds an endpoint to update a vAtom. /// /// - Parameter payload: Raw payload. @@ -282,7 +299,7 @@ extension API { static func updateVatom(payload: [String: Any]) -> Endpoint> { return API.Generic.updateVatom(payload: payload) } - + /// Builds an endpoint to search for vAtoms. /// /// - Parameter payload: Raw request payload. @@ -290,7 +307,7 @@ extension API { static func discover(_ payload: [String: Any]) -> Endpoint> { return API.Generic.discover(payload) } - + /// Builds an endpoint to geo search for vAtoms (i.e. search for dropped vAtoms). /// /// Use this endpoint to fetch a collection of vAtoms. @@ -307,14 +324,14 @@ extension API { topRightLat: Double, topRightLon: Double, filter: String) -> Endpoint> { - + return API.Generic.geoDiscover(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, topRightLat: topRightLat, topRightLon: topRightLon, filter: filter) } - + /// Builds the endpoint to geo search for vAtom groups (i.e. search for clusters of dropped vAtoms). /// /// Use this endpoint to fetch an collection of groups/annotation indicating the count @@ -334,25 +351,25 @@ extension API { topRightLon: Double, precision: Int, filter: String) -> Endpoint> { - + return API.Generic.geoDiscoverGroups(bottomLeftLat: bottomLeftLat, bottomLeftLon: bottomLeftLon, topRightLat: topRightLat, topRightLon: topRightLon, precision: precision, filter: filter) - + } - + } - + /// Namespace for action related endpoints with a typed reponse model. enum VatomAction { - + /* Each action's reactor returns it's own json payload. This does not need to be mapped as yet. */ - + /// Builds the endpoint to perform and action on a vAtom. /// /// - Parameters: @@ -362,12 +379,12 @@ extension API { static func custom(name: String, payload: [String: Any]) -> Endpoint { return API.Generic.perform(name: name, payload: payload) } - + } - + /// Namespace for user action related endpoints with a typed reponse model. enum UserActions { - + /// Builds the endpoint for fetching the actions configured for a template ID. /// /// - Parameter id: Uniquie identifier of the template. @@ -375,12 +392,12 @@ extension API { static func getActions(forTemplateID id: String) -> Endpoint> { return API.Generic.getActions(forTemplateID: id) } - + } - + /// Namespace for user activity related endpoints with a typed reponse model. enum UserActivity { - + /// Builds the endpoint for fetching the threads involving the current user. /// /// - Parameters: @@ -391,7 +408,7 @@ extension API { static func getThreads(cursor: String, count: Int) -> Endpoint> { return API.Generic.getThreads(cursor: cursor, count: count) } - + /// Builds the endpoint for fetching the message for a specified thread involving the current user. /// /// - Parameters: @@ -404,7 +421,7 @@ extension API { Endpoint> { return API.Generic.getMessages(forThreadId: threadId, cursor: cursor, count: count) } - + } - + } diff --git a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift index 57d91496..5e945e41 100644 --- a/BlockV/Core/Requests/BLOCKv+AuthRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+AuthRequests.swift @@ -269,16 +269,16 @@ extension BLOCKv { } } - + /// Fetches information regarding app versioning and support. /// - /// - Parameter result: Complettion handler that is called when the + /// - Parameter result: Complettion handler that is called when the request is completed. public static func getSupportedVersion(result: @escaping (Result) -> Void) { - + let endpoint = API.Session.getSupportedVersion() // send request self.client.request(endpoint) { innerResult in - + switch innerResult { case .success(let model): // model is available @@ -291,9 +291,41 @@ extension BLOCKv { result(.failure(error)) } } - + + } + + } + + /// Updates the push notification settings for this device. + /// + /// - Parameters: + /// - fcmToken: Firebase cloud messaging token. + /// - platformID: Identifier of the current plaform. Defaults to "ios" - recommended. + /// - enabled: Flag indicating whether push notifications should be sent to this device. Defaults to `true`. + /// - completion: Completion handler that is called when the request is completed. + public static func updatePushNotification(fcmToken: String, + platformID: String, + enabled: Bool, + completion: @escaping ((Error?) -> Void)) { + + let endpoint = API.Session.updatePushNotification(fcmToken: fcmToken, platformID: platformID, enabled: enabled) + // send request + self.client.request(endpoint) { result in + + switch result { + case .success: + DispatchQueue.main.async { + completion(nil) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(error) + } + } + } - + } } From 5a1de88bd5c233112db7fc647a322541d27f0a2f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 23 May 2019 13:50:16 +0200 Subject: [PATCH 098/165] Fix caching policy --- BlockV/Face/Face Views/Web/WebFaceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index dcc4aa21..956b6a68 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -120,7 +120,7 @@ class WebFaceView: FaceView { printBV(error: "Cannot initialise URL from: \(faceURL)") return } - let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 20) + let request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 20) self.webView.load(request) } From 7b36ab3ca99365eaec8ab91f0811420049241c73 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 23 May 2019 15:38:39 +0200 Subject: [PATCH 099/165] Ensure load completion is executed on the main queue --- BlockV/Face/Vatom View/VatomView.swift | 69 ++++++++++++++------------ 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 2c26ba7d..c2526a9b 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -465,41 +465,44 @@ open class VatomView: UIView { // 1. instruct face view to load its content (must be able to handle being called multiple times). newFaceView.load { [weak self] (error) in - + guard let self = self else { return } - - /* - Important: - - Since vatom view may be in a reuse pool, and load is async, we must check the underlying vatom has not - changed. - - As the vatom-view comes out of the reuse pool, `update(usingVatom:procedure:)` is called. Since `load` is - async, by the time load's closure executes the underlying vatom may have changed. - */ - guard self.vatom!.id == contextID else { - // vatom-view is no longer displaying the original vatom - printBV(info: "Load completed, but original vatom has changed.") - return - } - - // Error - Case 2 - Display error view if the face view encounters an error during its load operation. - - // ensure no error - guard error == nil else { - - // face view encountered an error - self.selectedFaceView?.unload() - self.selectedFaceView?.removeFromSuperview() - self.selectedFaceView = nil - self.state = .error - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) - return + + DispatchQueue.main.async { + + /* + Important: + - Since vatom view may be in a reuse pool, and load is async, we must check the underlying vatom has not + changed. + - As the vatom-view comes out of the reuse pool, `update(usingVatom:procedure:)` is called. Since `load` is + async, by the time load's closure executes the underlying vatom may have changed. + */ + guard self.vatom!.id == contextID else { + // vatom-view is no longer displaying the original vatom + printBV(info: "Load completed, but original vatom has changed.") + return + } + + // Error - Case 2 - Display error view if the face view encounters an error during its load operation. + + // ensure no error + guard error == nil else { + + // face view encountered an error + self.selectedFaceView?.unload() + self.selectedFaceView?.removeFromSuperview() + self.selectedFaceView = nil + self.state = .error + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .failure(VVLCError.faceViewLoadFailed) ) + return + } + + // show face + self.state = .completed + // inform delegate + self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(self.selectedFaceView!)) + } - - // show face - self.state = .completed - // inform delegate - self.vatomViewDelegate?.vatomView(self, didLoadFaceView: .success(self.selectedFaceView!)) - } } From 1d56f135f2d4307608736cbc3893025f3069c6c2 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 24 May 2019 10:50:00 +0200 Subject: [PATCH 100/165] Set padding to remove border --- BlockV/Core/Helpers/OAuth/AuthorizationServer.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift index d6f83c8d..f02111bb 100644 --- a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift +++ b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift @@ -53,7 +53,8 @@ public final class AuthorizationServer { URLQueryItem(name: "client_id", value: clientID), URLQueryItem(name: "redirect_uri", value: redirectURI), URLQueryItem(name: "state", value: savedState!), - URLQueryItem(name: "scope", value: scope) + URLQueryItem(name: "scope", value: scope), + URLQueryItem(name: "nopadding", value: "1") ] //TODO: Should the `callbackURLScheme` include more than the redirectURL, e.g. bundle identifier? From 5bb7151d07f10d3ac167b770e35d79e3027b6a76 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 24 May 2019 13:56:30 +0200 Subject: [PATCH 101/165] Add nonpushNotification property --- BlockV/Core/Network/Params/TokenRegisterParams.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Network/Params/TokenRegisterParams.swift b/BlockV/Core/Network/Params/TokenRegisterParams.swift index e0b3d4ce..5c5a7bbd 100644 --- a/BlockV/Core/Network/Params/TokenRegisterParams.swift +++ b/BlockV/Core/Network/Params/TokenRegisterParams.swift @@ -23,6 +23,7 @@ public struct UserInfo: Encodable { public var birthday: String? public var isAvatarPublic: Bool? public var language: String? + public var nonPushNotification: Bool? // email/sms public init(firstName: String? = nil, lastName: String? = nil, @@ -30,7 +31,8 @@ public struct UserInfo: Encodable { password: String? = nil, birthday: String? = nil, isAvatarPublic: Bool? = true, - language: String? = nil) { + language: String? = nil, + nonPushNotification: Bool? = false) { self.firstName = firstName self.lastName = lastName @@ -39,6 +41,7 @@ public struct UserInfo: Encodable { self.birthday = birthday self.isAvatarPublic = isAvatarPublic self.language = language + self.nonPushNotification = nonPushNotification } enum CodingKeys: String, CodingKey { @@ -49,6 +52,7 @@ public struct UserInfo: Encodable { case birthday = "birthday" case isAvatarPublic = "avatar_public" case language = "language" + case nonPushNotification = "nonpush_notification" } } @@ -64,7 +68,8 @@ extension UserInfo: DictionaryCodable { "password": password ?? "", "birthday": birthday ?? "", "avatar_public": isAvatarPublic ?? true, - "language": language ?? "" + "language": language ?? "", + "nonpush_notification": nonPushNotification ?? false ] } @@ -95,6 +100,9 @@ extension UserInfo: DictionaryCodable { if let language = language { params["language"] = language } + if let nonPushNotification = nonPushNotification { + params["nonpush_notification"] = nonPushNotification + } return params } From 09aec4371c80d4ef76abd7f7b1750ce80377ffc6 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 26 May 2019 12:27:53 +0200 Subject: [PATCH 102/165] Add reason associated value --- BlockV/Face/Vatom View/FaceMessageDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Face/Vatom View/FaceMessageDelegate.swift b/BlockV/Face/Vatom View/FaceMessageDelegate.swift index 2b4690ed..50626103 100644 --- a/BlockV/Face/Vatom View/FaceMessageDelegate.swift +++ b/BlockV/Face/Vatom View/FaceMessageDelegate.swift @@ -77,7 +77,7 @@ public extension VatomViewDelegate { public enum VVLCError: Error, CustomStringConvertible { /// Face selection failed. - case faceViewSelectionFailed + case faceViewSelectionFailed(reason: String) /// A face model was selected but no corresponding face view was registered. case unregisteredFaceViewSelected(_ displayURL: String) /// The face view failed to load. From 9c582f1256992fa97316825086039efbdcb7195b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 26 May 2019 12:28:41 +0200 Subject: [PATCH 103/165] Fix crash were selected face view was nil Refactor VVLC into simpler functions --- BlockV/Face/Vatom View/VatomView.swift | 148 ++++++++++++++----------- 1 file changed, 84 insertions(+), 64 deletions(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index c2526a9b..156b8556 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -74,6 +74,8 @@ open class VatomView: UIView { self.loaderView.startAnimating() self.errorView.isHidden = true case .error: + self.selectedFaceView?.removeFromSuperview() + self.selectedFaceView = nil self.loaderView.isHidden = true self.loaderView.stopAnimating() self.errorView.isHidden = false @@ -323,11 +325,19 @@ open class VatomView: UIView { /// Exectues the Vatom View Lifecycle (VVLC) on the current vAtom. /// + /// Two important cases must be handled: + /// A) New instance: selected-face-model, selected-face-view are nil. + /// B) Re-use: selected-face-view not nil. + /// /// 1. Run face selection procedure - /// > Compare the selected face to the current face - /// 2. Create face view - /// 3. Inform the face view to load it's content - /// 4. Display the face view + /// > Compare the selected face view to the current face view + /// + /// New instance (or re-use criteria don't match): + /// A.1. Create face view + /// A.2. Inform the face view to load it's content + /// A.3. Display the face view + /// Re-use: + /// B.1 Update current face view /// /// - Parameter oldVatom: The previous vAtom being visualized by this VatomView. internal func runVVLC(oldVatom: VatomModel? = nil) { @@ -340,51 +350,43 @@ open class VatomView: UIView { Both of these cases indicated developer error. */ + // ensure a vatom has been set guard let vatom = vatom else { - self.selectedFaceView?.removeFromSuperview() - self.selectedFaceView = nil - self.selectedFaceModel = nil self.state = .error - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) - assertionFailure("Developer error: vatom must not be nil.") + let reason = "Developer error: vatom must not be nil." + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: + .failure(VVLCError.faceViewSelectionFailed(reason: reason))) + assertionFailure(reason) return } - //TODO: Offload FSP to a background queue. - // 1. select the best face model - guard let newSelectedFaceModel = procedure(vatom, Set(roster.keys)) else { + guard let newFaceModel = procedure(vatom, Set(roster.keys)) else { - /* - Error - Case 1 - Show the error view if the FSP fails to select a face view. - */ - - //printBV(error: "Face Selection Procedure (FSP) returned without selecting a face model.") - self.selectedFaceView?.removeFromSuperview() - self.selectedFaceView = nil - self.selectedFaceModel = nil + // error - case 1 - show the error view if the FSP fails to select a face view self.state = .error - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) + let reason = "Face Selection Procedure (FSP) returned without selecting a face model." + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: + .failure(VVLCError.faceViewSelectionFailed(reason: reason))) return } /* - Here we check: + Here we check the re-use criteria. + + A. A face view has previously been selected (i.e. we are in a re-use flow). - A. If selected face model is still equal to the current. This is necessary since the face may + B. The newly selected face model is still equal to the previous. This is necessary since the face may change as a result of the publisher modifying the face (typically via a delete/add operation). - B. If the new vatom and the previous vatom share a template variation. This is needed since resources are + C. The new vatom and the previous vatom share a common template variation. This is needed since resources are defined at the template variation level. */ -// printBV(info: "Face model changed - Replacing face view.") - - // 2. check if the face model has not changed - - if (vatom.props.templateVariationID == oldVatom?.props.templateVariationID) && - (newSelectedFaceModel == self.selectedFaceModel) { - + if (self.selectedFaceView != nil) && + (newFaceModel == self.selectedFaceModel) && + (vatom.props.templateVariationID == oldVatom?.props.templateVariationID) { + //printBV(info: "Face model unchanged - Updating face view.") /* @@ -397,47 +399,65 @@ open class VatomView: UIView { self.selectedFaceView?.vatomChanged(vatom) // complete self.state = .completed - // inform delegate + // inform delegate the face view is unchanged self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(self.selectedFaceView!)) } else { - - // replace the selected face model - self.selectedFaceModel = newSelectedFaceModel - - // 3. find face view type - var faceViewType: FaceView.Type? - - if newSelectedFaceModel.isWeb { - faceViewType = roster["https://*"] - } else { - faceViewType = roster[newSelectedFaceModel.properties.displayURL] + + //printBV(info: "Face model changed - Creating new face view.") + + do { + let faceView = try createFaceView(forModel: newFaceModel, onVatom: vatom) + faceView.delegate = self + + // replace the selected face model + self.selectedFaceModel = newFaceModel + // replace currently selected face view with newly selected + self.replaceFaceView(with: faceView) + // inform delegate + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(faceView)) + + } catch { + self.state = .error + let reason = "Unable to create face view." + self.vatomViewDelegate?.vatomView(self, didSelectFaceView: + .failure(.faceViewSelectionFailed(reason: reason))) } - guard let viewType = faceViewType else { - // viewer developer MUST have registered the face view with the face registry - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .failure(VVLCError.faceViewSelectionFailed)) - assertionFailure( - """ + } + + } + + /// Finds the face view for the specidfied face model. + private func createFaceView(forModel faceModel: FaceModel, onVatom vatom: VatomModel) throws -> FaceView { + + //TODO: Check the face model is present on the vatom. + + var faceViewType: FaceView.Type? + + if faceModel.isWeb { + // find web face type + faceViewType = roster["https://*"] + } else { + // find native face type + faceViewType = roster[faceModel.properties.displayURL] + } + + guard let viewType = faceViewType else { + + // viewer developer MUST have registered the face view with the face registry + let reason = """ Developer error: Face Selection Procedure (FSP) selected a face model without an eligible face view being registered. Your FSP MUST check if the face view has been registered with the FaceRegistry. - """) - return - } - -// printBV(info: "Face view for face model: \(faceViewType)") - - let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, - faceModel: newSelectedFaceModel) - newSelectedFaceView.delegate = self - - // replace currently selected face view with newly selected - self.replaceFaceView(with: newSelectedFaceView) - // inform delegate - self.vatomViewDelegate?.vatomView(self, didSelectFaceView: .success(newSelectedFaceView)) - + """ + + throw VVLCError.faceViewSelectionFailed(reason: reason) + } - + + let newSelectedFaceView: FaceView = viewType.init(vatom: vatom, faceModel: faceModel) + return newSelectedFaceView + } /// Replaces the current face view (if any) with the specified face view and starts the FVLC. From 2a2ad58777241c83b159e9f54d0d9931cf423f13 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sun, 26 May 2019 12:38:53 +0200 Subject: [PATCH 104/165] Fix safe area content behaviour --- BlockV/Face/Face Views/Web/WebFaceView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BlockV/Face/Face Views/Web/WebFaceView.swift b/BlockV/Face/Face Views/Web/WebFaceView.swift index 956b6a68..cd386a82 100644 --- a/BlockV/Face/Face Views/Web/WebFaceView.swift +++ b/BlockV/Face/Face Views/Web/WebFaceView.swift @@ -53,9 +53,11 @@ class WebFaceView: FaceView { webConfiguration.userContentController.add(LeakAvoider(delegate: self), name: "vatomicBridge") webConfiguration.userContentController.add(LeakAvoider(delegate: self), name: "blockvBridge") - // content controller + // web view let webView = WKWebView(frame: self.bounds, configuration: webConfiguration) webView.navigationDelegate = self + webView.scrollView.isScrollEnabled = false + webView.scrollView.contentInsetAdjustmentBehavior = .never webView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] return webView From 8f402fc8e0ff705b23ef3059645625ddc13e6fe4 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 09:45:05 +0200 Subject: [PATCH 105/165] Comment out print statement --- BlockV/Face/Vatom View/VatomView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Face/Vatom View/VatomView.swift b/BlockV/Face/Vatom View/VatomView.swift index 156b8556..539ed4ca 100644 --- a/BlockV/Face/Vatom View/VatomView.swift +++ b/BlockV/Face/Vatom View/VatomView.swift @@ -499,7 +499,7 @@ open class VatomView: UIView { */ guard self.vatom!.id == contextID else { // vatom-view is no longer displaying the original vatom - printBV(info: "Load completed, but original vatom has changed.") + // printBV(info: "Load completed, but original vatom has changed.") return } From 7bc111341f642a27ef92a51c3e53dac48a4fbe73 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 10:40:45 +0200 Subject: [PATCH 106/165] Improve page fetching --- .../Data Pool/Regions/InventoryRegion.swift | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 693f4b36..4171c18a 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -71,7 +71,7 @@ class InventoryRegion: BLOCKvRegion { self.pauseMessages() // fetch all pages recursively - return self.fetch().ensure { + return self.fetchBatched().ensure { // resume websocket events self.resumeMessages() @@ -192,5 +192,111 @@ class InventoryRegion: BLOCKvRegion { } } + + /// Page size parameter sent to the server. + private let pageSize = 100 + /// Upper bound to prevent infinite recursion. + private let maxReasonablePages = 50 + /// Number of batch iterations. + private var iteration = 0 + /// Number of processed pages. + private var proccessedPageCount = 0 + /// Cummulative object ids. + fileprivate var cummulativeIds: [String] = [] } + +extension InventoryRegion { + + func fetchBatched(maxConcurrent: Int = 4) -> Promise<[String]?> { + + let intialRange: CountableClosedRange = 1...maxConcurrent + return fetchRange(intialRange) + + } + + private func fetchRange(_ range: CountableClosedRange) -> Promise<[String]?> { + + iteration += 1 + + print("[Pager] fetching range \(range) in iteration \(iteration).") + + var promises: [Promise<[String]?>] = [] + + // tracking flag + var shouldRecurse = true + + for i in range { + + // build raw request + let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: i, limit: pageSize) + + // exectute request + let promise = BLOCKv.client.requestJSON(endpoint).then(on: .global(qos: .userInitiated)) { json -> Promise<[String]?> in + + guard let json = json as? [String: Any], + let payload = json["payload"] as? [String: Any] else { + throw NSError.init("Unable to load") //FIXME: Create a better error + } + + // parse out data objects + guard let items = self.parseDataObject(from: payload) else { + return Promise.value([]) + } + let newIds = items.map { $0.id } + + return Promise { (resolver: Resolver) in + + DispatchQueue.main.async { + + // append new ids + self.cummulativeIds.append(contentsOf: newIds) + + // add data objects + self.add(objects: items) + + if (items.count == 0) || (self.proccessedPageCount > self.maxReasonablePages) { + shouldRecurse = false + } + + return resolver.fulfill(newIds) + + } + } + + }.ensure { + // increment page count + self.proccessedPageCount += 1 + } + + promises.append(promise) + + } + + return when(resolved: promises).then { results -> Promise<[String]?> in + + // check stopping condition + if shouldRecurse { + + print("[Pager] recursing.") + + // create the next range + let nextLower = range.upperBound.advanced(by: 1) + let nextUpper = range.upperBound.advanced(by: range.upperBound) // ranges alway have equal width + let nextRange: CountableClosedRange = nextLower...nextUpper + + return self.fetchRange(nextRange) + + } else { + + print("[Pager] stopping condition hit.") + + return Promise.value(self.cummulativeIds) + + } + + } + + } + +} From 79cde029dc0a1359a0e83c6703900d2dbe3e9161 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 11:12:30 +0200 Subject: [PATCH 107/165] Add region error Remove unnecessary code Lint --- .../Data Pool/Regions/InventoryRegion.swift | 129 ++++++------------ 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 4171c18a..d6f8b943 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -80,53 +80,6 @@ class InventoryRegion: BLOCKvRegion { } - /// Fetches all objects from the server. - /// - /// Recursivly pages through the server's pool until all object have been found. - fileprivate func fetch(page: Int = 1, previousItems: [String] = []) -> Promise<[String]?> { - - // stop if closed - if closed { - return Promise.value(previousItems) - } - - // execute it - printBV(info: "[DataPool > InventoryRegion] Loading page \(page), got \(previousItems.count) items so far...") - - // build raw request - let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: page) - - return BLOCKv.client.requestJSON(endpoint).then { json -> Promise<[String]?> in - - guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { - throw NSError.init("Unable to load") //FIXME: Create a better error - } - - // create list of items - var ids = previousItems - - // parse out data objects - guard let items = self.parseDataObject(from: payload) else { - return Promise.value(ids) - } - // append new ids - ids.append(contentsOf: items.map { $0.id }) - - // add data objects - self.add(objects: items) - - // if no more data, stop - if items.count == 0 { - return Promise.value(ids) - } - - // done, get next page - return self.fetch(page: page + 1, previousItems: ids) - - } - - } - /// Called on Web socket message. /// /// Allows super to handle 'state_update', then goes on to process 'inventory' events. @@ -166,12 +119,12 @@ class InventoryRegion: BLOCKvRegion { let object = try? JSONSerialization.jsonObject(with: data), let json = object as? [String: Any], let payload = json["payload"] as? [String: Any] else { - throw NSError.init("Unable to load") //FIXME: Create a better error + throw RegionError.failedParsingRespnse } // parse out objects guard let items = self.parseDataObject(from: payload) else { - throw NSError.init("Unable to parse data") //FIXME: Create a better error + throw RegionError.failedParsingObject } // add new objects @@ -192,7 +145,7 @@ class InventoryRegion: BLOCKvRegion { } } - + /// Page size parameter sent to the server. private let pageSize = 100 /// Upper bound to prevent infinite recursion. @@ -207,96 +160,96 @@ class InventoryRegion: BLOCKvRegion { } extension InventoryRegion { - + func fetchBatched(maxConcurrent: Int = 4) -> Promise<[String]?> { - + let intialRange: CountableClosedRange = 1...maxConcurrent return fetchRange(intialRange) - + } - + private func fetchRange(_ range: CountableClosedRange) -> Promise<[String]?> { - + iteration += 1 - + print("[Pager] fetching range \(range) in iteration \(iteration).") - + var promises: [Promise<[String]?>] = [] - + // tracking flag var shouldRecurse = true - + for i in range { - + // build raw request let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: i, limit: pageSize) - + // exectute request let promise = BLOCKv.client.requestJSON(endpoint).then(on: .global(qos: .userInitiated)) { json -> Promise<[String]?> in - + guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { - throw NSError.init("Unable to load") //FIXME: Create a better error + throw RegionError.failedParsingRespnse } - + // parse out data objects guard let items = self.parseDataObject(from: payload) else { return Promise.value([]) } let newIds = items.map { $0.id } - + return Promise { (resolver: Resolver) in - + DispatchQueue.main.async { - + // append new ids self.cummulativeIds.append(contentsOf: newIds) - + // add data objects self.add(objects: items) - + if (items.count == 0) || (self.proccessedPageCount > self.maxReasonablePages) { shouldRecurse = false } - + return resolver.fulfill(newIds) - + } } - + }.ensure { // increment page count self.proccessedPageCount += 1 } - + promises.append(promise) - + } - - return when(resolved: promises).then { results -> Promise<[String]?> in - + + return when(resolved: promises).then { _ -> Promise<[String]?> in + // check stopping condition if shouldRecurse { - + print("[Pager] recursing.") - - // create the next range + + // create the next range (with equal width) let nextLower = range.upperBound.advanced(by: 1) - let nextUpper = range.upperBound.advanced(by: range.upperBound) // ranges alway have equal width + let nextUpper = range.upperBound.advanced(by: range.upperBound) let nextRange: CountableClosedRange = nextLower...nextUpper - + return self.fetchRange(nextRange) - + } else { - + print("[Pager] stopping condition hit.") return Promise.value(self.cummulativeIds) - + } - + } - + } - + } From b364d55d388fc26a88b56dcccb756d01ac5b50a7 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 11:12:58 +0200 Subject: [PATCH 108/165] Add region error --- BlockV/Core/Data Pool/Regions/Region.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index 555c9a21..20ead581 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -42,6 +42,11 @@ import PromiseKit /// - Persistance. public class Region { + enum RegionError: Error { + case failedParsingRespnse + case failedParsingObject + } + /// Constructor required init(descriptor: Any) throws { } From d33a4e01bcaeebc9c305da6fb109cce0607f90cc Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 11:14:16 +0200 Subject: [PATCH 109/165] Fix spelling --- BlockV/Core/Data Pool/Regions/InventoryRegion.swift | 4 ++-- BlockV/Core/Data Pool/Regions/Region.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index d6f8b943..c199792e 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -119,7 +119,7 @@ class InventoryRegion: BLOCKvRegion { let object = try? JSONSerialization.jsonObject(with: data), let json = object as? [String: Any], let payload = json["payload"] as? [String: Any] else { - throw RegionError.failedParsingRespnse + throw RegionError.failedParsingResponse } // parse out objects @@ -189,7 +189,7 @@ extension InventoryRegion { guard let json = json as? [String: Any], let payload = json["payload"] as? [String: Any] else { - throw RegionError.failedParsingRespnse + throw RegionError.failedParsingResponse } // parse out data objects diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index 20ead581..48075c95 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -43,7 +43,7 @@ import PromiseKit public class Region { enum RegionError: Error { - case failedParsingRespnse + case failedParsingResponse case failedParsingObject } From bcea50690537725c9e5c5985ecd253942961e10e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 12:47:16 +0200 Subject: [PATCH 110/165] Minor clean up and refactor --- BlockV/Core/Data Pool/Regions/Region.swift | 41 ++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index 48075c95..b36541a2 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -60,7 +60,7 @@ public class Region { let noCache = false /// All objects currently in our cache. - var objects: [String: DataObject] = [:] + private(set) var objects: [String: DataObject] = [:] /// `true` if data in this region is in sync with the backend. public internal(set) var synchronized = false { @@ -130,21 +130,7 @@ public class Region { // check if subclass returned an array of IDs if let ids = ids { - - // create a list of keys to remove - var keysToRemove: [String] = [] - for id in self.objects.keys { - - // check if it's in our list - if !ids.contains(id) { - keysToRemove.append(id) - } - - } - - // Rrmove objects - self.remove(ids: keysToRemove) - + self.diffedRemove(ids: ids) } // data is up to date @@ -295,6 +281,25 @@ public class Region { } } + + /// Computes a diff between the supplied ids and the current object ids. Removes the stale ids. + func diffedRemove(ids: [String]) { + + // create a diff of keys to remove + var keysToRemove: [String] = [] + for id in self.objects.keys { + + // check if it's in our list + if !ids.contains(id) { + keysToRemove.append(id) + } + + } + + // remove objects + self.remove(ids: keysToRemove) + + } /// Removes the specified objects from our pool. /// @@ -346,9 +351,9 @@ public class Region { public func getAllStable() -> Guarantee<[Any]> { // synchronize now - return self.synchronize().map({ + return self.synchronize().map { return self.getAll() - }) + } } From 85f65c458cba13e8a8679a5d9924c8959795fa6d Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 28 May 2019 13:44:26 +0200 Subject: [PATCH 111/165] Add io serial queue to perform disk io tasks --- BlockV/Core/Data Pool/Regions/Region.swift | 119 ++++++++++++--------- 1 file changed, 68 insertions(+), 51 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index b36541a2..3121ddc3 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -46,6 +46,11 @@ public class Region { case failedParsingResponse case failedParsingObject } + + /// Serial io queue. + /// + /// Each subclass with have it's own io queue (the label does not dictate uniqueness). + let ioQueue = DispatchQueue(label: "io.blockv.sdk.datapool-io", qos: .utility) /// Constructor required init(descriptor: Any) throws { } @@ -109,7 +114,7 @@ public class Region { // remove pending error self.error = nil - self.emit(.updated) + self.emit(.updated) //FIXME: Why is this update broadcast? // stop if already in sync if synchronized { @@ -427,60 +432,72 @@ public class Region { /// Load objects from local storage. func loadFromCache() -> Promise { - - // get filename - let startTime = Date.timeIntervalSinceReferenceDate - let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") - - // get temporary file location - let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) - .appendingPathExtension("json") - - // read data - guard let data = try? Data(contentsOf: file) else { - printBV(error: ("[DataPool > Region] Unable to read cached data")) - return Promise() - } - - // parse JSON - guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[Any]] else { - printBV(error: "[DataPool > Region] Unable to parse cached JSON") - return Promise() - } - - // create objects - let objects = json.map { fields -> DataObject? in - - // get fields - guard let id = fields[0] as? String, let type = fields[1] as? String, - let data = fields[2] as? [String: Any] else { - return nil + + return Promise { (resolver: Resolver) in + + ioQueue.async { + + // get filename + let startTime = Date.timeIntervalSinceReferenceDate + let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") + + // get temporary file location + let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) + .appendingPathExtension("json") + + // read data + guard let data = try? Data(contentsOf: file) else { + printBV(error: ("[DataPool > Region] Unable to read cached data")) + resolver.fulfill_() + return + } + + // parse JSON + guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [[Any]] else { + printBV(error: "[DataPool > Region] Unable to parse cached JSON") + resolver.fulfill_() + return + } + + // create objects + let objects = json.map { fields -> DataObject? in + + // get fields + guard let id = fields[0] as? String, let type = fields[1] as? String, + let data = fields[2] as? [String: Any] else { + return nil + } + + // create DataObject + let obj = DataObject() + obj.id = id + obj.type = type + obj.data = data + return obj + + } + + // Strip out nils + let cleanObjects = objects.compactMap { $0 } + + DispatchQueue.main.async { + // add objects + self.add(objects: cleanObjects) + } + + // done + let delay = (Date.timeIntervalSinceReferenceDate - startTime) * 1000 + printBV(info: ("[DataPool > Region] Loaded \(cleanObjects.count) from cache in \(Int(delay))ms")) + resolver.fulfill_() + } - - // create DataObject - let obj = DataObject() - obj.id = id - obj.type = type - obj.data = data - return obj - + } - // Strip out nils - let cleanObjects = objects.compactMap { $0 } - - // add objects - self.add(objects: cleanObjects) - - // done - let delay = (Date.timeIntervalSinceReferenceDate - startTime) * 1000 - printBV(info: ("[DataPool > Region] Loaded \(cleanObjects.count) from cache in \(Int(delay))ms")) - return Promise() - } var saveTask: DispatchWorkItem? - + /// Saves the region to local storage. func save() { @@ -527,12 +544,12 @@ public class Region { // done let delay = (Date.timeIntervalSinceReferenceDate - startTime) * 1000 - printBV(info: ("[DataPool > Region] Saved \(self.objects.count) items to disk in \(Int(delay))ms")) + printBV(info: ("[DataPool > Region] Saved \(self.objects.count) objects to disk in \(Int(delay))ms")) } // Debounce save task - DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: saveTask!) + ioQueue.asyncAfter(deadline: .now() + 5, execute: saveTask!) } From 2787b645a8f0fb956333ff94f37384b251ebd135 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 30 May 2019 21:28:25 +0200 Subject: [PATCH 112/165] Re-connect to the socket on session launch --- BlockV/Core/BLOCKv.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index ef7d2f42..5830fb15 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -291,8 +291,9 @@ public final class BLOCKv { fatalError("Invalid cliam") } - // standup the client + // standup the client & socket _ = client + _ = socket.connect() // standup data pool DataPool.sessionInfo = ["userID": userId] From 6e799068c6d8df2e9300e587872bffe3a5f9bc75 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 11:15:11 +0200 Subject: [PATCH 113/165] Add write ping --- BlockV/Core/Web Socket/WebSocketManager.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/BlockV/Core/Web Socket/WebSocketManager.swift b/BlockV/Core/Web Socket/WebSocketManager.swift index 766ba1c3..9ca46d52 100644 --- a/BlockV/Core/Web Socket/WebSocketManager.swift +++ b/BlockV/Core/Web Socket/WebSocketManager.swift @@ -249,6 +249,18 @@ public class WebSocketManager { } + // MARK: Debugging + + /// Writes a ping frame to the socket. + func writePing(data: Data = Data(), completion: @escaping () -> Void) { + + // write a ping control frame + self.socket?.write(ping: data) { + completion() + } + + } + } // MARK: - Extension WebSocket Delegate From b27b24436b94f10a43d5a3d1047a674783229f4e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 11:18:12 +0200 Subject: [PATCH 114/165] Add debug HUD --- Debug/DebugHUD.swift | 144 +++++++++++++++++++++++++ Debug/SocketInstrument.swift | 197 +++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 Debug/DebugHUD.swift create mode 100644 Debug/SocketInstrument.swift diff --git a/Debug/DebugHUD.swift b/Debug/DebugHUD.swift new file mode 100644 index 00000000..c6f9222b --- /dev/null +++ b/Debug/DebugHUD.swift @@ -0,0 +1,144 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import UIKit +import BLOCKv + +/// UIWindow subclass which is designed to be set at the top-level window. +/// +/// All events are passed through to underlying windows expect if the view conforms the `FloatingView` protocol. In this +/// case, the touch events will forwarded to the view itself. +class FloatingHUDWindow: UIWindow { + + init() { + super.init(frame: UIScreen.main.bounds) + backgroundColor = nil + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Determine whether the hit test should be + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + + let hitView = super.hitTest(point, with: event) + // check if this window should handle this event + if hitView!.self is FloatingView { + return hitView + } + // indicates this window does not handle this event + return nil + + } + +} + +/// A view controller which provides a context for a floating HUD. +/// +/// Initialising this view controller will create a floating HUD using an instance of the `FloatingHUDWindow`. +/// +/// HUD content should be added as a subview to the view controller's view. If the view's need to capture touch events +/// they should conform to the `FloatingView` protocol. +class DebugHUDViewController: UIViewController { + + // MARK: - Properties + + lazy var socketContentView: SocketContentView = { + let view = SocketContentView() + view.cornerRadius = 8 + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.gray.withAlphaComponent(0.4) + + return view + }() + + private let window = FloatingHUDWindow() + + // MARK: - Initializer + + init() { + super.init(nibName: nil, bundle: nil) + // window.frame = CGRect(x: 0, y: 0, width: 500, height: 500) + window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude) + window.isHidden = false + window.rootViewController = self + NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), + name: UIResponder.keyboardDidShowNotification, + object: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + self.setup() + } + + var socketContentViewCenterXContraint: NSLayoutConstraint? + var socketContentViewCenterYContraint: NSLayoutConstraint? + + private func setup() { + + // socket content + self.view.addSubview(socketContentView) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panDidFire)) + socketContentView.addGestureRecognizer(panGesture) + + // constraints + socketContentViewCenterXContraint = socketContentView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, + constant: 0) + socketContentViewCenterXContraint?.isActive = true + + socketContentViewCenterYContraint = socketContentView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, + constant: 0) + socketContentViewCenterYContraint?.isActive = true + + socketContentView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.25).isActive = true + socketContentView.widthAnchor.constraint(equalTo: socketContentView.heightAnchor, multiplier: 1).isActive = true + + } + + // MARK: - Section + + @objc func panDidFire(pan: UIPanGestureRecognizer) { + + socketContentView.layoutIfNeeded() + + let offset = pan.translation(in: self.view) + pan.setTranslation(CGPoint.zero, in: self.view) + + socketContentViewCenterXContraint?.constant += offset.x + socketContentViewCenterYContraint?.constant += offset.y + + } + + @objc func keyboardDidShow(note: NSNotification) { + window.windowLevel = UIWindow.Level(rawValue: 0) + window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude) + } + +} + +// MARK: - Helpers + +/// Protocol that indicates a view is a floating view. This allows for hit testing. +protocol FloatingView where Self: UIView { } + +/// Simple subclass of UIView that conforms to FloatingView. Use this subsclass to add interactable content on a +/// floating window. +class ContentView: UIView, FloatingView { } diff --git a/Debug/SocketInstrument.swift b/Debug/SocketInstrument.swift new file mode 100644 index 00000000..63748b6f --- /dev/null +++ b/Debug/SocketInstrument.swift @@ -0,0 +1,197 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +enum HUDStatus { + case green + case orange + case red +} + +/// Controller object that interacts with the web socket. +class SocketInstrument { + + private let pingInterval: TimeInterval = 5 + + private var timer: Timer! + + fileprivate var statusHandler: ((_ status: HUDStatus) -> Void)? + + init() { + + // listen to web socket lifecycle events + BLOCKv.socket.onDisconnected.subscribe(with: self) { [weak self] _ in + self?.statusHandler?(.red) + } + + BLOCKv.socket.onConnected.subscribe(with: self) { [weak self] _ in + self?.statusHandler?(.green) + } + + DispatchQueue.main.async { + self.activate(pingInterval: self.pingInterval) + } + } + + private func activate(pingInterval: TimeInterval) { + + // create timer + self.timer = Timer.scheduledTimer(withTimeInterval: pingInterval, repeats: true) { [weak self] timer in + + /* + Technical: + A pong must + */ + + var didTimeout = false + + BLOCKv.socket.writePing(completion: { + + if didTimeout { + self?.statusHandler?(.red) + } else { + self?.statusHandler?(.green) + } + + }) + + // fire a timeout 0.1 seconds before the next ping + DispatchQueue.main.asyncAfter(deadline: .now() + pingInterval - 0.1, execute: { + didTimeout = true + }) + + } + + } + + +} + +// MARK: - Socket Content View + +class SocketContentView: RoundedView { + + // MARK: - Properties + + private var didSetupConstraints = false + + lazy var statusDotView: CircleView = { + let view = CircleView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .red + return view + }() + + // controller object to interact with the model + var socketInstrument: SocketInstrument! + + // MARK: - Initializer + + init() { + super.init(frame: CGRect.zero) + self.commonInit() + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func commonInit() { + self.addSubview(statusDotView) + self.setNeedsUpdateConstraints() + self.socketInstrument = SocketInstrument() + + socketInstrument.statusHandler = pulse(status:) + } + + // MARK: - Lifecycle + + override func updateConstraints() { + + if !didSetupConstraints { + + statusDotView.widthAnchor.constraint(equalToConstant: 12).isActive = true + statusDotView.heightAnchor.constraint(equalToConstant: 12).isActive = true + statusDotView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 12).isActive = true + statusDotView.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true + + didSetupConstraints = true + } + + super.updateConstraints() + } + + // MARK: - Updates + + private func pulse(status: HUDStatus) { + switch status { + case .green: + statusDotView.backgroundColor = .green + case .orange: + statusDotView.backgroundColor = .orange + case .red: + statusDotView.backgroundColor = .red + } + statusDotView.pulse() + } + +} + +// MARK: - Helper Views + +/// Create a view with rounded edges. +class RoundedView: ContentView { + + var cornerRadius: CGFloat = 0 + + override func layoutSubviews() { + super.layoutSubviews() + self.layer.cornerRadius = cornerRadius + } + +} + +/// Creates a round UIView. +class CircleView: UIView { + + override func layoutSubviews() { + super.layoutSubviews() + self.layer.cornerRadius = self.bounds.size.width/2 + } + +} + +extension CircleView: Pulseable { } + +protocol Pulseable where Self: UIView { + func pulse() +} + +extension Pulseable { + + func pulse() { + let pulseAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity)) + pulseAnimation.duration = 1 + pulseAnimation.fromValue = 1 + pulseAnimation.toValue = 0.2 + pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) + pulseAnimation.autoreverses = true + pulseAnimation.repeatCount = 1 + self.layer.add(pulseAnimation, forKey: "animateOpacity") + } + +} From 9cb3088a44d5808f7da3594bfd261473c7b9fa0e Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 11:24:12 +0200 Subject: [PATCH 115/165] Lint --- .../Core/Debug/DebugHUDViewController.swift | 65 +++++++------ .../Core/Debug}/SocketInstrument.swift | 91 +++++++++---------- 2 files changed, 77 insertions(+), 79 deletions(-) rename Debug/DebugHUD.swift => BlockV/Core/Debug/DebugHUDViewController.swift (93%) rename {Debug => BlockV/Core/Debug}/SocketInstrument.swift (92%) diff --git a/Debug/DebugHUD.swift b/BlockV/Core/Debug/DebugHUDViewController.swift similarity index 93% rename from Debug/DebugHUD.swift rename to BlockV/Core/Debug/DebugHUDViewController.swift index c6f9222b..0aa256bc 100644 --- a/Debug/DebugHUD.swift +++ b/BlockV/Core/Debug/DebugHUDViewController.swift @@ -17,19 +17,19 @@ import BLOCKv /// All events are passed through to underlying windows expect if the view conforms the `FloatingView` protocol. In this /// case, the touch events will forwarded to the view itself. class FloatingHUDWindow: UIWindow { - + init() { super.init(frame: UIScreen.main.bounds) backgroundColor = nil } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + /// Determine whether the hit test should be override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - + let hitView = super.hitTest(point, with: event) // check if this window should handle this event if hitView!.self is FloatingView { @@ -37,9 +37,9 @@ class FloatingHUDWindow: UIWindow { } // indicates this window does not handle this event return nil - + } - + } /// A view controller which provides a context for a floating HUD. @@ -49,25 +49,24 @@ class FloatingHUDWindow: UIWindow { /// HUD content should be added as a subview to the view controller's view. If the view's need to capture touch events /// they should conform to the `FloatingView` protocol. class DebugHUDViewController: UIViewController { - + // MARK: - Properties - + lazy var socketContentView: SocketContentView = { let view = SocketContentView() view.cornerRadius = 8 view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = UIColor.gray.withAlphaComponent(0.4) - + return view }() - + private let window = FloatingHUDWindow() - + // MARK: - Initializer - + init() { super.init(nibName: nil, bundle: nil) - // window.frame = CGRect(x: 0, y: 0, width: 500, height: 500) window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude) window.isHidden = false window.rootViewController = self @@ -75,69 +74,69 @@ class DebugHUDViewController: UIViewController { name: UIResponder.keyboardDidShowNotification, object: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Life cycle - + override func viewDidLoad() { super.viewDidLoad() - + self.setup() } - + var socketContentViewCenterXContraint: NSLayoutConstraint? var socketContentViewCenterYContraint: NSLayoutConstraint? - + private func setup() { - + // socket content self.view.addSubview(socketContentView) - + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panDidFire)) socketContentView.addGestureRecognizer(panGesture) - + // constraints socketContentViewCenterXContraint = socketContentView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor, constant: 0) socketContentViewCenterXContraint?.isActive = true - + socketContentViewCenterYContraint = socketContentView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 0) socketContentViewCenterYContraint?.isActive = true - + socketContentView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.25).isActive = true socketContentView.widthAnchor.constraint(equalTo: socketContentView.heightAnchor, multiplier: 1).isActive = true } - + // MARK: - Section - + @objc func panDidFire(pan: UIPanGestureRecognizer) { - + socketContentView.layoutIfNeeded() - + let offset = pan.translation(in: self.view) pan.setTranslation(CGPoint.zero, in: self.view) - + socketContentViewCenterXContraint?.constant += offset.x socketContentViewCenterYContraint?.constant += offset.y - + } - + @objc func keyboardDidShow(note: NSNotification) { window.windowLevel = UIWindow.Level(rawValue: 0) window.windowLevel = UIWindow.Level(rawValue: CGFloat.greatestFiniteMagnitude) } - + } // MARK: - Helpers /// Protocol that indicates a view is a floating view. This allows for hit testing. -protocol FloatingView where Self: UIView { } +protocol FloatingView where Self: UIView { } /// Simple subclass of UIView that conforms to FloatingView. Use this subsclass to add interactable content on a /// floating window. diff --git a/Debug/SocketInstrument.swift b/BlockV/Core/Debug/SocketInstrument.swift similarity index 92% rename from Debug/SocketInstrument.swift rename to BlockV/Core/Debug/SocketInstrument.swift index 63748b6f..8c1ee731 100644 --- a/Debug/SocketInstrument.swift +++ b/BlockV/Core/Debug/SocketInstrument.swift @@ -19,124 +19,123 @@ enum HUDStatus { /// Controller object that interacts with the web socket. class SocketInstrument { - + private let pingInterval: TimeInterval = 5 - + private var timer: Timer! - + fileprivate var statusHandler: ((_ status: HUDStatus) -> Void)? - + init() { - + // listen to web socket lifecycle events BLOCKv.socket.onDisconnected.subscribe(with: self) { [weak self] _ in self?.statusHandler?(.red) } - + BLOCKv.socket.onConnected.subscribe(with: self) { [weak self] _ in self?.statusHandler?(.green) } - + DispatchQueue.main.async { self.activate(pingInterval: self.pingInterval) } } - + private func activate(pingInterval: TimeInterval) { - + // create timer - self.timer = Timer.scheduledTimer(withTimeInterval: pingInterval, repeats: true) { [weak self] timer in - + self.timer = Timer.scheduledTimer(withTimeInterval: pingInterval, repeats: true) { [weak self] _ in + /* Technical: A pong must */ - + var didTimeout = false - + BLOCKv.socket.writePing(completion: { - + if didTimeout { self?.statusHandler?(.red) } else { self?.statusHandler?(.green) } - + }) - + // fire a timeout 0.1 seconds before the next ping DispatchQueue.main.asyncAfter(deadline: .now() + pingInterval - 0.1, execute: { didTimeout = true }) - + } - + } - - + } // MARK: - Socket Content View class SocketContentView: RoundedView { - + // MARK: - Properties - + private var didSetupConstraints = false - + lazy var statusDotView: CircleView = { let view = CircleView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .red return view }() - + // controller object to interact with the model var socketInstrument: SocketInstrument! - + // MARK: - Initializer - + init() { super.init(frame: CGRect.zero) self.commonInit() } - + override init(frame: CGRect) { super.init(frame: frame) self.commonInit() } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func commonInit() { self.addSubview(statusDotView) self.setNeedsUpdateConstraints() self.socketInstrument = SocketInstrument() - + socketInstrument.statusHandler = pulse(status:) } - + // MARK: - Lifecycle - + override func updateConstraints() { - + if !didSetupConstraints { - + statusDotView.widthAnchor.constraint(equalToConstant: 12).isActive = true statusDotView.heightAnchor.constraint(equalToConstant: 12).isActive = true statusDotView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 12).isActive = true statusDotView.topAnchor.constraint(equalTo: self.topAnchor, constant: 12).isActive = true - + didSetupConstraints = true } - + super.updateConstraints() } - + // MARK: - Updates - + private func pulse(status: HUDStatus) { switch status { case .green: @@ -148,31 +147,31 @@ class SocketContentView: RoundedView { } statusDotView.pulse() } - + } // MARK: - Helper Views /// Create a view with rounded edges. class RoundedView: ContentView { - + var cornerRadius: CGFloat = 0 - + override func layoutSubviews() { super.layoutSubviews() self.layer.cornerRadius = cornerRadius } - + } /// Creates a round UIView. class CircleView: UIView { - + override func layoutSubviews() { super.layoutSubviews() self.layer.cornerRadius = self.bounds.size.width/2 } - + } extension CircleView: Pulseable { } @@ -182,7 +181,7 @@ protocol Pulseable where Self: UIView { } extension Pulseable { - + func pulse() { let pulseAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity)) pulseAnimation.duration = 1 @@ -193,5 +192,5 @@ extension Pulseable { pulseAnimation.repeatCount = 1 self.layer.add(pulseAnimation, forKey: "animateOpacity") } - + } From 4fd04f305362825beda4597625397f3c53c23842 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 16:56:55 +0200 Subject: [PATCH 116/165] Create a BoundedView to inform its subclass once the bounds have been set Update the cacheKey to include the target size --- BlockV/Face/Vatom View/DefaultErrorView.swift | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index ff2c2c29..2f931eef 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -18,7 +18,7 @@ import Nuke /// Shows: /// 1. vAtoms activated image. /// 2. Warning trigangle (that is tappable). -internal final class DefaultErrorView: UIView & VatomViewError { +internal final class DefaultErrorView: BoundedView & VatomViewError { // MARK: - Debug @@ -59,7 +59,8 @@ internal final class DefaultErrorView: UIView & VatomViewError { var vatom: VatomModel? { didSet { - self.loadResources() + // raise flag that layout is needed on the bounds are known + self.requiresBoundsBasedLayout = true } } @@ -90,6 +91,12 @@ internal final class DefaultErrorView: UIView & VatomViewError { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func layoutWithKnownBounds() { + super.layoutWithKnownBounds() + // only at this point is the frame know, and therefore the target size is valid + loadResources() + } // MARK: - Logic @@ -97,6 +104,7 @@ internal final class DefaultErrorView: UIView & VatomViewError { /// /// The error view uses the activated image as a placeholder. private func loadResources() { + print(#function, self.bounds) activityIndicator.startAnimating() @@ -120,9 +128,18 @@ internal final class DefaultErrorView: UIView & VatomViewError { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: .aspectFit) - // use unencoded url as cache key - request.cacheKey = resourceModel.url - // load the image (reuse pool is automatically handled) + + // create a hash for the cacheKey + var hasher = Hasher() + hasher.combine(resourceModel.url) + hasher.combine(pixelSize.width) + hasher.combine(pixelSize.height) + // set cache key + request.cacheKey = hasher.finalize() + + print("CacheKey", request.cacheKey) + + // load image Nuke.loadImage(with: request, into: activatedImageView) { [weak self] (_, _) in self?.activityIndicator.stopAnimating() } @@ -134,7 +151,9 @@ internal final class DefaultErrorView: UIView & VatomViewError { extension DefaultErrorView { /// Size of the bounds of the view in pixels. - public var pixelSize: CGSize { + /// + /// Be sure to call this property *after* the view has been layed out. + var pixelSize: CGSize { get { return CGSize(width: self.bounds.size.width * UIScreen.main.scale, height: self.bounds.size.height * UIScreen.main.scale) @@ -142,3 +161,40 @@ extension DefaultErrorView { } } + + +/// This view class provides a convenient way to know when the bounds of a view have been set. +class BoundedView: UIView { + + /// Setting this value to `true` will trigger a subview layout and ensure that `layoutWithKnowBounds()` is called + /// thereafter. + var requiresBoundsBasedLayout = false { + didSet { + if requiresBoundsBasedLayout { + // trigger a new layout cycle + hasCompletedLayoutSubviews = false + self.setNeedsLayout() + } + } + } + + /// Boolean value indicating whether a layout pass has been completed since `requiresBoundsBasedLayout` + private var hasCompletedLayoutSubviews = false + + override func layoutSubviews() { + super.layoutSubviews() + + if requiresBoundsBasedLayout && !hasCompletedLayoutSubviews { + layoutWithKnownBounds() + hasCompletedLayoutSubviews = true + requiresBoundsBasedLayout = false + } + + } + + /// Called only after `layoutSubviews` has been called (i.e. bounds are set). + func layoutWithKnownBounds() { + // subclass should override + } + +} From 87496bd48c9f78dfb472860b8ed8a3b7adb5994d Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 16:57:16 +0200 Subject: [PATCH 117/165] Update cacheKey to include the target size --- .../Image Layered/ImageLayeredFaceView.swift | 9 +++++++-- .../Image Policy/ImagePolicyFaceView.swift | 10 ++++++++-- .../ImageProgressFaceView.swift | 20 +++++++++++++++---- .../Face/Face Views/Image/ImageFaceView.swift | 11 +++++++--- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 6ddbfb44..312eea83 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -271,8 +271,13 @@ class ImageLayeredFaceView: FaceView { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: .aspectFit) - // use unencoded url as cache key - request.cacheKey = resourceModel.url + // create a hash for the cacheKey + var hasher = Hasher() + hasher.combine(resourceModel.url) + hasher.combine(pixelSize.width) + hasher.combine(pixelSize.height) + // set cache key + request.cacheKey = hasher.finalize() // load image (auto cancel previous) Nuke.loadImage(with: request, into: self.baseLayer) { (_, error) in diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index 217a8ca4..d3429808 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -214,8 +214,14 @@ class ImagePolicyFaceView: FaceView { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: .aspectFit) - // use unencoded url as cache key - request.cacheKey = resourceModel.url + // create a hash for the cacheKey + var hasher = Hasher() + hasher.combine(resourceModel.url) + hasher.combine(pixelSize.width) + hasher.combine(pixelSize.height) + // set cache key + request.cacheKey = hasher.finalize() + // load image (automatically handles reuse) Nuke.loadImage(with: request, into: self.animatedImageView) { (_, error) in self.isLoaded = true diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index d4b9420e..32570166 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -288,16 +288,28 @@ class ImageProgressFaceView: FaceView { dispatchGroup.enter() var requestEmpty = ImageRequest(url: encodedEmptyURL) - // use unencoded url as cache key - requestEmpty.cacheKey = emptyImageResource.url + // create a hash for the cacheKey + var hasherEmpty = Hasher() + hasherEmpty.combine(emptyImageResource.url) + hasherEmpty.combine(pixelSize.width) + hasherEmpty.combine(pixelSize.height) + // set cache key + requestEmpty.cacheKey = hasherEmpty.finalize() + // load image (automatically handles reuse) Nuke.loadImage(with: requestEmpty, into: self.emptyImageView) { (_, _) in self.dispatchGroup.leave() } var requestFull = ImageRequest(url: encodedFullURL) - // use unencoded url as cache key - requestFull.cacheKey = fullImageResource.url + // create a hash for the cacheKey + var hasherFull = Hasher() + hasherFull.combine(fullImageResource.url) + hasherFull.combine(pixelSize.width) + hasherFull.combine(pixelSize.height) + // set cache key + requestFull.cacheKey = hasherFull.finalize() + // load image (automatically handles reuse) Nuke.loadImage(with: requestFull, into: self.fullImageView) { (_, _) in self.dispatchGroup.leave() diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index 2bb34abb..ca7050bb 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -215,9 +215,14 @@ class ImageFaceView: FaceView { targetSize: pixelSize, contentMode: nukeContentMode) - // use unencoded url as cache key - request.cacheKey = resourceModel.url - + // create a hash for the cacheKey + var hasher = Hasher() + hasher.combine(resourceModel.url) + hasher.combine(pixelSize.width) + hasher.combine(pixelSize.height) + // set cache key + request.cacheKey = hasher.finalize() + /* Nuke's `loadImage` cancels any exisitng requests and nils out the old image. This takes care of the reuse-pool use case where the same face view is used to display a vatom of the same template variation. From fd6270a7c4b056b2b907c1bcace4c0a98a5aa05c Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 17:45:56 +0200 Subject: [PATCH 118/165] Add extension to generate a cache key --- BlockV/Face/Extensions/Nuke+AnimatedImage.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/BlockV/Face/Extensions/Nuke+AnimatedImage.swift b/BlockV/Face/Extensions/Nuke+AnimatedImage.swift index bafa3f9c..e6cd5165 100644 --- a/BlockV/Face/Extensions/Nuke+AnimatedImage.swift +++ b/BlockV/Face/Extensions/Nuke+AnimatedImage.swift @@ -33,3 +33,20 @@ extension FLAnimatedImageView { } } } + + +extension ImageRequest { + + /// Generates a cache key based on the specified arguments. + func generateCacheKey(url: URL, targetSize: CGSize? = nil) -> Int { + // create a hash for the cacheKey + var hasher = Hasher() + hasher.combine(url) + if let targetSize = targetSize { + hasher.combine(targetSize.width) + hasher.combine(targetSize.height) + } + return hasher.finalize() + } + +} From 8f5bca62604777d6b66f1ae94a75c785a12f5009 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 17:46:47 +0200 Subject: [PATCH 119/165] Use `generateCacheKey(url:targetSize:)` function --- .../Image Layered/ImageLayeredFaceView.swift | 8 ++------ .../Image Policy/ImagePolicyFaceView.swift | 14 +++++--------- .../Image Progress/ImageProgressFaceView.swift | 18 ++++-------------- .../Face/Face Views/Image/ImageFaceView.swift | 17 ++++++----------- BlockV/Face/Vatom View/DefaultErrorView.swift | 12 ++---------- 5 files changed, 19 insertions(+), 50 deletions(-) diff --git a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift index 312eea83..6f5155e4 100644 --- a/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift +++ b/BlockV/Face/Face Views/Image Layered/ImageLayeredFaceView.swift @@ -271,13 +271,9 @@ class ImageLayeredFaceView: FaceView { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: .aspectFit) - // create a hash for the cacheKey - var hasher = Hasher() - hasher.combine(resourceModel.url) - hasher.combine(pixelSize.width) - hasher.combine(pixelSize.height) + // set cache key - request.cacheKey = hasher.finalize() + request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: pixelSize) // load image (auto cancel previous) Nuke.loadImage(with: request, into: self.baseLayer) { (_, error) in diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index d3429808..d35f811a 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -59,7 +59,7 @@ class ImagePolicyFaceView: FaceView { } super.init(vatom: vatom, faceModel: faceModel) - + // enable animated images ImagePipeline.Configuration.isAnimatedImageDataEnabled = true @@ -104,7 +104,7 @@ class ImagePolicyFaceView: FaceView { if self.vatom.id == vatom.id { // replace vatom, update UI self.vatom = vatom - + } else { // replace vatom, reset and update UI self.vatom = vatom @@ -214,14 +214,10 @@ class ImagePolicyFaceView: FaceView { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: .aspectFit) - // create a hash for the cacheKey - var hasher = Hasher() - hasher.combine(resourceModel.url) - hasher.combine(pixelSize.width) - hasher.combine(pixelSize.height) + // set cache key - request.cacheKey = hasher.finalize() - + request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: pixelSize) + // load image (automatically handles reuse) Nuke.loadImage(with: request, into: self.animatedImageView) { (_, error) in self.isLoaded = true diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 32570166..580ed94c 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -288,28 +288,18 @@ class ImageProgressFaceView: FaceView { dispatchGroup.enter() var requestEmpty = ImageRequest(url: encodedEmptyURL) - // create a hash for the cacheKey - var hasherEmpty = Hasher() - hasherEmpty.combine(emptyImageResource.url) - hasherEmpty.combine(pixelSize.width) - hasherEmpty.combine(pixelSize.height) // set cache key - requestEmpty.cacheKey = hasherEmpty.finalize() - + requestEmpty.cacheKey = requestEmpty.generateCacheKey(url: emptyImageResource.url, targetSize: pixelSize) + // load image (automatically handles reuse) Nuke.loadImage(with: requestEmpty, into: self.emptyImageView) { (_, _) in self.dispatchGroup.leave() } var requestFull = ImageRequest(url: encodedFullURL) - // create a hash for the cacheKey - var hasherFull = Hasher() - hasherFull.combine(fullImageResource.url) - hasherFull.combine(pixelSize.width) - hasherFull.combine(pixelSize.height) // set cache key - requestFull.cacheKey = hasherFull.finalize() - + requestFull.cacheKey = requestFull.generateCacheKey(url: fullImageResource.url, targetSize: pixelSize) + // load image (automatically handles reuse) Nuke.loadImage(with: requestFull, into: self.fullImageView) { (_, _) in self.dispatchGroup.leave() diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index ca7050bb..53c5505f 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -50,7 +50,7 @@ class ImageFaceView: FaceView { /// ### Legacy Support /// The first resource name in the resources array (if present) is used in place of the activate image. init(_ faceModel: FaceModel) { - + // enable animated images ImagePipeline.Configuration.isAnimatedImageDataEnabled = true @@ -117,7 +117,7 @@ class ImageFaceView: FaceView { private func updateContentMode() { self.animatedImageView.contentMode = configuredContentMode } - + var configuredContentMode: UIView.ContentMode { // check face config switch config.scale { @@ -190,7 +190,7 @@ class ImageFaceView: FaceView { } // MARK: - Resources - + var nukeContentMode: ImageDecompressor.ContentMode { // check face config, convert to nuke content mode switch config.scale { @@ -214,15 +214,10 @@ class ImageFaceView: FaceView { var request = ImageRequest(url: encodeURL, targetSize: pixelSize, contentMode: nukeContentMode) - - // create a hash for the cacheKey - var hasher = Hasher() - hasher.combine(resourceModel.url) - hasher.combine(pixelSize.width) - hasher.combine(pixelSize.height) + // set cache key - request.cacheKey = hasher.finalize() - + request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: pixelSize) + /* Nuke's `loadImage` cancels any exisitng requests and nils out the old image. This takes care of the reuse-pool use case where the same face view is used to display a vatom of the same template variation. diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index 2f931eef..40a426ee 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -127,17 +127,10 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { // create request var request = ImageRequest(url: encodeURL, targetSize: pixelSize, - contentMode: .aspectFit) + contentMode: .aspectFill) - // create a hash for the cacheKey - var hasher = Hasher() - hasher.combine(resourceModel.url) - hasher.combine(pixelSize.width) - hasher.combine(pixelSize.height) // set cache key - request.cacheKey = hasher.finalize() - - print("CacheKey", request.cacheKey) + request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: pixelSize) // load image Nuke.loadImage(with: request, into: activatedImageView) { [weak self] (_, _) in @@ -162,7 +155,6 @@ extension DefaultErrorView { } - /// This view class provides a convenient way to know when the bounds of a view have been set. class BoundedView: UIView { From ce639c3327f79a4d1933e29b7c0d1bc106173d55 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 5 Jun 2019 22:17:23 +0200 Subject: [PATCH 120/165] Fix increment of processed page count --- BlockV/Core/Data Pool/Regions/InventoryRegion.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index c199792e..7909f957 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -211,15 +211,15 @@ extension InventoryRegion { if (items.count == 0) || (self.proccessedPageCount > self.maxReasonablePages) { shouldRecurse = false } + + // increment page count + self.proccessedPageCount += 1 return resolver.fulfill(newIds) } } - }.ensure { - // increment page count - self.proccessedPageCount += 1 } promises.append(promise) From 7f40ed4ce109c3a436c0e2331981c105a0591e60 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 6 Jun 2019 09:07:04 +0200 Subject: [PATCH 121/165] Add source key pair to better inform the listen of the source of the update --- BlockV/Core/Data Pool/DataObjectAnimator.swift | 2 +- BlockV/Core/Data Pool/Regions/Region.swift | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Data Pool/DataObjectAnimator.swift b/BlockV/Core/Data Pool/DataObjectAnimator.swift index 05e4a857..b3a1d14f 100644 --- a/BlockV/Core/Data Pool/DataObjectAnimator.swift +++ b/BlockV/Core/Data Pool/DataObjectAnimator.swift @@ -156,7 +156,7 @@ internal class DataObjectAnimator { // execute the changes on all regions for region in self.regions { - region.value?.update(objects: changes) + region.value?.update(objects: changes, source: .brain) } } diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index 3121ddc3..7d619b62 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -236,11 +236,15 @@ public class Region { } } + + enum Source: String { + case brain + } /// Updates data objects within our pool. /// /// - Parameter objects: The list of changes to perform to our data objects. - func update(objects: [DataObjectUpdateRecord]) { + func update(objects: [DataObjectUpdateRecord], source: Source? = nil) { // batch emit events, so if a object is updated multiple times, only one event is sent var changedIDs = Set() @@ -276,7 +280,7 @@ public class Region { // notify each item that was updated for id in changedIDs { - self.emit(.objectUpdated, userInfo: ["id": id]) + self.emit(.objectUpdated, userInfo: ["id": id, "source": source?.rawValue ?? ""]) } // notify overall update From 90156f0d91ecf1063ff3f7f72bc6f3a442858b75 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 7 Jun 2019 17:20:24 +0200 Subject: [PATCH 122/165] Fix transformation and encoding issue preventing ui.qr.scan in v1 of the bridge protocol from working. --- .../Web/WebScriptMessageHandler.swift | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift index 616b4019..437840fc 100644 --- a/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift +++ b/BlockV/Face/Face Views/Web/WebScriptMessageHandler.swift @@ -102,14 +102,19 @@ extension WebFaceView: WKScriptMessageHandler { } // encode response - guard let data = try? JSONEncoder.blockv.encode(response), - let jsonString = String.init(data: data, encoding: .utf8) else { - // handle error - let error = BridgeError.viewer("Unable to encode response.") - self.sendResponse(forRequestMessage: message, result: .failure(error)) - return + if let jsonString = response?.stringValue?.json { + // corner case: ui.qr.scan return a top-level string which JSONEncoder cannot encode + self.postMessage(message.requestID, withJSONString: jsonString) + } else { + guard let data = try? JSONEncoder.blockv.encode(response), + let jsonString = String.init(data: data, encoding: .utf8) else { + // handle error + let error = BridgeError.viewer("Unable to encode response.") + self.sendResponse(forRequestMessage: message, result: .failure(error)) + return + } + self.postMessage(message.requestID, withJSONString: jsonString) } - self.postMessage(message.requestID, withJSONString: jsonString) case .failure(let error): // create response @@ -246,20 +251,28 @@ extension WebFaceView { private func routeMessageToViewer(_ message: RequestScriptMessage) { // notify the host's message delegate of the custom message from the web page - self.delegate?.faceView(self, - didSendMessage: message.name, - withObject: message.payload, - completion: { result in - switch result { - case .success(let payload): - self.sendResponse(forRequestMessage: message, result: .success(payload)) - return - case .failure(let error): - // transform the bridge error - let bridgeError = BridgeError.viewer(error.message) - self.sendResponse(forRequestMessage: message, result: .failure(bridgeError)) - return - } + self.delegate?.faceView(self, didSendMessage: message.name, withObject: message.payload, completion: + { result in + switch result { + case .success(let payload): + + // v1.0.0 backward compatibility + if message.version == "1.0.0" && message.name == "viewer.qr.scan" { + // transform reponse paylaod from ["data": ] to . + if let newPayload = payload["data"] { + self.sendResponse(forRequestMessage: message, result: .success(newPayload)) + return + } + } + + self.sendResponse(forRequestMessage: message, result: .success(payload)) + return + case .failure(let error): + // transform the bridge error + let bridgeError = BridgeError.viewer(error.message) + self.sendResponse(forRequestMessage: message, result: .failure(bridgeError)) + return + } }) } @@ -309,3 +322,16 @@ extension WebFaceView { } } + +fileprivate extension String { + + /// Returns a JSON compatible version of the string. + var json: String { + return "\"" + self + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\r", with: "\\r") + .replacingOccurrences(of: "\n", with: "\\n") + .replacingOccurrences(of: "\"", with: "\\\"") + "\"" + } + +} From 216b41a43f6502fc8db48b5ea74a4d6f7c19a4bb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sat, 8 Jun 2019 12:53:17 +0200 Subject: [PATCH 123/165] Add pixelSize property to UIImageView --- BlockV/Face/Extensions/Nuke+AnimatedImage.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/BlockV/Face/Extensions/Nuke+AnimatedImage.swift b/BlockV/Face/Extensions/Nuke+AnimatedImage.swift index e6cd5165..197a2865 100644 --- a/BlockV/Face/Extensions/Nuke+AnimatedImage.swift +++ b/BlockV/Face/Extensions/Nuke+AnimatedImage.swift @@ -50,3 +50,17 @@ extension ImageRequest { } } + +extension UIImageView { + + /// Size of the bounds of the view in pixels. + /// + /// Be sure to call this property *after* the view has been layed out. + var pixelSize: CGSize { + get { + return CGSize(width: self.bounds.size.width * UIScreen.main.scale, + height: self.bounds.size.height * UIScreen.main.scale) + } + } + +} From bd95b0bcec191fc3f4650e3b25277cc527d0ec13 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sat, 8 Jun 2019 12:53:38 +0200 Subject: [PATCH 124/165] Make FaceView a subclass of BoundedView --- BlockV/Face/Face Views/FaceView.swift | 42 ++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/BlockV/Face/Face Views/FaceView.swift b/BlockV/Face/Face Views/FaceView.swift index e224b748..1f918ca9 100644 --- a/BlockV/Face/Face Views/FaceView.swift +++ b/BlockV/Face/Face Views/FaceView.swift @@ -98,7 +98,7 @@ public protocol FaceViewLifecycle: class { } /// Abstract class all face views must derive from. -open class BaseFaceView: UIView { +open class BaseFaceView: BoundedView { /// Vatom to render. public internal(set) var vatom: VatomModel @@ -130,3 +130,43 @@ enum FaceError: Error { case missingVatomResource case invalidURL } + + +/// This view class provides a convenient way to know when the bounds of a view have been set. +open class BoundedView: UIView { + + /// Setting this value to `true` will trigger a subview layout and ensure that `layoutWithKnowBounds()` is called + /// after the layout. + var requiresBoundsBasedSetup = false { + didSet { + if requiresBoundsBasedSetup { + // trigger a new layout cycle + hasCompletedLayoutSubviews = false + self.setNeedsLayout() + } + } + } + + /// Boolean value indicating whether a layout pass has been completed since `requiresBoundsBasedLayout` + private var hasCompletedLayoutSubviews = false + + override open func layoutSubviews() { + super.layoutSubviews() + + if requiresBoundsBasedSetup && !hasCompletedLayoutSubviews { + setupWithBounds() + hasCompletedLayoutSubviews = true + requiresBoundsBasedSetup = false + } + + } + + /// Called only after `layoutSubviews` has been called (i.e. bounds are set). + /// + /// This function is usefull for cases where the bounds of the view are important, for example, scaling an image + /// to the correct size. + func setupWithBounds() { + // subclass should override + } + +} From 8c57d058d307b6ce0bddb0111d9112012ebcb0bd Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Sat, 8 Jun 2019 12:56:51 +0200 Subject: [PATCH 125/165] Update to use bounded view lifecycle --- .../Face/Face Views/Image/ImageFaceView.swift | 41 ++++++++---- BlockV/Face/Vatom View/DefaultErrorView.swift | 66 +++---------------- 2 files changed, 35 insertions(+), 72 deletions(-) diff --git a/BlockV/Face/Face Views/Image/ImageFaceView.swift b/BlockV/Face/Face Views/Image/ImageFaceView.swift index 53c5505f..064f5499 100644 --- a/BlockV/Face/Face Views/Image/ImageFaceView.swift +++ b/BlockV/Face/Face Views/Image/ImageFaceView.swift @@ -127,6 +127,8 @@ class ImageFaceView: FaceView { } // MARK: - Face View Lifecycle + + private var storedCompletion: ((Error?) -> Void)? /// Begins loading the face view's content. func load(completion: ((Error?) -> Void)?) { @@ -142,20 +144,11 @@ class ImageFaceView: FaceView { // reset content self.reset() - // load required resources - self.loadResources { [weak self] error in - - guard let self = self else { return } - // update state and inform delegate of load completion - if let error = error { - self.isLoaded = false - completion?(error) - } else { - self.isLoaded = true - completion?(nil) - } - - } + // store the completion + self.storedCompletion = completion + // + self.requiresBoundsBasedSetup = true + } /// Updates the backing Vatom and loads the new state. @@ -198,6 +191,26 @@ class ImageFaceView: FaceView { case .fit: return .aspectFit } } + + override func setupWithBounds() { + super.setupWithBounds() + + // load required resources + self.loadResources { [weak self] error in + + guard let self = self else { return } + // update state and inform delegate of load completion + if let error = error { + self.isLoaded = false + self.storedCompletion?(error) + } else { + self.isLoaded = true + self.storedCompletion?(nil) + } + + } + + } private func loadResources(completion: ((Error?) -> Void)?) { diff --git a/BlockV/Face/Vatom View/DefaultErrorView.swift b/BlockV/Face/Vatom View/DefaultErrorView.swift index 40a426ee..740fde87 100644 --- a/BlockV/Face/Vatom View/DefaultErrorView.swift +++ b/BlockV/Face/Vatom View/DefaultErrorView.swift @@ -60,7 +60,7 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { var vatom: VatomModel? { didSet { // raise flag that layout is needed on the bounds are known - self.requiresBoundsBasedLayout = true + self.requiresBoundsBasedSetup = true } } @@ -92,9 +92,9 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { fatalError("init(coder:) has not been implemented") } - override func layoutWithKnownBounds() { - super.layoutWithKnownBounds() - // only at this point is the frame know, and therefore the target size is valid + override func setupWithBounds() { + super.setupWithBounds() + // only at this point is the bounds known which can be used to compute the target size loadResources() } @@ -104,7 +104,6 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { /// /// The error view uses the activated image as a placeholder. private func loadResources() { - print(#function, self.bounds) activityIndicator.startAnimating() @@ -123,14 +122,15 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { guard let encodeURL = try? BLOCKv.encodeURL(resourceModel.url) else { return } - + + let targetSize = self.activatedImageView.pixelSize // create request var request = ImageRequest(url: encodeURL, - targetSize: pixelSize, + targetSize: targetSize, contentMode: .aspectFill) // set cache key - request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: pixelSize) + request.cacheKey = request.generateCacheKey(url: resourceModel.url, targetSize: targetSize) // load image Nuke.loadImage(with: request, into: activatedImageView) { [weak self] (_, _) in @@ -140,53 +140,3 @@ internal final class DefaultErrorView: BoundedView & VatomViewError { } } - -extension DefaultErrorView { - - /// Size of the bounds of the view in pixels. - /// - /// Be sure to call this property *after* the view has been layed out. - var pixelSize: CGSize { - get { - return CGSize(width: self.bounds.size.width * UIScreen.main.scale, - height: self.bounds.size.height * UIScreen.main.scale) - } - } - -} - -/// This view class provides a convenient way to know when the bounds of a view have been set. -class BoundedView: UIView { - - /// Setting this value to `true` will trigger a subview layout and ensure that `layoutWithKnowBounds()` is called - /// thereafter. - var requiresBoundsBasedLayout = false { - didSet { - if requiresBoundsBasedLayout { - // trigger a new layout cycle - hasCompletedLayoutSubviews = false - self.setNeedsLayout() - } - } - } - - /// Boolean value indicating whether a layout pass has been completed since `requiresBoundsBasedLayout` - private var hasCompletedLayoutSubviews = false - - override func layoutSubviews() { - super.layoutSubviews() - - if requiresBoundsBasedLayout && !hasCompletedLayoutSubviews { - layoutWithKnownBounds() - hasCompletedLayoutSubviews = true - requiresBoundsBasedLayout = false - } - - } - - /// Called only after `layoutSubviews` has been called (i.e. bounds are set). - func layoutWithKnownBounds() { - // subclass should override - } - -} From 40c1db65b785c25e2854b9857617237da69a90cb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 11 Jun 2019 22:45:30 +0200 Subject: [PATCH 126/165] Auto reconnect the socket after an unintentional disconnect --- BlockV/Core/Web Socket/WebSocketManager.swift | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Web Socket/WebSocketManager.swift b/BlockV/Core/Web Socket/WebSocketManager.swift index 9ca46d52..0af419c6 100644 --- a/BlockV/Core/Web Socket/WebSocketManager.swift +++ b/BlockV/Core/Web Socket/WebSocketManager.swift @@ -102,6 +102,12 @@ public class WebSocketManager { return decoder }() + /// Time interval between reconnect attempts (measured in seconds). + private let reconnectInterval: TimeInterval = 5 + + /// Timer intendend to trigger reconnects. + private var reconnectTimer: Timer? + /// Web socket instance private var socket: WebSocket? private let baseURLString: String @@ -269,6 +275,11 @@ extension WebSocketManager: WebSocketDelegate { public func websocketDidConnect(socket: WebSocketClient) { printBV(info: "Web socket - Connected") + + // invalidate auto-reconnect timer + self.reconnectTimer?.invalidate() + self.reconnectTimer = nil + self.onConnected.fire(()) } @@ -285,9 +296,20 @@ extension WebSocketManager: WebSocketDelegate { // Fire an error informing the observers that the Web socket has disconnected. self.onDisconnected.fire((nil)) - //TODO: The Web socket should reconnect here: - // The app may fire this message when entering the foreground - // (after the Web socket was disconnected after entering the background). + /* + Note + The app may fire this message when entering the foreground (after the Web socket was disconnected after + entering the background). + */ + + if !shouldAutoConnect { return } + + // attempt to reconnect + self.connect() + // attempt to reconnect in `n` seconds + self.reconnectTimer = Timer.scheduledTimer(withTimeInterval: reconnectInterval, repeats: true, block: { _ in + self.connect() + }) } From c5a1968ab73d804aea0c9723b0aa0da778140fdb Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 12 Jun 2019 12:57:45 +0200 Subject: [PATCH 127/165] Add delay options --- BlockV/Core/Helpers/DelayOptions.swift | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 BlockV/Core/Helpers/DelayOptions.swift diff --git a/BlockV/Core/Helpers/DelayOptions.swift b/BlockV/Core/Helpers/DelayOptions.swift new file mode 100644 index 00000000..cd018d4b --- /dev/null +++ b/BlockV/Core/Helpers/DelayOptions.swift @@ -0,0 +1,40 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +// Adapted from: https://github.com/kean + +enum DelayOption { + /// Zero delay. + case immediate + /// Constant delay. + case constant(time: Double) + /// Exponential backoff delay. + case exponential(initial: Double, base: Double, maxDelay: Double) + /// Custom delay where `attempt` is the itteration count. + case custom(closure: (_ attempt: Int) -> Double) +} + +extension DelayOption { + + /// Returns a delay computed as a function of some iteration counter. + func make(_ attempt: Int) -> Double { + switch self { + case .immediate: return 0.0 + case .constant(let time): return time + case .exponential(let initial, let base, let maxDelay): + // for first attempt, simply use initial delay, otherwise calculate delay + let delay = attempt == 1 ? initial : initial * pow(base, Double(attempt - 1)) + return min(maxDelay, delay) + case .custom(let closure): return closure(attempt) + } + } + +} From 46226c3f215de22b1412830416c8c77559740d21 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 12 Jun 2019 12:57:59 +0200 Subject: [PATCH 128/165] Update to use delay options --- BlockV/Core/Web Socket/WebSocketManager.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Web Socket/WebSocketManager.swift b/BlockV/Core/Web Socket/WebSocketManager.swift index 0af419c6..c84df228 100644 --- a/BlockV/Core/Web Socket/WebSocketManager.swift +++ b/BlockV/Core/Web Socket/WebSocketManager.swift @@ -102,8 +102,11 @@ public class WebSocketManager { return decoder }() - /// Time interval between reconnect attempts (measured in seconds). - private let reconnectInterval: TimeInterval = 5 + /// Delay option enum used to create a reconnect interval (measured in seconds). + let delayOption = DelayOption.exponential(initial: 1, base: 1.2, maxDelay: 5) + + /// Tally of the numnber of reconnect attempts. + private var reconnectCount: Int = 0 /// Timer intendend to trigger reconnects. private var reconnectTimer: Timer? @@ -279,6 +282,7 @@ extension WebSocketManager: WebSocketDelegate { // invalidate auto-reconnect timer self.reconnectTimer?.invalidate() self.reconnectTimer = nil + self.reconnectCount = 0 self.onConnected.fire(()) } @@ -307,7 +311,9 @@ extension WebSocketManager: WebSocketDelegate { // attempt to reconnect self.connect() // attempt to reconnect in `n` seconds - self.reconnectTimer = Timer.scheduledTimer(withTimeInterval: reconnectInterval, repeats: true, block: { _ in + let delay = delayOption.make(reconnectCount) + self.reconnectTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: true, block: { _ in + self.reconnectCount += 1 self.connect() }) From 299f663c1fb27d5e292efafe82397769728cc2c5 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 12 Jun 2019 17:21:19 +0200 Subject: [PATCH 129/165] Add support for bridge 2.1 core.user.current.get --- .../Web/Face Bridge/CoreBridgeV2.swift | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift index 5ef2f14a..b76ad1b1 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift @@ -31,6 +31,7 @@ class CoreBridgeV2: CoreBridge { // 2.1 case setVatomParent = "core.vatom.parent.set" case observeVatomChildren = "core.vatom.children.observe" + case getCurrentUser = "core.user.current.get" } /// Represents the Native Bridge Request contract. @@ -246,6 +247,23 @@ class CoreBridgeV2: CoreBridge { } } + + case .getCurrentUser: + self.getCurrentUser { result in + switch result { + case .success(let payload): + // json dance + guard let payload = try? JSON.init(encodable: ["user": payload]) else { + let error = BridgeError.viewer("Unable to encode data.") + completion(.failure(error)) + return + } + completion(.success(payload)) + + case .failure(let error): + completion(.failure(error)) + } + } case .performAction: // ensure caller supplied params @@ -418,6 +436,27 @@ class CoreBridgeV2: CoreBridge { } + private struct BRCurrentUser: Encodable { + + struct Properties: Encodable { + let firstName: String + let lastName: String + let avatarURI: String + let isGuest: Bool + + enum CodingKeys: String, CodingKey { //swiftlint:disable:this nesting + case firstName = "first_name" + case lastName = "last_name" + case avatarURI = "avatar_uri" + case isGuest = "is_guest" + } + } + + let id: String + let properties: Properties + + } + // MARK: - Message Handling /// Invoked when a face would like to create the web bridge. @@ -555,6 +594,29 @@ class CoreBridgeV2: CoreBridge { } + /// Fetches the bridge-available properties of the current user. + private func getCurrentUser(completion: @escaping (Result) -> Void) { + + BLOCKv.getCurrentUser { result in + + switch result { + case .success(let user): + // build response + let properties = BRCurrentUser.Properties(firstName: user.firstName, + lastName: user.lastName, + avatarURI: user.avatarURL?.absoluteString ?? "", + isGuest: user.guestID.isEmpty ? false : true) + let response = BRCurrentUser(id: user.id, properties: properties) + completion(.success(response)) + + case .failure: + let bridgeError = BridgeError.viewer("Unable to fetch current user.") + completion(.failure(bridgeError)) + } + } + + } + /// Performs the action. /// /// - Parameters: From afcb537cb268e622927009345bb74004b1bb371b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 13 Jun 2019 15:53:43 +0200 Subject: [PATCH 130/165] Remove comment --- BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift index 691dc3ae..30f5a674 100644 --- a/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift +++ b/BlockV/Core/Data Pool/Regions/BLOCKvRegion.swift @@ -10,7 +10,6 @@ // import Foundation -//import DictionaryCoding /// Abstract subclass of `Region`. This intermediate class handles updates from the BLOCKv Web socket. Regions should /// subclass to automatically handle Web socket updates. From 9bea28605180ef2e67bf85e4d9a0f1715c22e66a Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 14 Jun 2019 14:39:53 +0200 Subject: [PATCH 131/165] Fix key look-up --- .../Network/Models/Package/Vatom/VatomModel+Descriptor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift index 47abffe2..db546bad 100644 --- a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift @@ -55,7 +55,7 @@ extension RootProperties: Descriptable { guard let _author = descriptor["author"] as? String, - let _rootType = descriptor["category"] as? String, + let _rootType = descriptor["root_type"] as? String, let _templateID = descriptor["template"] as? String, let _templateVariationID = descriptor["template_variation"] as? String, let _publisherFQDN = descriptor["publisher_fqdn"] as? String, From b5e30a48e96f33ad9f6c583af3617fc4eb2d9333 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 18 Jun 2019 15:48:16 +0200 Subject: [PATCH 132/165] Explicitly set sharedUrlCache capacities --- BlockV/Core/BLOCKv.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index 5830fb15..4ec8154e 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -12,6 +12,7 @@ import Foundation import Alamofire import JWTDecode +import Nuke /* Goal: @@ -99,6 +100,13 @@ public final class BLOCKv { name: Notification.Name.BVInternal.UserAuthorizationRequried, object: nil) + // configure in-memory cache (store processed images ready for display) + ImageCache.shared.costLimit = ImageCache.defaultCostLimit() + + // configure http cache (store unprocessed image data at the http level) + DataLoader.sharedUrlCache.memoryCapacity = 50 // 50 MB + DataLoader.sharedUrlCache.diskCapacity = 180 // 180 MB + // handle session launch if self.isLoggedIn { self.onSessionLaunch() From 86a5f3c6132c778f8b7f31b4be5a4e2536939f38 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 20 Jun 2019 20:19:08 +0200 Subject: [PATCH 133/165] Add 1604 error message --- BlockV/Core/BVError.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BlockV/Core/BVError.swift b/BlockV/Core/BVError.swift index fc615c48..615ce66a 100644 --- a/BlockV/Core/BVError.swift +++ b/BlockV/Core/BVError.swift @@ -61,6 +61,7 @@ public enum BVError: Error { // case malformedRequestBody(Int, String) case invalidDataValidation(Int, String) + case vatomNotOwned(Int, String) // case vatomNotFound(Int, String) // @@ -106,6 +107,8 @@ public enum BVError: Error { case 1004: self = .malformedRequestBody(code, message) // vAtom is unrecognized by the platform. case 1701: self = .vatomNotFound(code, message) + // vAtom not owned by current user + case 1604: self = .vatomNotOwned(code, message) // User token (phone, email, id) is unrecognized by the platfrom. case 2030: self = .unknownUserToken(code, message) // Login phone/email wrong. password @@ -240,6 +243,7 @@ extension BVError.PlatformErrorReason { case let .malformedRequestBody(code, message), let .invalidDataValidation(code, message), let .vatomNotFound(code, message), + let .vatomNotOwned(code, message), let .avatarUploadFailed(code, message), let .unableToRetrieveToken(code, message), let .tokenUnavailable(code, message), From 6d90d05d93c742afc86bbddb9ba306257924e4da Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 27 Jun 2019 07:02:14 +0200 Subject: [PATCH 134/165] Update error bindings --- BlockV/Core/BVError.swift | 216 ++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 104 deletions(-) diff --git a/BlockV/Core/BVError.swift b/BlockV/Core/BVError.swift index 615ce66a..9b92b955 100644 --- a/BlockV/Core/BVError.swift +++ b/BlockV/Core/BVError.swift @@ -23,9 +23,9 @@ import Foundation /// NB: The BLOCKv platform is in the process of unifying error codes. /// BVError is subject to change in future releases. public enum BVError: Error { - + // MARK: Cases - + /// Models a native swift model decoding error. case modelDecoding(reason: String) /// Models a BLOCKv platform error. @@ -39,121 +39,122 @@ public enum BVError: Error { /// Models a custom error. This should be used in very limited circumstances. /// A more defined error is preferred. case custom(reason: String) - + // MARK: Reasons - + public enum SessionErrorReason: Equatable { - case invalidAuthoriationCode + case invalidAuthorizationCode case nonMatchingStates - } - + /// Platform error. Associated values: `code` and `message`. public enum PlatformErrorReason: Equatable { - + + //TODO: Remove. Temporary until all error responses return a code key-value pair. + case unknownWithMissingCode(Int, String) case unknownAppId(Int, String) + case unhandledAction(Int, String) case internalServerIssue(Int, String) - // - case tokenExpired(Int, String) + case unauthorized(Int, String) + case tokenExists(Int, String) + case rateLimited(Int, String) + case invalidAppKey(Int, String) case invalidPayload(Int, String) case tokenUnavailable(Int, String) case invalidDateFormat(Int, String) - // case malformedRequestBody(Int, String) - case invalidDataValidation(Int, String) + case passwordRequired(Int, String) + case invalidToken(Int, String) + case invalidFormData(Int, String) + case usernameNotFound(Int, String) + case unverifiedAccount(Int, String) case vatomNotOwned(Int, String) - // + case maxSharesReached(Int, String) + case redemptionError(Int, String) + case recipientLimit(Int, String) case vatomNotFound(Int, String) - // case unknownUserToken(Int, String) + case insufficientPermission(Int, String) case authenticationFailed(Int, String) - case invalidToken(Int, String) case avatarUploadFailed(Int, String) - case userRefreshTokenInvalid(Int, String) - case authenticationLimit(Int, String) - // - case unknownTokenType(Int, String) case unknownTokenId(Int, String) - case tokenNotFound(Int, String) case cannotDeletePrimaryToken(Int, String) - case unableToRetrieveToken(Int, String) case tokenAlreadyConfirmed(Int, String) case invalidVerificationCode(Int, String) + case unknownTokenType(Int, String) case invalidPhoneNumber(Int, String) case invalidEmailAddress(Int, String) - //TODO: Remove. Temporary until all error responses return a code key-value pair. - case unknownWithMissingCode(Int, String) - case unknown(Int, String) //TODO: Remove. All errors should be mapped. - + + case unknown(Int, String) + /// Init using a BLOCKv platform error code and message. init(code: Int, message: String) { switch code { - case -1: self = .unknownWithMissingCode(code, message) - // App Id is unacceptable. - case 2: self = .unknownAppId(code, message) - // Server encountered an error processing the request. - case 11: self = .internalServerIssue(code, message) - // App Id is unacceptable. - case 17: self = .unknownAppId(code, message) - // Request paylaod is invalid. - case 516: self = .invalidPayload(code, message) - // Request paylaod is invalid. - case 517: self = .invalidPayload(code, message) - // User token (phone, email) is already taken. - case 521: self = .tokenUnavailable(code, message) - // Date format is invalid (e.g. invalid birthday in update user call). - case 527: self = .invalidDateFormat(code, message) - // Invalid request payload on an action. + case -1: self = .unknownWithMissingCode(code, message) + case 2: self = .unknownAppId(code, message) + case 13: self = .unhandledAction(code, message) + case 11: self = .internalServerIssue(code, message) + + case 401: self = .unauthorized(code, message) + case 409: self = .tokenExists(code, message) + case 429: self = .rateLimited(code, message) + + case 513: self = .invalidAppKey(code, message) + case 516: self = .invalidPayload(code, message) + case 517: self = .invalidPayload(code, message) + case 521: self = .tokenUnavailable(code, message) + case 527: self = .invalidDateFormat(code, message) + + case 1001: self = .unauthorized(code, message) case 1004: self = .malformedRequestBody(code, message) - // vAtom is unrecognized by the platform. - case 1701: self = .vatomNotFound(code, message) - // vAtom not owned by current user + case 1006: self = .passwordRequired(code, message) + case 1007: self = .invalidPhoneNumber(code, message) + case 1008: self = .invalidToken(code, message) + case 1010: self = .tokenAlreadyConfirmed(code, message) + case 1012: self = .invalidFormData(code, message) + case 1014: self = .usernameNotFound(code, message) + case 1015: self = .unverifiedAccount(code, message) + case 1604: self = .vatomNotOwned(code, message) - // User token (phone, email, id) is unrecognized by the platfrom. + case 1627: self = .maxSharesReached(code, message) + case 1630: self = .redemptionError(code, message) + case 1632: self = .redemptionError(code, message) + case 1654: self = .recipientLimit(code, message) + + case 1701: self = .vatomNotFound(code, message) + case 1702: self = .unknownUserToken(code, message) + case 1703: self = .unknownUserToken(code, message) + case 1705: self = .unknownUserToken(code, message) + case 1708: self = .insufficientPermission(code, message) + case 2030: self = .unknownUserToken(code, message) - // Login phone/email wrong. password case 2032: self = .authenticationFailed(code, message) - // Uploading the avatar data. failed. case 2037: self = .avatarUploadFailed(code, message) - // Refresh token is not on the whitelist, or the token has expired. - case 2049: self = .userRefreshTokenInvalid(code, message) - // Too many login requests. - case 2051: self = .authenticationLimit(code, message) - //??? - case 2552: self = .unableToRetrieveToken(code, message) - // Token id does not map to a token. case 2553: self = .unknownTokenId(code, message) - // Primary token cannot be deleted. case 2562: self = .cannotDeletePrimaryToken(code, message) - // Attempting to verfiy an already verified token. case 2566: self = .tokenAlreadyConfirmed(code, message) - // Invalid verification code used when attempting to verify an account. case 2567: self = .invalidVerificationCode(code, message) - // Unrecognized token type (only `phone` and `email` are currently accepted). case 2569: self = .unknownTokenType(code, message) - // Invalid email address. case 2571: self = .invalidEmailAddress(code, message) - // Invalid phone number. case 2572: self = .invalidPhoneNumber(code, message) + default: - // useful for debugging - //assertionFailure("Unhandled error: \(code) \(message)") self = .unknown(code, message) } } - + } - + /// public enum WebSocketErrorReason: Equatable { case connectionFailed case connectionDisconnected } - + } extension BVError: Equatable { - + public static func == (lhs: BVError, rhs: BVError) -> Bool { switch (lhs, rhs) { case (let .modelDecoding(lhsReason), let .modelDecoding(rhsReason)): @@ -170,34 +171,34 @@ extension BVError: Equatable { return false } } - + /* # Example Usage - + ## Option 1 - + if case let BVError.platform(reason) = error, case .unknownAppId = reason { - print("App Id Error") + print("App Id Error") } - + ## Option 2 if case let BVError.platform(reason) = error { - if case .unknownAppId(_, _) = reason { - print("App Id Error") - } + if case .unknownAppId(_, _) = reason { + print("App Id Error") } - + } + ## Option 3 - + switch error { case .platform(reason: .unknownAppId(_, _)): - print("App Id Error") + print("App Id Error") default: - return + return } */ - + } extension BVError: LocalizedError { @@ -233,41 +234,48 @@ extension BVError.WebSocketErrorReason { extension BVError.PlatformErrorReason { var localizedDescription: String { switch self { - - //TODO: Is there a better way to do this with pattern matching? + case let .unknownWithMissingCode(_, message): return "Unrecogonized: BLOCKv Platform Error: (Missing Code) - Message: \(message)" case let .unknown(code, message): return "Unrecogonized: BLOCKv Platform Error: (\(code)) - Message: \(message)" - - case let .malformedRequestBody(code, message), - let .invalidDataValidation(code, message), - let .vatomNotFound(code, message), - let .vatomNotOwned(code, message), - let .avatarUploadFailed(code, message), - let .unableToRetrieveToken(code, message), - let .tokenUnavailable(code, message), - let .authenticationLimit(code, message), - let .tokenAlreadyConfirmed(code, message), - let .invalidVerificationCode(code, message), - let .invalidPhoneNumber(code, message), - let .invalidEmailAddress(code, message), - let .invalidPayload(code, message), - let .invalidDateFormat(code, message), - let .userRefreshTokenInvalid(code, message), - let .tokenNotFound(code, message), - let .cannotDeletePrimaryToken(code, message), + + case let .unknownWithMissingCode(code, message), let .unknownAppId(code, message), + let .unhandledAction(code, message), let .internalServerIssue(code, message), - let .tokenExpired(code, message), + let .unauthorized(code, message), + let .tokenExists(code, message), + let .rateLimited(code, message), + let .invalidAppKey(code, message), + let .invalidPayload(code, message), + let .tokenUnavailable(code, message), + let .invalidDateFormat(code, message), + let .malformedRequestBody(code, message), + let .passwordRequired(code, message), + let .invalidToken(code, message), + let .invalidFormData(code, message), + let .usernameNotFound(code, message), + let .unverifiedAccount(code, message), + let .vatomNotOwned(code, message), + let .maxSharesReached(code, message), + let .redemptionError(code, message), + let .recipientLimit(code, message), + let .vatomNotFound(code, message), let .unknownUserToken(code, message), + let .insufficientPermission(code, message), let .authenticationFailed(code, message), - let .invalidToken(code, message), + let .avatarUploadFailed(code, message), + let .unknownTokenId(code, message), + let .cannotDeletePrimaryToken(code, message), + let .tokenAlreadyConfirmed(code, message), + let .invalidVerificationCode(code, message), let .unknownTokenType(code, message), - let .unknownTokenId(code, message): + let .invalidPhoneNumber(code, message), + let .invalidEmailAddress(code, message): return "BLOCKv Platform Error: (\(code)) Message: \(message)" - + } } - + } From 59f3bbe6a8f0df3c64e76162d9e21cdb1855fb17 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Thu, 27 Jun 2019 07:02:27 +0200 Subject: [PATCH 135/165] Fix typo --- BlockV/Core/Helpers/OAuth/AuthorizationServer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift index f02111bb..9de3b014 100644 --- a/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift +++ b/BlockV/Core/Helpers/OAuth/AuthorizationServer.swift @@ -108,7 +108,7 @@ public final class AuthorizationServer { func getToken(completion: @escaping (Result) -> Void) { // sanity checks guard let code = receivedCode else { - let error = BVError.session(reason: .invalidAuthoriationCode) + let error = BVError.session(reason: .invalidAuthorizationCode) completion(.failure(error)) return } From 25b70b4a7caa2636f20341add51410f45884d0ae Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 8 Jul 2019 09:28:34 +0200 Subject: [PATCH 136/165] Pin GenericJSON to 2.0 --- BLOCKv.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BLOCKv.podspec b/BLOCKv.podspec index a0bf6d74..4b6e5947 100644 --- a/BLOCKv.podspec +++ b/BLOCKv.podspec @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.dependency 'JWTDecode', '~> 2.1' # JWT decoding s.dependency 'Signals', '~> 6.0' # Elegant eventing s.dependency 'SwiftLint', '~> 0.26' # Linter - s.dependency 'GenericJSON', '~> 1.2' # JSON + s.dependency 'GenericJSON', '~> 2.0' # JSON s.dependency 'PromiseKit', '~> 6.8' # Promises #s.exclude_files = '**/Info*.plist' end From 2747e9193e2d6e247723154ea9021783d978562b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 8 Jul 2019 09:28:47 +0200 Subject: [PATCH 137/165] Fix migration issues --- .../Network/Models/Package/Vatom/VatomModel+Update.swift | 9 +++------ .../Face Views/Image Policy/ImagePolicyFaceView.swift | 2 +- .../Image Progress/ImageProgressFaceView.swift | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift index be6d475e..eb61be2a 100644 --- a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Update.swift @@ -50,9 +50,9 @@ extension VatomModel { rootProperties["in_contract"]?.boolValue.flatMap { vatom.props.isInContract = $0 } rootProperties["in_contract_with"]?.stringValue.flatMap { vatom.props.inContractWith = $0 } rootProperties["transferred_by"]?.stringValue.flatMap { vatom.props.transferredBy = $0 } - rootProperties["num_direct_clones"]?.floatValue.flatMap { vatom.props.numberDirectClones = Int($0) } + rootProperties["num_direct_clones"]?.doubleValue.flatMap { vatom.props.numberDirectClones = Int($0) } rootProperties["cloned_from"]?.stringValue.flatMap { vatom.props.clonedFrom = $0 } - rootProperties["cloning_score"]?.floatValue.flatMap { vatom.props.cloningScore = Double($0) } + rootProperties["cloning_score"]?.doubleValue.flatMap { vatom.props.cloningScore = Double($0) } rootProperties["acquirable"]?.boolValue.flatMap { vatom.props.isAcquirable = $0 } rootProperties["redeemable"]?.boolValue.flatMap { vatom.props.isRedeemable = $0 } rootProperties["disabled"]?.boolValue.flatMap { vatom.props.isDisabled = $0 } @@ -75,11 +75,8 @@ extension VatomModel { .flatMap { vatom.props.commerce.pricing.validThrough = $0 } rootProperties["commerce"]?["pricing"]?["value"]?["vat_included"]?.boolValue .flatMap { vatom.props.commerce.pricing.isVatIncluded = $0 } - - //FIXME: There is a data type issue here. [18.68768, -33.824017] is converted to - // [18.687679290771484, -33.82401657104492] rootProperties["geo_pos"]?["coordinates"]?.arrayValue.flatMap { - vatom.props.geoPosition.coordinates = $0.compactMap { $0.floatValue }.map { Double($0) } + vatom.props.geoPosition.coordinates = $0.compactMap { $0.doubleValue } } // TODO: Version diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index d35f811a..e4b43af0 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -322,7 +322,7 @@ private extension ImagePolicyFaceView { } // child count - if let countMax = imagePolicyDescriptor["count_max"]?.floatValue { + if let countMax = imagePolicyDescriptor["count_max"]?.doubleValue { let childCountPolicy = ChildCount(resourceName: resourceName, countMax: Int(countMax)) self.policies.append(childCountPolicy) continue diff --git a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift index 580ed94c..15839058 100644 --- a/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift +++ b/BlockV/Face/Face Views/Image Progress/ImageProgressFaceView.swift @@ -86,10 +86,10 @@ class ImageProgressFaceView: FaceView { self.direction ?= faceConfig["direction"]?.stringValue self.showPercentage ?= faceConfig["show_percentage"]?.boolValue - if let paddingEnd = faceConfig["padding_end"]?.floatValue { + if let paddingEnd = faceConfig["padding_end"]?.doubleValue { self.paddingEnd ?= Double(paddingEnd) } - if let paddingStart = faceConfig["padding_start"]?.floatValue { + if let paddingStart = faceConfig["padding_start"]?.doubleValue { self.paddingStart = Double(paddingStart) } } From cc9b3c4ff74db7d18be45902c63cd88be22cc286 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 8 Jul 2019 14:31:38 +0200 Subject: [PATCH 138/165] Add root props lookup for cloning_score and num_direct_clones --- .../Image Policy/ImagePolicyFaceView.swift | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift index e4b43af0..05704fe3 100644 --- a/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift +++ b/BlockV/Face/Face Views/Image Policy/ImagePolicyFaceView.swift @@ -164,15 +164,25 @@ class ImagePolicyFaceView: FaceView { } else if let policy = policy as? Config.FieldLookup { // create key path and split into head and tail - guard let component = KeyPath(policy.field).headAndTail(), - // only private section lookups are allowed - component.head == "private", + guard let component = KeyPath(policy.field).headAndTail() else { continue } + + var vatomValue: JSON? + // check container + if component.head == "private" { // current value on the vatom - let vatomValue = self.vatom.private?[keyPath: component.tail.path] else { - continue + let vatomValue = self.vatom.private?[keyPath: component.tail.path] + } else if component.head == "vAtom::vAtomType" { + //TODO: Create a keypath-to-keypath look up + if component.tail.path == "cloning_score" { + vatomValue = try? JSON(self.vatom.props.cloningScore) + } else if component.tail.path == "num_direct_clones" { + vatomValue = try? JSON(self.vatom.props.numberDirectClones) + } } - - if vatomValue == policy.value { + + guard let value = vatomValue else { continue } + + if value == policy.value { // update image //print(">>:: vAtom Value: \(vatomValue) | Policy Value: \(policy.value)\n") return policy.resourceName From 550774845de88b463d330308517cdf3709902d72 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Sun, 14 Jul 2019 16:30:55 +0400 Subject: [PATCH 139/165] added accounds api call --- .../Models/User/AddressAccountModel.swift | 31 +++++++++++++++++++ .../Network/Stack/API+TypedResponses.swift | 4 +++ .../Core/Requests/BLOCKv+UserRequests.swift | 26 ++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 BlockV/Core/Network/Models/User/AddressAccountModel.swift diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift new file mode 100644 index 00000000..dc0d95b5 --- /dev/null +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -0,0 +1,31 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/// Eth address response model. +public struct AddressAccountModel: Codable, Equatable { + + public let id: String + public let user_id: String + public let address: String + public let type: String + public let created_at: String + + enum CodingKeys: String, CodingKey { + case id = "id" + case user_id = "user_id" + case address = "address" + case type = "type" + case created_at = "created_at" + } + +} diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 9d083e13..bc0bb760 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -121,6 +121,10 @@ extension API { static func getTokens() -> Endpoint> { return Endpoint(path: currentUserPath + "/tokens") } + + static func getAccounts() -> Endpoint> { + return Endpoint(path: currentUserPath + "/accounts") + } /// Builds the endpoint to log out the current user. /// diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index babf9746..bfa8d359 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -286,6 +286,32 @@ extension BLOCKv { } } + + /// Fetches the current user's accounts description from the BLOCKv platform. + /// + /// - completion: The completion handler to call when the request is completed. + /// This handler is executed on the main queue. + public static func getCurrentUserBlockchainAccounts(completion: @escaping (Result) -> Void) { + + let endpoint = API.CurrentUser.getAccounts() + + self.client.request(endpoint) { result in + + switch result { + case .success(let baseModel): + // model is available + DispatchQueue.main.async { + completion(.success(baseModel.payload)) + } + case .failure(let error): + // handle error + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + + } /// Removes the token from the current user's token list on the BLOCKv platform. /// From 7553554bb75fb9850b6fada658d9301027ad94c6 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Sun, 14 Jul 2019 17:01:04 +0400 Subject: [PATCH 140/165] added docs --- .../Models/User/AddressAccountModel.swift | 20 ++++++++++++++++++- .../Network/Stack/API+TypedResponses.swift | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift index dc0d95b5..4ac7741b 100644 --- a/BlockV/Core/Network/Models/User/AddressAccountModel.swift +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -18,7 +18,7 @@ public struct AddressAccountModel: Codable, Equatable { public let user_id: String public let address: String public let type: String - public let created_at: String + public let created_at: Date enum CodingKeys: String, CodingKey { case id = "id" @@ -28,4 +28,22 @@ public struct AddressAccountModel: Codable, Equatable { case created_at = "created_at" } +// public init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// id = try container.decode(String.self, forKey: .id) +// user_id = try container.decode(String.self, forKey: .user_id) +// address = try container.decode(String.self, forKey: .address) +// type = try container.decode(String.self, forKey: .type) +// created_at = try container.decode(Date.self, forKey: .created_at) +// } +// +// public func encode(to encoder: Encoder) throws { +// var container = encoder.container(keyedBy: CodingKeys.self) +// try container.encode(id, forKey: .id) +// try container.encode(user_id, forKey: .user_id) +// try container.encode(address, forKey: .address) +// try container.encode(type, forKey: .type) +// try container.encode(created_at, forKey: .created_at) +// } + } diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index bc0bb760..30cf2abe 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -122,6 +122,9 @@ extension API { return Endpoint(path: currentUserPath + "/tokens") } + /// Builds the endpoint to get the current user's accounts. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. static func getAccounts() -> Endpoint> { return Endpoint(path: currentUserPath + "/accounts") } From c39dd04f9a0a081886cb4b636faf61659bb5491c Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Sun, 14 Jul 2019 17:39:59 +0400 Subject: [PATCH 141/165] Update AddressAccountModel.swift --- .../Models/User/AddressAccountModel.swift | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift index 4ac7741b..8608436c 100644 --- a/BlockV/Core/Network/Models/User/AddressAccountModel.swift +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -12,38 +12,39 @@ import Foundation /// Eth address response model. -public struct AddressAccountModel: Codable, Equatable { +public struct AddressAccountModel: Codable { public let id: String - public let user_id: String + public let userId: String public let address: String public let type: String - public let created_at: Date + public let createdAt: Date enum CodingKeys: String, CodingKey { case id = "id" - case user_id = "user_id" + case userId = "user_id" case address = "address" case type = "type" - case created_at = "created_at" + case createdAt = "created_at" } -// public init(from decoder: Decoder) throws { -// let container = try decoder.container(keyedBy: CodingKeys.self) -// id = try container.decode(String.self, forKey: .id) -// user_id = try container.decode(String.self, forKey: .user_id) -// address = try container.decode(String.self, forKey: .address) -// type = try container.decode(String.self, forKey: .type) -// created_at = try container.decode(Date.self, forKey: .created_at) -// } -// -// public func encode(to encoder: Encoder) throws { -// var container = encoder.container(keyedBy: CodingKeys.self) -// try container.encode(id, forKey: .id) -// try container.encode(user_id, forKey: .user_id) -// try container.encode(address, forKey: .address) -// try container.encode(type, forKey: .type) -// try container.encode(created_at, forKey: .created_at) -// } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + userId = try container.decode(String.self, forKey: .userId) + address = try container.decode(String.self, forKey: .address) + type = try container.decode(String.self, forKey: .type) + createdAt = try container.decode(Date.self, forKey: .createdAt) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(userId, forKey: .userId) + try container.encode(address, forKey: .address) + try container.encode(type, forKey: .type) + try container.encode(createdAt, forKey: .createdAt) + } } + From c78919ae2e39acc0ad59e5d35686835eb8bb54f6 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Mon, 15 Jul 2019 17:06:24 +0400 Subject: [PATCH 142/165] Update AddressAccountModel.swift --- BlockV/Core/Network/Models/User/AddressAccountModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift index 8608436c..3e8d7dbb 100644 --- a/BlockV/Core/Network/Models/User/AddressAccountModel.swift +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -21,10 +21,10 @@ public struct AddressAccountModel: Codable { public let createdAt: Date enum CodingKeys: String, CodingKey { - case id = "id" + case id case userId = "user_id" - case address = "address" - case type = "type" + case address + case type case createdAt = "created_at" } From c06e21dc2f6c0b405301e44e82e6b19755778ac7 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Mon, 15 Jul 2019 18:09:19 +0400 Subject: [PATCH 143/165] update model --- BlockV/Core/Network/Stack/API+TypedResponses.swift | 2 +- BlockV/Core/Requests/BLOCKv+UserRequests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 30cf2abe..5f4d9edf 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -125,7 +125,7 @@ extension API { /// Builds the endpoint to get the current user's accounts. /// /// - Returns: Constructed endpoint generic over response model that may be passed to a request. - static func getAccounts() -> Endpoint> { + static func getAccounts() -> Endpoint> { return Endpoint(path: currentUserPath + "/accounts") } diff --git a/BlockV/Core/Requests/BLOCKv+UserRequests.swift b/BlockV/Core/Requests/BLOCKv+UserRequests.swift index bfa8d359..f58576dc 100644 --- a/BlockV/Core/Requests/BLOCKv+UserRequests.swift +++ b/BlockV/Core/Requests/BLOCKv+UserRequests.swift @@ -291,7 +291,7 @@ extension BLOCKv { /// /// - completion: The completion handler to call when the request is completed. /// This handler is executed on the main queue. - public static func getCurrentUserBlockchainAccounts(completion: @escaping (Result) -> Void) { + public static func getCurrentUserBlockchainAccounts(completion: @escaping (Result<[AddressAccountModel], BVError>) -> Void) { let endpoint = API.CurrentUser.getAccounts() From a3f623d271e6bd2774ff777ab0d2c80886fa25d2 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 15 Jul 2019 20:32:40 +0200 Subject: [PATCH 144/165] adjust access control to open --- BlockV/Face/Face Views/FaceView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BlockV/Face/Face Views/FaceView.swift b/BlockV/Face/Face Views/FaceView.swift index 1f918ca9..c5257e5b 100644 --- a/BlockV/Face/Face Views/FaceView.swift +++ b/BlockV/Face/Face Views/FaceView.swift @@ -137,7 +137,7 @@ open class BoundedView: UIView { /// Setting this value to `true` will trigger a subview layout and ensure that `layoutWithKnowBounds()` is called /// after the layout. - var requiresBoundsBasedSetup = false { + open var requiresBoundsBasedSetup = false { didSet { if requiresBoundsBasedSetup { // trigger a new layout cycle @@ -165,7 +165,7 @@ open class BoundedView: UIView { /// /// This function is usefull for cases where the bounds of the view are important, for example, scaling an image /// to the correct size. - func setupWithBounds() { + open func setupWithBounds() { // subclass should override } From 2c617cd65c85faf85392a954c2a928a4e116af4e Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Fri, 19 Jul 2019 09:34:50 +0200 Subject: [PATCH 145/165] added unit tests forr address accounts model --- .../Models/User/AddressAccountModel.swift | 2 +- .../Miscellaneous/BVError_Tests.swift | 2 +- .../Mocks/MockAddressModel.swift | 20 +++++++ .../Codable/AddressModelCodable_Tests.swift | 35 ++++++++++++ Example/BlockV.xcodeproj/project.pbxproj | 16 ++++-- Example/Podfile | 2 +- Example/Podfile.lock | 56 +++++++++---------- 7 files changed, 98 insertions(+), 35 deletions(-) create mode 100644 Example/BLOCKv Unit Tests/Mocks/MockAddressModel.swift create mode 100644 Example/BLOCKv Unit Tests/Models/Codable/AddressModelCodable_Tests.swift diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift index 3e8d7dbb..8447a3f7 100644 --- a/BlockV/Core/Network/Models/User/AddressAccountModel.swift +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -12,7 +12,7 @@ import Foundation /// Eth address response model. -public struct AddressAccountModel: Codable { +public struct AddressAccountModel: Codable, Equatable { public let id: String public let userId: String diff --git a/Example/BLOCKv Unit Tests/Miscellaneous/BVError_Tests.swift b/Example/BLOCKv Unit Tests/Miscellaneous/BVError_Tests.swift index d01beac7..a2255913 100644 --- a/Example/BLOCKv Unit Tests/Miscellaneous/BVError_Tests.swift +++ b/Example/BLOCKv Unit Tests/Miscellaneous/BVError_Tests.swift @@ -30,7 +30,7 @@ class BVError_Tests: XCTestCase { // Top level - let errorPlatform = BVError.platform(reason: .authenticationLimit(0, "test")) + let errorPlatform = BVError.platform(reason: .authenticationFailed(0, "test")) let errorWebSocket = BVError.webSocket(error: .connectionFailed) let errorModelDecoding = BVError.modelDecoding(reason: "test") diff --git a/Example/BLOCKv Unit Tests/Mocks/MockAddressModel.swift b/Example/BLOCKv Unit Tests/Mocks/MockAddressModel.swift new file mode 100644 index 00000000..fd7c3780 --- /dev/null +++ b/Example/BLOCKv Unit Tests/Mocks/MockAddressModel.swift @@ -0,0 +1,20 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +public struct MockAddressModel { + + public static let ethAddress = """ + {"id": "aaaa","user_id": "bbbb","address": "0x4bbbb","type": "system","created_at": "2018-04-09T12:31:36Z"} + """.data(using: .utf8)! + +} diff --git a/Example/BLOCKv Unit Tests/Models/Codable/AddressModelCodable_Tests.swift b/Example/BLOCKv Unit Tests/Models/Codable/AddressModelCodable_Tests.swift new file mode 100644 index 00000000..784a07c8 --- /dev/null +++ b/Example/BLOCKv Unit Tests/Models/Codable/AddressModelCodable_Tests.swift @@ -0,0 +1,35 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import XCTest +@testable import BLOCKv + +class AddressModelCodable_Tests: XCTestCase { + + // MARK: - Test Methods + + func testActionDecoding() { + + do { + _ = try TestUtility.jsonDecoder.decode(AddressAccountModel.self, from: MockAddressModel.ethAddress) + } catch { + XCTFail("Decoding failed: \(error.localizedDescription)") + } + + } + + func testModelCodable() { + + self.decodeEncodeCompare(type: AddressAccountModel.self, from:MockAddressModel.ethAddress) + + } + +} diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 89b7b239..6b20babe 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ D597FF272051305A009B2910 /* UIAlertController+Etx.swift in Sources */ = {isa = PBXBuildFile; fileRef = D597FF262051305A009B2910 /* UIAlertController+Etx.swift */; }; D59C7DDD206CE1D000FCE1DD /* Actions.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D59C7DDC206CE1D000FCE1DD /* Actions.storyboard */; }; D5E3AC9F2049883900CFFEEA /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E3AC9E2049883800CFFEEA /* LoginViewController.swift */; }; + D9BEC41D22E0C48C0068F280 /* AddressModelCodable_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BEC41922E0BCDB0068F280 /* AddressModelCodable_Tests.swift */; }; + D9BEC41E22E0C4A40068F280 /* MockAddressModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9BEC41B22E0BEBD0068F280 /* MockAddressModel.swift */; }; FE05FFA026A3E5A0C79B6E20 /* Pods_BLOCKv_Unit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7956FE101E9335CDDC8248F3 /* Pods_BLOCKv_Unit_Tests.framework */; }; /* End PBXBuildFile section */ @@ -128,6 +130,8 @@ D597FF262051305A009B2910 /* UIAlertController+Etx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Etx.swift"; sourceTree = ""; }; D59C7DDC206CE1D000FCE1DD /* Actions.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Actions.storyboard; sourceTree = ""; }; D5E3AC9E2049883800CFFEEA /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + D9BEC41922E0BCDB0068F280 /* AddressModelCodable_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressModelCodable_Tests.swift; sourceTree = ""; }; + D9BEC41B22E0BEBD0068F280 /* MockAddressModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAddressModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -283,6 +287,7 @@ ADA07DF121A94025007A6322 /* VatomModelCodable_Tests.swift */, ADA07DF221A94025007A6322 /* ActionModelCodable_Tests.swift */, AD09ECAE222D5667003F46C0 /* ActivityModelCodable_Tests.swift */, + D9BEC41922E0BCDB0068F280 /* AddressModelCodable_Tests.swift */, ); path = Codable; sourceTree = ""; @@ -293,6 +298,7 @@ AD61462C213DC87100204E5B /* MockModelFaces.swift */, D5307588211199C200DE7FD0 /* MockModel.swift */, ADA07DFC21A9464C007A6322 /* MockModel2.swift */, + D9BEC41B22E0BEBD0068F280 /* MockAddressModel.swift */, ); path = Mocks; sourceTree = ""; @@ -592,7 +598,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n"; + shellScript = "#${PODS_ROOT}/SwiftLint/swiftlint\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -636,8 +642,10 @@ AD61462F213DC88B00204E5B /* MockModelFaces.swift in Sources */, ADA07DF621A94061007A6322 /* BVError_Tests.swift in Sources */, ADA07DF821A94068007A6322 /* FaceModelCodable_Tests.swift in Sources */, + D9BEC41D22E0C48C0068F280 /* AddressModelCodable_Tests.swift in Sources */, AD61462E213DC88700204E5B /* FaceSelectionProcedure_Tests.swift in Sources */, D53075A42112E9E000DE7FD0 /* FaceModel_Tests.swift in Sources */, + D9BEC41E22E0C4A40068F280 /* MockAddressModel.swift in Sources */, ADA07DF721A94061007A6322 /* KeyPath_Tests.swift in Sources */, AD0BCC922178828A001836DE /* VatomModelUpdate_Tests.swift in Sources */, ADA07DFA21A94068007A6322 /* ActionModelCodable_Tests.swift in Sources */, @@ -794,7 +802,7 @@ DEVELOPMENT_TEAM = WA2R22UJPX; ENVIRONMENT_MAPPING = dev_env; INFOPLIST_FILE = BlockV/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -D DEBUG"; @@ -818,7 +826,7 @@ DEVELOPMENT_TEAM = WA2R22UJPX; ENVIRONMENT_MAPPING = dev_env; INFOPLIST_FILE = BlockV/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.sdk.sample.${CONFIGURATION}"; @@ -1043,7 +1051,7 @@ DEVELOPMENT_TEAM = WA2R22UJPX; ENVIRONMENT_MAPPING = prod_env; INFOPLIST_FILE = BlockV/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "io.blockv.sdk.sample.${CONFIGURATION}"; diff --git a/Example/Podfile b/Example/Podfile index 911864bb..f53b1f05 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,5 +1,5 @@ use_frameworks! -platform :ios, '10.0' +platform :ios, '11.0' target 'BlockV_Example' do diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 9ade2cd5..6ecf0ce8 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -4,7 +4,7 @@ PODS: - BLOCKv/Face (= 3.1.0) - BLOCKv/Core (3.1.0): - Alamofire (~> 4.7) - - GenericJSON (~> 1.2) + - GenericJSON (~> 2.0) - JWTDecode (~> 2.1) - PromiseKit (~> 6.8) - Signals (~> 6.0) @@ -15,25 +15,25 @@ PODS: - FLAnimatedImage (~> 1.0) - Nuke (~> 7.0) - FLAnimatedImage (1.0.12) - - GenericJSON (1.2.0) - - JWTDecode (2.2.0) - - Nuke (7.5.2) - - NVActivityIndicatorView (4.6.1): - - NVActivityIndicatorView/Presenter (= 4.6.1) - - NVActivityIndicatorView/Presenter (4.6.1) - - PromiseKit (6.8.3): - - PromiseKit/CorePromise (= 6.8.3) - - PromiseKit/Foundation (= 6.8.3) - - PromiseKit/UIKit (= 6.8.3) - - PromiseKit/CorePromise (6.8.3) - - PromiseKit/Foundation (6.8.3): + - GenericJSON (2.0.0) + - JWTDecode (2.3.0) + - Nuke (7.6.3) + - NVActivityIndicatorView (4.7.0): + - NVActivityIndicatorView/Presenter (= 4.7.0) + - NVActivityIndicatorView/Presenter (4.7.0) + - PromiseKit (6.10.0): + - PromiseKit/CorePromise (= 6.10.0) + - PromiseKit/Foundation (= 6.10.0) + - PromiseKit/UIKit (= 6.10.0) + - PromiseKit/CorePromise (6.10.0) + - PromiseKit/Foundation (6.10.0): - PromiseKit/CorePromise - - PromiseKit/UIKit (6.8.3): + - PromiseKit/UIKit (6.10.0): - PromiseKit/CorePromise - - Signals (6.0.1) + - Signals (6.1.0) - Starscream (3.0.6) - - SwiftLint (0.31.0) - - VatomFace3D (2.0.0): + - SwiftLint (0.33.1) + - VatomFace3D (3.0.2): - BLOCKv/Face - FLAnimatedImage - GenericJSON @@ -64,18 +64,18 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Alamofire: ae5c501addb7afdbb13687d7f2f722c78734c2d3 - BLOCKv: 391d6db2099bedc152bdc629eca934d26b0a612d + BLOCKv: 6d0675eb6f8802e3864af8ec77e735358e535f90 FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 - GenericJSON: 05eb212cd12bf0562b816075090bea3eda2ab2ed - JWTDecode: 85a405ab16d5473e99bd89ded1d535090ebd6a0e - Nuke: 0350d346a688426e8f2331253ef28dc2fc4f6178 - NVActivityIndicatorView: 4ca19fccc84595a78957336a086d00a49be6ce61 - PromiseKit: 94c6e781838c5bf4717677d0d882b0e7250c80fc - Signals: 2c92e8639f97fe45678460840fabe2056b3190b4 + GenericJSON: a967fb5c199c9aae24063e47a771e04e305dc836 + JWTDecode: fb77675c8049c1a7e9433c7cf448c3ce33ee4ac7 + Nuke: 44130e95e09463f8773ae4b96b90de1eba6b3350 + NVActivityIndicatorView: b19ddab2576f805cbe0fb2306cba3476e09a1dea + PromiseKit: 1fdaeb6c0a94a5114fcb814ff3d772b86886ad4e + Signals: f8e5c979e93c35273f8dfe55ee7f47213d3e8fe8 Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5 - SwiftLint: 7a0227733d786395817373b2d0ca799fd0093ff3 - VatomFace3D: 78d08c503819d5e2d8e1978d953885d596e379cf + SwiftLint: 9fc1143bbacec37f94994a7d0d2a85b2154b6294 + VatomFace3D: aabeb4a3c9d72835fbfd8a4bcac3917151b30e0c -PODFILE CHECKSUM: 572540b34367caffea356b90ca04601e730b047d +PODFILE CHECKSUM: d2ad0cdcd830de687ee1e7475c49221546a727b8 -COCOAPODS: 1.6.1 +COCOAPODS: 1.7.4 From 8f7f6414b17f8e743380f13cd99032013a37c986 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Fri, 19 Jul 2019 09:35:52 +0200 Subject: [PATCH 146/165] Update project.pbxproj --- Example/BlockV.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 6b20babe..5a045678 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -598,7 +598,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#${PODS_ROOT}/SwiftLint/swiftlint\n"; + shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n"; }; /* End PBXShellScriptBuildPhase section */ From 62cf97ce222ab81358811cd5ca3598c979fb6f32 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 19 Jul 2019 10:03:54 +0200 Subject: [PATCH 147/165] Add sync --- BlockV/Core/Data Pool/Client+PromiseKit.swift | 16 ++++++ .../Data Pool/Regions/InventoryRegion.swift | 55 ++++++++++++++++--- .../Network/Models/Package/HashModel.swift | 36 ++++++++++++ .../Package/Vatom/VatomModel+Descriptor.swift | 2 + .../Models/Package/Vatom/VatomModel.swift | 3 + .../Network/Stack/API+TypedResponses.swift | 15 +++++ BlockV/Core/Network/Stack/API.swift | 18 ++++++ 7 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 BlockV/Core/Network/Models/Package/HashModel.swift diff --git a/BlockV/Core/Data Pool/Client+PromiseKit.swift b/BlockV/Core/Data Pool/Client+PromiseKit.swift index 41c43d4e..f9fbfa04 100644 --- a/BlockV/Core/Data Pool/Client+PromiseKit.swift +++ b/BlockV/Core/Data Pool/Client+PromiseKit.swift @@ -53,5 +53,21 @@ internal extension Client { } } + + func request(_ endpoint: Endpoint) -> Promise { + + return Promise { seal in + // convert result type into promise + self.request(endpoint) { result in + switch result { + case .success(let model): + seal.fulfill(model) + case .failure(let error): + seal.reject(error) + } + } + } + + } } diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 7909f957..9b318d39 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -41,6 +41,12 @@ class InventoryRegion: BLOCKvRegion { } } + + var lastHash: String? { + didSet { + print("lastHash \(String(describing:lastHash))") + } + } /// Current user ID. let currentUserID = DataPool.sessionInfo["userID"] as? String ?? "" @@ -69,13 +75,26 @@ class InventoryRegion: BLOCKvRegion { // pause websocket events self.pauseMessages() - - // fetch all pages recursively - return self.fetchBatched().ensure { - - // resume websocket events - self.resumeMessages() - + + return self.fetchInventoryHash().then { newHash -> Promise<[String]?> in + + // replace current hash + let oldHash = self.lastHash + self.lastHash = newHash + + if oldHash == newHash { + // nothing has changes + self.resumeMessages() + // return nil(no-op) + return Promise.value(nil) + } + + // fetch all pages recursively + return self.fetchBatched().ensure { + // resume websocket events + self.resumeMessages() + } + } } @@ -94,9 +113,13 @@ class InventoryRegion: BLOCKvRegion { guard let oldOwner = payload["old_owner"] as? String else { return } guard let newOwner = payload["new_owner"] as? String else { return } guard let vatomID = payload["id"] as? String else { return } - if msgType != "inventory" { - return + + // nil out the hash if something has changed + if msgType == "inventory" || msgType == "state_update" { + self.lastHash = nil } + + if msgType != "inventory" { return } // check if this is an incoming or outgoing vatom if oldOwner == self.currentUserID && newOwner != self.currentUserID { @@ -253,3 +276,17 @@ extension InventoryRegion { } } + +extension InventoryRegion { + + /// Fetches the remote inventory's hash value. + func fetchInventoryHash() -> Promise { + + let endpoint = API.Vatom.getInventoryHash() + return BLOCKv.client.request(endpoint).map { result -> String in + return result.payload.hash + } + + } + +} diff --git a/BlockV/Core/Network/Models/Package/HashModel.swift b/BlockV/Core/Network/Models/Package/HashModel.swift new file mode 100644 index 00000000..b381dd6f --- /dev/null +++ b/BlockV/Core/Network/Models/Package/HashModel.swift @@ -0,0 +1,36 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +// MARK: - Hash + +public struct InventoryHashModel: Codable { + let hash: String +} + +// MARK: - Sync + +public struct VatomSyncModel: Codable { + let id: String + let sync: UInt +} + +public struct InventorySyncModel: Codable { + + let vatoms: [VatomSyncModel] + let nextToken: String + + enum CodingKeys: String, CodingKey { + case vatoms + case nextToken = "next_token" + } +} diff --git a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift index db546bad..937d1891 100644 --- a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift @@ -25,6 +25,7 @@ extension VatomModel: Descriptable { let _version = descriptor["version"] as? String, let _whenCreated = descriptor["when_created"] as? String, let _whenModified = descriptor["when_modified"] as? String, + let _sync = descriptor["sync"] as? UInt, let _private = descriptor["private"] as? [String: Any], let _rootDescriptor = descriptor["vAtom::vAtomType"] as? [String: Any] @@ -34,6 +35,7 @@ extension VatomModel: Descriptable { self.version = _version self.whenCreated = DateFormatter.blockvDateFormatter.date(from: _whenCreated)! //FIXME: force self.whenModified = DateFormatter.blockvDateFormatter.date(from: _whenModified)! //FIXME: force + self.sync = _sync self.private = try? JSON.init(_private) self.props = try RootProperties(from: _rootDescriptor) diff --git a/BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift index a2026a63..71329bf7 100644 --- a/BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel.swift @@ -23,6 +23,7 @@ public struct VatomModel: Equatable { // variables public var whenModified: Date public var isUnpublished: Bool + public var sync: UInt /// Template properties. /// @@ -58,6 +59,7 @@ public struct VatomModel: Equatable { case actionModels = "actions" case eos = "eos" case eth = "eth" + case sync = "sync" } } @@ -71,6 +73,7 @@ extension VatomModel: Codable { version = try items.decode(String.self, forKey: .version) whenCreated = try items.decode(Date.self, forKey: .whenCreated) whenModified = try items.decode(Date.self, forKey: .whenModified) + sync = try items.decode(UInt.self, forKey: .sync) props = try items.decode(RootProperties.self, forKey: .props) isUnpublished = try items.decodeIfPresent(Bool.self, forKey: .isUnpublished) ?? false diff --git a/BlockV/Core/Network/Stack/API+TypedResponses.swift b/BlockV/Core/Network/Stack/API+TypedResponses.swift index 9d083e13..8055cf5f 100644 --- a/BlockV/Core/Network/Stack/API+TypedResponses.swift +++ b/BlockV/Core/Network/Stack/API+TypedResponses.swift @@ -260,6 +260,21 @@ extension API { /// Namespace for vatom related endpoints with a typed reponse model. enum Vatom { + + /// Builds an endpoint to get the current user's inventory sync hash. + /// + /// - Returns: Constructed endpoint specialized to parse out a `InventoryHashModel`. + static func getInventoryHash() -> Endpoint> { + return API.Generic.getInventoryHash() + } + + /// Builds an endpoint to get the current user's inventory vatoms' sync number. + /// + /// - Returns: Constructed endpoint specialized to parse out a `InventorySyncModel`. + static func getInventoryVatomSyncNumbers(limit: Int = 1000, + token: String) -> Endpoint> { + return API.Generic.getInventoryVatomSyncNumbers(limit: limit, token: token) + } /// Builds an endpoint to get the current user's inventory. /// diff --git a/BlockV/Core/Network/Stack/API.swift b/BlockV/Core/Network/Stack/API.swift index 108bf96e..bf7868c0 100644 --- a/BlockV/Core/Network/Stack/API.swift +++ b/BlockV/Core/Network/Stack/API.swift @@ -56,6 +56,24 @@ extension API { } // MARK: Vatoms + + /// Builds the generic endpoint to get the current user's inventory vatom's sync number. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getInventoryVatomSyncNumbers(limit: Int = 1000, token: String) -> Endpoint { + return Endpoint(method: .get, + path: userVatomPath + "/inventory/index", + parameters: ["limit": limit, "nextToken": token], + encoding: URLEncoding.queryString) + } + + /// Builds the generic endpoint to get the current user's inventory sync hash. + /// + /// - Returns: Constructed endpoint generic over response model that may be passed to a request. + static func getInventoryHash() -> Endpoint { + return Endpoint(method: .get, + path: userVatomPath + "/inventory/hash") + } /// Builds the generic endpoint to get the current user's inventory. /// From 467d08beb0b6bfcd06c1191d8d8fe367f93b9448 Mon Sep 17 00:00:00 2001 From: Malcolmn Date: Fri, 19 Jul 2019 10:55:51 +0200 Subject: [PATCH 148/165] Update AddressAccountModel.swift --- .../Models/User/AddressAccountModel.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/BlockV/Core/Network/Models/User/AddressAccountModel.swift b/BlockV/Core/Network/Models/User/AddressAccountModel.swift index 8447a3f7..e05d260a 100644 --- a/BlockV/Core/Network/Models/User/AddressAccountModel.swift +++ b/BlockV/Core/Network/Models/User/AddressAccountModel.swift @@ -28,23 +28,5 @@ public struct AddressAccountModel: Codable, Equatable { case createdAt = "created_at" } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(String.self, forKey: .id) - userId = try container.decode(String.self, forKey: .userId) - address = try container.decode(String.self, forKey: .address) - type = try container.decode(String.self, forKey: .type) - createdAt = try container.decode(Date.self, forKey: .createdAt) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(userId, forKey: .userId) - try container.encode(address, forKey: .address) - try container.encode(type, forKey: .type) - try container.encode(createdAt, forKey: .createdAt) - } - } From dac850bea61dc49b92ab4ba1608a08f1804ccf40 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 22 Jul 2019 17:43:43 +0200 Subject: [PATCH 149/165] Fix spelling --- BlockV/Core/Data Pool/Regions/Region+Notifications.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlockV/Core/Data Pool/Regions/Region+Notifications.swift b/BlockV/Core/Data Pool/Regions/Region+Notifications.swift index 6046f0a3..870650a8 100644 --- a/BlockV/Core/Data Pool/Regions/Region+Notifications.swift +++ b/BlockV/Core/Data Pool/Regions/Region+Notifications.swift @@ -18,7 +18,7 @@ public enum RegionEvent: String { case updated = "region.updated" /// Triggered when an object is added. - case objectAdded = "region.object.addded" + case objectAdded = "region.object.added" /// Triggered when an object is removed. case objectRemoved = "region.object.removed" From 4483f16e64965d910dce166ff70f01aa7e38d4d6 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 22 Jul 2019 17:43:59 +0200 Subject: [PATCH 150/165] Add WSMapEvent --- .../Core/Web Socket/Models/WSMapEvent.swift | 73 +++++++++++++++++++ BlockV/Core/Web Socket/WebSocketManager.swift | 13 ++++ 2 files changed, 86 insertions(+) create mode 100644 BlockV/Core/Web Socket/Models/WSMapEvent.swift diff --git a/BlockV/Core/Web Socket/Models/WSMapEvent.swift b/BlockV/Core/Web Socket/Models/WSMapEvent.swift new file mode 100644 index 00000000..0560603e --- /dev/null +++ b/BlockV/Core/Web Socket/Models/WSMapEvent.swift @@ -0,0 +1,73 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import MapKit + +/* + { + "msg_type": "map", + "payload": { + "event_id": "map_ed949f70-d520-4c3a-8f78-0ada05678160", + "op": "remove", + "vatom_id": "ed949f70-d520-4c3a-8f78-0ada05678160", + "action_name": "Pickup", + "lat": -33.93383934281483, + "lon": 18.51217876331921 + } +} + */ + +/// Web socket event model - unowned map vatoms. +public struct WSMapEvent: Decodable { + + // MARK: - Properties + + /// Unique identifier of this inventory event. + public let eventId: String + /// Database operation. + public let operation: String + /// Unique identifier of the vAtom which generated this event. + public let vatomId: String + /// Action name. + public let actionName: String + /// Coordinate of event. + public let coordinate: CLLocationCoordinate2D + + enum CodingKeys: String, CodingKey { + case payload + } + + enum PayloadCodingKeys: String, CodingKey { + case eventId = "event_id" + case operation = "op" + case vatomId = "vatom_id" + case actionName = "action_name" + case latitude = "lat" + case longitude = "lon" + } + + public init(from decoder: Decoder) throws { + + let items = try decoder.container(keyedBy: CodingKeys.self) + // de-nest payload to top level + let payloadContainer = try items.nestedContainer(keyedBy: PayloadCodingKeys.self, forKey: .payload) + // de-nest payload to top level + eventId = try payloadContainer.decode(String.self, forKey: .eventId) + operation = try payloadContainer.decode(String.self, forKey: .operation) + vatomId = try payloadContainer.decode(String.self, forKey: .vatomId) + actionName = try payloadContainer.decode(String.self, forKey: .actionName) + let latitude = try payloadContainer.decode(Double.self, forKey: .latitude) + let longitude = try payloadContainer.decode(Double.self, forKey: .longitude) + coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + } + +} diff --git a/BlockV/Core/Web Socket/WebSocketManager.swift b/BlockV/Core/Web Socket/WebSocketManager.swift index c84df228..1a9614a8 100644 --- a/BlockV/Core/Web Socket/WebSocketManager.swift +++ b/BlockV/Core/Web Socket/WebSocketManager.swift @@ -61,6 +61,8 @@ public class WebSocketManager { case stateUpdate = "state_update" /// Activity event case activity = "my_events" + /// Map event (unowned vatom) + case map = "map" } // MARK: - Signals @@ -80,6 +82,9 @@ public class WebSocketManager { /// Fires when the Web socket receives an **activity** update event. public let onActivityUpdate = Signal() + + /// Fires when the Web socket receives a *map* update event for *unowned* vatoms. + public let onMapUpdate = Signal() // - Lifecycle @@ -399,6 +404,14 @@ extension WebSocketManager: WebSocketDelegate { } catch { printBV(error: error.localizedDescription) } + + case .map: + do { + let mapEvent = try blockvJSONDecoder.decode(WSMapEvent.self, from: data) + self.onMapUpdate.fire(mapEvent) + } catch { + printBV(error: error.localizedDescription) + } } default: From 0de16054a441cca8fc4a5a17c26d68737c57eb16 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 22 Jul 2019 17:44:20 +0200 Subject: [PATCH 151/165] Observer map event updates --- .../Core/Data Pool/Regions/GeoPosRegion.swift | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift index bb5116de..0bfb7436 100644 --- a/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift +++ b/BlockV/Core/Data Pool/Regions/GeoPosRegion.swift @@ -175,13 +175,16 @@ class GeoPosRegion: BLOCKvRegion { // - Look at state update // get info - guard let msgType = msg["msg_type"] as? String else { return } - guard let payload = msg["payload"] as? [String: Any] else { return } - guard let newData = payload["new_object"] as? [String: Any] else { return } - guard let vatomID = payload["id"] as? String else { return } + guard + let msgType = msg["msg_type"] as? String, + let payload = msg["payload"] as? [String: Any] else { return } if msgType == "state_update" { + guard + let newData = payload["new_object"] as? [String: Any], + let vatomID = payload["id"] as? String else { return } + // check update is related to drop guard let properties = newData["vAtom::vAtomType"] as? [String: Any], let dropped = properties["dropped"] as? Bool @@ -232,13 +235,13 @@ class GeoPosRegion: BLOCKvRegion { } - } - - // inspect inventory events - guard let oldOwner = payload["old_owner"] as? String else { return } - guard let newOwner = payload["new_owner"] as? String else { return } - - if msgType == "inventory" { + } else if msgType == "inventory" { + + // inspect inventory events + guard + let vatomID = payload["id"] as? String, + let oldOwner = payload["old_owner"] as? String, + let newOwner = payload["new_owner"] as? String else { return } /* Iventory events indicate a vatom has entered or exited the user's inventory. It is unlikely that dropped vatoms @@ -255,6 +258,47 @@ class GeoPosRegion: BLOCKvRegion { self.remove(ids: [vatomID]) } + } else if msgType == "map" { + + guard let operation = payload["op"] as? String, + let vatomID = payload["vatom_id"] as? String, + let actionName = payload["action_name"] as? String, + let lat = payload["lat"] as? Double, + let lon = payload["lon"] as? Double else { + return + } + + // check operation typw + if operation == "add" { + + // create endpoint over void + let endpoint: Endpoint = API.Generic.getVatoms(withIDs: [vatomID]) + BLOCKv.client.request(endpoint).done { data in + + // convert + guard + let object = try? JSONSerialization.jsonObject(with: data), + let json = object as? [String: Any], + let payload = json["payload"] as? [String: Any] else { + throw RegionError.failedParsingResponse + } + + // parse out objects + guard let items = self.parseDataObject(from: payload) else { + throw RegionError.failedParsingObject + } + + // add new objects + self.add(objects: items) + + }.catch { error in + printBV(error: "[GeoPosRegion] Unable to vatom. \(error.localizedDescription)") + } + + } else if operation == "remove" { + self.remove(ids: [vatomID]) + } + } } From 245fe39f766ab8147a89ad96955d7e42a8a07816 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 23 Jul 2019 11:57:52 +0200 Subject: [PATCH 152/165] Fix linter issues --- BlockV/Core/Data Pool/Regions/InventoryRegion.swift | 4 ++-- .../Models/Package/Action/ActionModel+Descriptor.swift | 2 ++ .../Network/Models/Package/Face/FaceModel+Descriptable.swift | 2 ++ .../Network/Models/Package/Vatom/VatomModel+Descriptor.swift | 2 ++ BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift | 4 +++- Example/.swiftlint.yml | 1 + 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift index 9b318d39..2da59b5d 100644 --- a/BlockV/Core/Data Pool/Regions/InventoryRegion.swift +++ b/BlockV/Core/Data Pool/Regions/InventoryRegion.swift @@ -202,10 +202,10 @@ extension InventoryRegion { // tracking flag var shouldRecurse = true - for i in range { + for page in range { // build raw request - let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: i, limit: pageSize) + let endpoint: Endpoint = API.Generic.getInventory(parentID: "*", page: page, limit: pageSize) // exectute request let promise = BLOCKv.client.requestJSON(endpoint).then(on: .global(qos: .userInitiated)) { json -> Promise<[String]?> in diff --git a/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift index 650cead0..7998fabb 100644 --- a/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift +++ b/BlockV/Core/Network/Models/Package/Action/ActionModel+Descriptor.swift @@ -11,6 +11,8 @@ import Foundation +//swiftlint:disable identifier_name + extension ActionModel: Descriptable { init(from descriptor: [String: Any]) throws { diff --git a/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift b/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift index 6666fda3..6fbc970e 100644 --- a/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift +++ b/BlockV/Core/Network/Models/Package/Face/FaceModel+Descriptable.swift @@ -12,6 +12,8 @@ import Foundation import GenericJSON +//swiftlint:disable identifier_name + extension FaceModel: Descriptable { init(from descriptor: [String: Any]) throws { diff --git a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift index 937d1891..d30f9e54 100644 --- a/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift +++ b/BlockV/Core/Network/Models/Package/Vatom/VatomModel+Descriptor.swift @@ -12,6 +12,8 @@ import Foundation import GenericJSON +//swiftlint:disable identifier_name + protocol Descriptable { init(from descriptor: [String: Any]) throws } diff --git a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift index b76ad1b1..d0323464 100644 --- a/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift +++ b/BlockV/Face/Face Views/Web/Face Bridge/CoreBridgeV2.swift @@ -12,10 +12,12 @@ import Foundation import GenericJSON +//FIXME: Remove temporary disable type_body_length + /// Core Bridge (Version 2.0.0) /// /// Bridges into the Core module. -class CoreBridgeV2: CoreBridge { +class CoreBridgeV2: CoreBridge { //swiftlint:disable:this type_body_length // MARK: - Enums diff --git a/Example/.swiftlint.yml b/Example/.swiftlint.yml index 88b7bbdb..afbf02dc 100644 --- a/Example/.swiftlint.yml +++ b/Example/.swiftlint.yml @@ -12,6 +12,7 @@ excluded: # paths to ignore during linting. Takes precedence over `included`. - Pods identifier_name: + allowed_symbols: "_" excluded: # excluded via string array - id nesting: From 997421e290d633303253ea54c2ad7ab0112358d8 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 23 Jul 2019 12:30:33 +0200 Subject: [PATCH 153/165] Add map event unit test --- .../Models/Web Socket/MockWebSocket.swift | 30 +++++++++++++++++++ .../Web Socket/WebSocketEvent_Tests.swift | 27 +++++++++++++++++ Example/BlockV.xcodeproj/project.pbxproj | 16 ++++++++++ 3 files changed, 73 insertions(+) create mode 100644 Example/BLOCKv Unit Tests/Models/Web Socket/MockWebSocket.swift create mode 100644 Example/BLOCKv Unit Tests/Models/Web Socket/WebSocketEvent_Tests.swift diff --git a/Example/BLOCKv Unit Tests/Models/Web Socket/MockWebSocket.swift b/Example/BLOCKv Unit Tests/Models/Web Socket/MockWebSocket.swift new file mode 100644 index 00000000..57f44e11 --- /dev/null +++ b/Example/BLOCKv Unit Tests/Models/Web Socket/MockWebSocket.swift @@ -0,0 +1,30 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +struct MockWebSocket { + + static let mapEvent = """ + { + "msg_type": "map", + "payload": { + "event_id": "map_83ce7912-82ea-45c4-9b91-36daa3e61234", + "op": "add", + "vatom_id": "83ce7912-82ea-1234-9b91-36daa3e61234", + "action_name": "Drop", + "lat": -33.123456789, + "lon": 18.123456789 + } + } + """.data(using: .utf8)! + +} diff --git a/Example/BLOCKv Unit Tests/Models/Web Socket/WebSocketEvent_Tests.swift b/Example/BLOCKv Unit Tests/Models/Web Socket/WebSocketEvent_Tests.swift new file mode 100644 index 00000000..4f069c45 --- /dev/null +++ b/Example/BLOCKv Unit Tests/Models/Web Socket/WebSocketEvent_Tests.swift @@ -0,0 +1,27 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import XCTest +@testable import BLOCKv + +class WebSocketEvent_Tests: XCTestCase { + + func testMapEventDecoding() { + + do { + _ = try TestUtility.jsonDecoder.decode(WSMapEvent.self, from: MockWebSocket.mapEvent) + } catch { + XCTFail("Decoding failed: \(error.localizedDescription)") + } + + } + +} diff --git a/Example/BlockV.xcodeproj/project.pbxproj b/Example/BlockV.xcodeproj/project.pbxproj index 5a045678..989fbb54 100644 --- a/Example/BlockV.xcodeproj/project.pbxproj +++ b/Example/BlockV.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ ADA07DF921A94068007A6322 /* VatomModelCodable_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA07DF121A94025007A6322 /* VatomModelCodable_Tests.swift */; }; ADA07DFA21A94068007A6322 /* ActionModelCodable_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA07DF221A94025007A6322 /* ActionModelCodable_Tests.swift */; }; ADA07DFE21A94737007A6322 /* MockModel2.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA07DFC21A9464C007A6322 /* MockModel2.swift */; }; + ADA36A4322E717140064BB58 /* MockWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA36A4222E717140064BB58 /* MockWebSocket.swift */; }; + ADA36A4622E717B70064BB58 /* WebSocketEvent_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA36A4522E717B70064BB58 /* WebSocketEvent_Tests.swift */; }; CDC268C7232CDB5AF1386116 /* Pods_BlockV_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DE93FD598C0B61200B996A4 /* Pods_BlockV_Example.framework */; settings = {ATTRIBUTES = (Required, ); }; }; D519A84B205E83AB006B0D19 /* VatomDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D519A84A205E83AB006B0D19 /* VatomDetailTableViewController.swift */; }; D530759C21119E2A00DE7FD0 /* MockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5307588211199C200DE7FD0 /* MockModel.swift */; }; @@ -106,6 +108,8 @@ ADA07DF121A94025007A6322 /* VatomModelCodable_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VatomModelCodable_Tests.swift; sourceTree = ""; }; ADA07DF221A94025007A6322 /* ActionModelCodable_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionModelCodable_Tests.swift; sourceTree = ""; }; ADA07DFC21A9464C007A6322 /* MockModel2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModel2.swift; sourceTree = ""; }; + ADA36A4222E717140064BB58 /* MockWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocket.swift; sourceTree = ""; }; + ADA36A4522E717B70064BB58 /* WebSocketEvent_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketEvent_Tests.swift; sourceTree = ""; }; B5A79A9FDD3524D3628C080E /* Pods-BlockV_Example.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlockV_Example.dev.xcconfig"; path = "Pods/Target Support Files/Pods-BlockV_Example/Pods-BlockV_Example.dev.xcconfig"; sourceTree = ""; }; BC27A08D03F2D2401C48F394 /* Pods-BlockV_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlockV_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-BlockV_Example/Pods-BlockV_Example.release.xcconfig"; sourceTree = ""; }; C55D0BD7FD8ECD417C235202 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; @@ -303,6 +307,15 @@ path = Mocks; sourceTree = ""; }; + ADA36A4422E7178F0064BB58 /* Web Socket */ = { + isa = PBXGroup; + children = ( + ADA36A4222E717140064BB58 /* MockWebSocket.swift */, + ADA36A4522E717B70064BB58 /* WebSocketEvent_Tests.swift */, + ); + path = "Web Socket"; + sourceTree = ""; + }; D519A849205E8360006B0D19 /* Inventory */ = { isa = PBXGroup; children = ( @@ -337,6 +350,7 @@ D53075A02111DBD600DE7FD0 /* Models */ = { isa = PBXGroup; children = ( + ADA36A4422E7178F0064BB58 /* Web Socket */, ADA07DEF21A94025007A6322 /* Codable */, D53075A32112E9E000DE7FD0 /* FaceModel_Tests.swift */, AD0BCC912178828A001836DE /* VatomModelUpdate_Tests.swift */, @@ -642,6 +656,7 @@ AD61462F213DC88B00204E5B /* MockModelFaces.swift in Sources */, ADA07DF621A94061007A6322 /* BVError_Tests.swift in Sources */, ADA07DF821A94068007A6322 /* FaceModelCodable_Tests.swift in Sources */, + ADA36A4622E717B70064BB58 /* WebSocketEvent_Tests.swift in Sources */, D9BEC41D22E0C48C0068F280 /* AddressModelCodable_Tests.swift in Sources */, AD61462E213DC88700204E5B /* FaceSelectionProcedure_Tests.swift in Sources */, D53075A42112E9E000DE7FD0 /* FaceModel_Tests.swift in Sources */, @@ -652,6 +667,7 @@ AD09ECAF222D5667003F46C0 /* ActivityModelCodable_Tests.swift in Sources */, D530759C21119E2A00DE7FD0 /* MockModel.swift in Sources */, ADA07DF921A94068007A6322 /* VatomModelCodable_Tests.swift in Sources */, + ADA36A4322E717140064BB58 /* MockWebSocket.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 35aae28788d616a8d0029146f4e8fcb588ce11c9 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 23 Jul 2019 15:23:03 +0200 Subject: [PATCH 154/165] Remove example payload --- BlockV/Core/Web Socket/Models/WSMapEvent.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/BlockV/Core/Web Socket/Models/WSMapEvent.swift b/BlockV/Core/Web Socket/Models/WSMapEvent.swift index 0560603e..a01123ef 100644 --- a/BlockV/Core/Web Socket/Models/WSMapEvent.swift +++ b/BlockV/Core/Web Socket/Models/WSMapEvent.swift @@ -12,20 +12,6 @@ import Foundation import MapKit -/* - { - "msg_type": "map", - "payload": { - "event_id": "map_ed949f70-d520-4c3a-8f78-0ada05678160", - "op": "remove", - "vatom_id": "ed949f70-d520-4c3a-8f78-0ada05678160", - "action_name": "Pickup", - "lat": -33.93383934281483, - "lon": 18.51217876331921 - } -} - */ - /// Web socket event model - unowned map vatoms. public struct WSMapEvent: Decodable { From 7cf2ba6e06fc23498423841412f048214561a662 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 24 Jul 2019 17:15:57 +0200 Subject: [PATCH 155/165] Add resource downloader --- .../Face/Resources/ResourceDownloader.swift | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 BlockV/Face/Resources/ResourceDownloader.swift diff --git a/BlockV/Face/Resources/ResourceDownloader.swift b/BlockV/Face/Resources/ResourceDownloader.swift new file mode 100644 index 00000000..fa69e590 --- /dev/null +++ b/BlockV/Face/Resources/ResourceDownloader.swift @@ -0,0 +1,183 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// +import Foundation +import Nuke + +/// Provides utilities for downloading resources. +/// +/// This resouce downloader should only be used glb files. All image downloading should flow through Nuke. +public class ResourceDownloader { + + private static var _dataCache: DataCache? + + static var dataCache: DataCache? { + if let cache = _dataCache { return cache } + let cache = try? DataCache(name: "io.viewer.resources") + cache?.sizeLimit = 1024 * 1024 * 300 // 300 MB + self._dataCache = cache + return cache + } + + /// Currently running downloads + internal static var currentDownloads: [ResourceDownloader] = [] + + /// Download a resource + public static func download(url: URL) -> ResourceDownloader { + + // Check if currently downloading + if let existing = currentDownloads.first(where: { $0.url == url }) { + return existing + } + + // Do download + let download = ResourceDownloader(url: url) + + // Store it in the list of current downloads + currentDownloads.append(download) + + // Done, return it + return download + + } + + /// Current state + private var isDownloading = true + private var error: Error? + + /// Currently downloading URL + public let url: URL + + /// Downloaded data + public private(set) var data: Data? + + public typealias CallbackComplete = (Result) -> Void + + /// Callbacks that are called on completion. + private var completionCallback: [CallbackComplete] = [] + + /// Constructor + init(url: URL) { + + // store URL + self.url = url + + print("[3DFace] [Resource Downloader] Face requested data for: \(url.absoluteString.prefix(140))") + + // check cache + if let data = ResourceDownloader.dataCache?.cachedData(for: url.cacheHash) { + + print("[3DFace] [Resource Downloader] Found cache data: \(data)") + + // done + self.isDownloading = false + self.error = nil + self.data = data + + self.completionCallback.forEach { $0(.success(data)) } + + // remove callbacks + self.completionCallback.removeAll() + + } else { + + print("[3DFace] [Resource Downloader] Downloading: \(url.absoluteString.prefix(140))") + + // create request, ignore local cache (since we have a manual cache). + let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) + + // start download + URLSession.shared.dataTask(with: request) { [weak self] (data, _, error) in + + guard let self = self else { return } + + // done + self.isDownloading = false + self.error = error + self.data = data + + // check for error + if let error = error { + + // notify failed + self.completionCallback.forEach { $0(.failure(error)) } + + } else if let data = data { + + print("[3DFace] [Resource Downloader] Caching data with key: \(url.cacheHash)") + + // store data (async but returns syncrhonously) + ResourceDownloader.dataCache?.storeData(data, for: url.cacheHash) + + // notify completed + self.completionCallback.forEach { $0(.success(data)) } + + } + + // remove callbacks + self.completionCallback.removeAll() + // remove self from currently running tasks + ResourceDownloader.currentDownloads = ResourceDownloader.currentDownloads.filter { $0 !== self } + + }.resume() + } + + } + + enum ResourceError: Error { + case downloadFailed + } + + /// Add a callback for when the download is complete + public func onComplete(_ callBack: @escaping CallbackComplete) { + + // check if done already + if !isDownloading { + + // check if we have data + if let data = self.data { + callBack(.success(data)) + } else if let err = self.error { + callBack(.failure(err)) + } else { + callBack(.failure(ResourceError.downloadFailed)) + } + + // stop + return + + } + + // not downloading, add to callback list + completionCallback.append(callBack) + + } + +} + +private extension URL { + + var cacheHash: String { + let absoluteTrimmedQuery = self.absoluteStringByTrimmingQuery! + return absoluteTrimmedQuery.md5 + } + +} + +private extension URL { + + var absoluteStringByTrimmingQuery: String? { + if var urlcomponents = URLComponents(url: self, resolvingAgainstBaseURL: false) { + urlcomponents.query = nil + return urlcomponents.string + } + return nil + } +} From a9e6f53ff902a5e4578abf2ccca376a89125e775 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 24 Jul 2019 17:18:49 +0200 Subject: [PATCH 156/165] Add md5 hash extension --- BlockV/Face/Extensions/String+Etx.swift | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 BlockV/Face/Extensions/String+Etx.swift diff --git a/BlockV/Face/Extensions/String+Etx.swift b/BlockV/Face/Extensions/String+Etx.swift new file mode 100644 index 00000000..f2e02219 --- /dev/null +++ b/BlockV/Face/Extensions/String+Etx.swift @@ -0,0 +1,29 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation +import var CommonCrypto.CC_MD5_DIGEST_LENGTH +import func CommonCrypto.CC_MD5 +import typealias CommonCrypto.CC_LONG + +extension String { + + /// Returns md5 hash. + var md5: String { + let data = Data(self.utf8) + let hash = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in + var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash) + return hash + } + return hash.map { String(format: "%02x", $0) }.joined() + } +} From b4d097402bb57de9ef3d3264df6bef6c05a38358 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Wed, 24 Jul 2019 20:30:33 +0200 Subject: [PATCH 157/165] Clean up and docs --- .../Face/Resources/ResourceDownloader.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/BlockV/Face/Resources/ResourceDownloader.swift b/BlockV/Face/Resources/ResourceDownloader.swift index fa69e590..f3794cee 100644 --- a/BlockV/Face/Resources/ResourceDownloader.swift +++ b/BlockV/Face/Resources/ResourceDownloader.swift @@ -18,32 +18,32 @@ public class ResourceDownloader { private static var _dataCache: DataCache? - static var dataCache: DataCache? { + private static var dataCache: DataCache? { if let cache = _dataCache { return cache } - let cache = try? DataCache(name: "io.viewer.resources") + let cache = try? DataCache(name: "io.viewer.resource_cache") cache?.sizeLimit = 1024 * 1024 * 300 // 300 MB self._dataCache = cache return cache } - /// Currently running downloads + /// Currently running downloads. internal static var currentDownloads: [ResourceDownloader] = [] - /// Download a resource + /// Downloads the resource specified by the URL. public static func download(url: URL) -> ResourceDownloader { - // Check if currently downloading + // check if currently downloading if let existing = currentDownloads.first(where: { $0.url == url }) { return existing } - // Do download + // download let download = ResourceDownloader(url: url) - // Store it in the list of current downloads + // store it in the list of current downloads currentDownloads.append(download) - // Done, return it + // done return download } @@ -135,7 +135,7 @@ public class ResourceDownloader { case downloadFailed } - /// Add a callback for when the download is complete + /// Add a callback for when the download is complete. public func onComplete(_ callBack: @escaping CallbackComplete) { // check if done already @@ -164,6 +164,7 @@ public class ResourceDownloader { private extension URL { + /// Creates an MD5 hash value using the URL (after the queury components have been removed). var cacheHash: String { let absoluteTrimmedQuery = self.absoluteStringByTrimmingQuery! return absoluteTrimmedQuery.md5 @@ -173,6 +174,7 @@ private extension URL { private extension URL { + /// Returns the string representing the URL without query components. var absoluteStringByTrimmingQuery: String? { if var urlcomponents = URLComponents(url: self, resolvingAgainstBaseURL: false) { urlcomponents.query = nil From ac8518da166a4d4ee24403f05df0c9af82163d4f Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 26 Jul 2019 21:49:06 +0200 Subject: [PATCH 158/165] Add data downloader --- BlockV/Face/Resources/DataDownloader.swift | 230 +++++++++++++++++++++ BlockV/Face/Resources/DataPipeline.swift | 49 +++++ 2 files changed, 279 insertions(+) create mode 100644 BlockV/Face/Resources/DataDownloader.swift create mode 100644 BlockV/Face/Resources/DataPipeline.swift diff --git a/BlockV/Face/Resources/DataDownloader.swift b/BlockV/Face/Resources/DataDownloader.swift new file mode 100644 index 00000000..9c33be14 --- /dev/null +++ b/BlockV/Face/Resources/DataDownloader.swift @@ -0,0 +1,230 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +public protocol Cancellable: class { + func cancel() +} + +public protocol DataDownloading { + /// - parameter didReceiveData: Can be called multiple times if streaming + /// is supported. + /// - parameter completion: Must be called once after all (or none in case + /// of an error) `didReceiveData` closures have been called. + func downloadData(url: URL, + destination: @escaping DataDownloader.Destination, + progress: @escaping (NSNumber) -> Void, + completion: @escaping (Result) -> Void) -> Cancellable +} + +extension URLSessionTask: Cancellable {} + +public class DataDownloader: DataDownloading { + + // MARK: - Session + + public let session: URLSession + private let _impl: _DataDownloader + + /// A closure executed once a download request has successfully completed in order to determine where to move the + /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL + /// and the + public typealias Destination = (_ temporaryURL: URL) -> URL //, options: Options) + + /// Create a download file destination closure which uses the default file manager to move the temporary file to a + /// file URL in the recommended face directory `face_data/resources/`. Placing downloads in this file gives all + /// faces the opportunity to share the on disk cache. + /// + /// - Returns: The `Destination` closure. + public static let recommenedDestination: Destination = { (url: URL) in + + let hash = url.path.md5 + + let directory = FileManager.SearchPathDirectory.cachesDirectory + let domain = FileManager.SearchPathDomainMask.userDomainMask + + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + let destinationURL = directoryURLs.first! + .appendingPathComponent("face_data") + .appendingPathComponent("resources") + .appendingPathComponent(hash) + .appendingPathComponent(url.lastTwoPathComponents) + + return destinationURL + } + + /// Returns a default configuration which has a `nil` set as a `urlCache`. + public static var defaultConfiguration: URLSessionConfiguration { + let conf = URLSessionConfiguration.default + conf.urlCache = nil // cache is on disk + return conf + } + + /// Validates `HTTP` responses by checking that the status code is 2xx. If + /// it's not returns `DataLoader.Error.statusCodeUnacceptable`. + public static func validate(response: URLResponse) -> Swift.Error? { + guard let response = response as? HTTPURLResponse else { return nil } + return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode) + } + + /// Initializes `DataDownloader` with the given configuration. + init(configuration: URLSessionConfiguration = DataDownloader.defaultConfiguration, + validate: @escaping (URLResponse) -> Swift.Error? = DataDownloader.validate ) { + _impl = _DataDownloader() + self.session = URLSession(configuration: configuration, delegate: _impl, delegateQueue: _impl.queue) //FIXME: Nuke uses a separate queue + self._impl.session = self.session + self._impl.validate = validate + } + + public func downloadData(url: URL, destination: @escaping DataDownloader.Destination, progress: @escaping (NSNumber) -> Void, completion: @escaping (Result) -> Void) -> Cancellable { + return _impl.downloadData(url: url, destination: destination, progress: progress, completion: completion) + } + + /// Errors produced by `DataLoader`. + public enum Error: Swift.Error, CustomDebugStringConvertible { + /// Validation failed. + case statusCodeUnacceptable(Int) + + public var debugDescription: String { + switch self { + case let .statusCodeUnacceptable(code): return "Response status code was unacceptable: " + code.description + } + } + } + +} + +// MARK: - Implementation + +/// DataDownloader implementation. +private final class _DataDownloader: NSObject, URLSessionDelegate, URLSessionDownloadDelegate { + + weak var session: URLSession! // this is safe + var validate: (URLResponse) -> Swift.Error? = DataDownloader.validate + let queue = OperationQueue() + + private var handlers = [URLSessionTask: _Handler]() + + override init() { + self.queue.maxConcurrentOperationCount = 1 + } + + // MARK: - Methods + + public func downloadData(url: URL, destination: @escaping DataDownloader.Destination, progress: @escaping (NSNumber) -> Void, completion: @escaping (Result) -> Void) -> Cancellable { + + let downloadTask = session.downloadTask(with: url) + let handler = _Handler(url: url, destination: destination, progress: progress, completion: completion) + queue.addOperation { + self.handlers[downloadTask] = handler + } + downloadTask.resume() + return downloadTask + + } + + // MARK: URLSession Delegate + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let handler = handlers[task] else { return } + handlers[task] = nil + guard let error = error else { return } + handler.completion(.failure(error)) + } + + // MARK: - URLSessionDownload Delegate + + func urlSession(_ session: URLSession, + downloadTask: URLSessionDownloadTask, + didWriteData bytesWritten: Int64, + totalBytesWritten: Int64, + totalBytesExpectedToWrite: Int64) { + + guard let handler = handlers[downloadTask] else { return } + + // compute progress + let calculatedProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) + DispatchQueue.main.async { + NSNumber(value: calculatedProgress) + + //TODO: Implement + + } + + } + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + + guard let handler = handlers[downloadTask], let response = downloadTask.response else { return } + + if let error = validate(response) { + handler.completion(.failure(error)) + return + } + + do { + + let destinationURL = handler.destinationURL + + if FileManager.default.fileExists(atPath: destinationURL.path) { + // in this case it should not have been re-downloaded, but return anyway + handler.completion(.success(destinationURL)) + } else { + // create directory + try FileManager.default.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) + // move from temp to final url + try FileManager.default.moveItem(at: location, to: destinationURL) + handler.completion(.success(destinationURL)) + } + + } catch { + handler.completion(.failure(error)) + } + + } + + // MARK: - Helper + + private final class _Handler { + + let destinationURL: URL + let completion: (Result) -> Void + + init(url: URL, + destination: @escaping DataDownloader.Destination = DataDownloader.recommenedDestination, + progress: @escaping (NSNumber) -> Void, + completion: @escaping (Result) -> Void) { + + self.destinationURL = destination(url) + self.completion = completion + } + } +} + +fileprivate extension URL { + + /// The last two components of the path, or an empty string if there are less than two compoenents. + /// + /// If the URL has less than two path components + var lastTwoPathComponents: String { + + if self.pathComponents.count > 2 { + let last = self.lastPathComponent + let secondLast = self.deletingLastPathComponent().lastPathComponent + return secondLast + "/" + last + } else { + return "" + } + + } + +} diff --git a/BlockV/Face/Resources/DataPipeline.swift b/BlockV/Face/Resources/DataPipeline.swift new file mode 100644 index 00000000..975c2a97 --- /dev/null +++ b/BlockV/Face/Resources/DataPipeline.swift @@ -0,0 +1,49 @@ +// +// BlockV AG. Copyright (c) 2018, all rights reserved. +// +// Licensed under the BlockV SDK License (the "License"); you may not use this file or +// the BlockV SDK except in compliance with the License accompanying it. Unless +// required by applicable law or agreed to in writing, the BlockV SDK distributed under +// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +// ANY KIND, either express or implied. See the License for the specific language +// governing permissions and limitations under the License. +// + +import Foundation + +/* + TODO: + - Rate limiting +*/ +public class DataPipeline { + + public static let shared = DataPipeline() + + /// Data loader used by the pipeline. + public var dataDownloader: DataDownloader + + init(dataDownloader: DataDownloader = DataDownloader()) { + self.dataDownloader = dataDownloader + } + + public func downloadData(url: URL, + destination: @escaping DataDownloader.Destination = DataDownloader.recommenedDestination, + progress: @escaping (NSNumber) -> Void, + completion: @escaping (Result) -> Void) -> Cancellable? { + + let finalURL = destination(url) + if self.checkDiskCache(for: finalURL) { + progress(1) + completion(.success(finalURL)) + return nil + } else { + return self.dataDownloader.downloadData(url: url, destination: destination, progress: progress, completion: completion) + } + + } + + private func checkDiskCache(for url: URL) -> Bool { + return FileManager.default.fileExists(atPath: url.path) + } + +} From 837e5ce5bf352a14f442e0313a11e668a1e8b004 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 26 Jul 2019 21:49:23 +0200 Subject: [PATCH 159/165] Remove file --- .../Face/Resources/ResourceDownloader.swift | 185 ------------------ 1 file changed, 185 deletions(-) delete mode 100644 BlockV/Face/Resources/ResourceDownloader.swift diff --git a/BlockV/Face/Resources/ResourceDownloader.swift b/BlockV/Face/Resources/ResourceDownloader.swift deleted file mode 100644 index f3794cee..00000000 --- a/BlockV/Face/Resources/ResourceDownloader.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// BlockV AG. Copyright (c) 2018, all rights reserved. -// -// Licensed under the BlockV SDK License (the "License"); you may not use this file or -// the BlockV SDK except in compliance with the License accompanying it. Unless -// required by applicable law or agreed to in writing, the BlockV SDK distributed under -// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -// ANY KIND, either express or implied. See the License for the specific language -// governing permissions and limitations under the License. -// -import Foundation -import Nuke - -/// Provides utilities for downloading resources. -/// -/// This resouce downloader should only be used glb files. All image downloading should flow through Nuke. -public class ResourceDownloader { - - private static var _dataCache: DataCache? - - private static var dataCache: DataCache? { - if let cache = _dataCache { return cache } - let cache = try? DataCache(name: "io.viewer.resource_cache") - cache?.sizeLimit = 1024 * 1024 * 300 // 300 MB - self._dataCache = cache - return cache - } - - /// Currently running downloads. - internal static var currentDownloads: [ResourceDownloader] = [] - - /// Downloads the resource specified by the URL. - public static func download(url: URL) -> ResourceDownloader { - - // check if currently downloading - if let existing = currentDownloads.first(where: { $0.url == url }) { - return existing - } - - // download - let download = ResourceDownloader(url: url) - - // store it in the list of current downloads - currentDownloads.append(download) - - // done - return download - - } - - /// Current state - private var isDownloading = true - private var error: Error? - - /// Currently downloading URL - public let url: URL - - /// Downloaded data - public private(set) var data: Data? - - public typealias CallbackComplete = (Result) -> Void - - /// Callbacks that are called on completion. - private var completionCallback: [CallbackComplete] = [] - - /// Constructor - init(url: URL) { - - // store URL - self.url = url - - print("[3DFace] [Resource Downloader] Face requested data for: \(url.absoluteString.prefix(140))") - - // check cache - if let data = ResourceDownloader.dataCache?.cachedData(for: url.cacheHash) { - - print("[3DFace] [Resource Downloader] Found cache data: \(data)") - - // done - self.isDownloading = false - self.error = nil - self.data = data - - self.completionCallback.forEach { $0(.success(data)) } - - // remove callbacks - self.completionCallback.removeAll() - - } else { - - print("[3DFace] [Resource Downloader] Downloading: \(url.absoluteString.prefix(140))") - - // create request, ignore local cache (since we have a manual cache). - let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) - - // start download - URLSession.shared.dataTask(with: request) { [weak self] (data, _, error) in - - guard let self = self else { return } - - // done - self.isDownloading = false - self.error = error - self.data = data - - // check for error - if let error = error { - - // notify failed - self.completionCallback.forEach { $0(.failure(error)) } - - } else if let data = data { - - print("[3DFace] [Resource Downloader] Caching data with key: \(url.cacheHash)") - - // store data (async but returns syncrhonously) - ResourceDownloader.dataCache?.storeData(data, for: url.cacheHash) - - // notify completed - self.completionCallback.forEach { $0(.success(data)) } - - } - - // remove callbacks - self.completionCallback.removeAll() - // remove self from currently running tasks - ResourceDownloader.currentDownloads = ResourceDownloader.currentDownloads.filter { $0 !== self } - - }.resume() - } - - } - - enum ResourceError: Error { - case downloadFailed - } - - /// Add a callback for when the download is complete. - public func onComplete(_ callBack: @escaping CallbackComplete) { - - // check if done already - if !isDownloading { - - // check if we have data - if let data = self.data { - callBack(.success(data)) - } else if let err = self.error { - callBack(.failure(err)) - } else { - callBack(.failure(ResourceError.downloadFailed)) - } - - // stop - return - - } - - // not downloading, add to callback list - completionCallback.append(callBack) - - } - -} - -private extension URL { - - /// Creates an MD5 hash value using the URL (after the queury components have been removed). - var cacheHash: String { - let absoluteTrimmedQuery = self.absoluteStringByTrimmingQuery! - return absoluteTrimmedQuery.md5 - } - -} - -private extension URL { - - /// Returns the string representing the URL without query components. - var absoluteStringByTrimmingQuery: String? { - if var urlcomponents = URLComponents(url: self, resolvingAgainstBaseURL: false) { - urlcomponents.query = nil - return urlcomponents.string - } - return nil - } -} From a53efe55cd27f5854b75372f59d29e9f808aec0b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Fri, 26 Jul 2019 23:05:52 +0200 Subject: [PATCH 160/165] Fix docs --- BlockV/Face/Resources/DataDownloader.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/BlockV/Face/Resources/DataDownloader.swift b/BlockV/Face/Resources/DataDownloader.swift index 9c33be14..b6265ec1 100644 --- a/BlockV/Face/Resources/DataDownloader.swift +++ b/BlockV/Face/Resources/DataDownloader.swift @@ -16,10 +16,14 @@ public protocol Cancellable: class { } public protocol DataDownloading { - /// - parameter didReceiveData: Can be called multiple times if streaming - /// is supported. - /// - parameter completion: Must be called once after all (or none in case - /// of an error) `didReceiveData` closures have been called. + + /// - Parameters: + /// - url: Request URL. + /// - destination: Destination directory. + /// - progress: Progress value. + /// - completion: Must be called once after all (or none in case + /// of an error) `didFinishDownloadingTo` has been called. + /// - Returns: Cancellable item. func downloadData(url: URL, destination: @escaping DataDownloader.Destination, progress: @escaping (NSNumber) -> Void, From e81a0e7535c5c731e7eff53dab04862ac76447f9 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 29 Jul 2019 22:06:12 +0200 Subject: [PATCH 161/165] Extensions to compute allocated size of directory --- BlockV/Face/Extensions/FileManager+Etx.swift | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 BlockV/Face/Extensions/FileManager+Etx.swift diff --git a/BlockV/Face/Extensions/FileManager+Etx.swift b/BlockV/Face/Extensions/FileManager+Etx.swift new file mode 100644 index 00000000..58e7076c --- /dev/null +++ b/BlockV/Face/Extensions/FileManager+Etx.swift @@ -0,0 +1,79 @@ + +// +// Copyright (c) 2016, 2018 Nikolai Ruhe. All rights reserved. +// +import Foundation + +public extension FileManager { + + /// Calculate the allocated size of a directory and all its contents on the volume. + /// + /// As there's no simple way to get this information from the file system the method + /// has to crawl the entire hierarchy, accumulating the overall sum on the way. + /// The resulting value is roughly equivalent with the amount of bytes + /// that would become available on the volume if the directory would be deleted. + /// + /// - note: There are a couple of oddities that are not taken into account (like symbolic links, meta data of + /// directories, hard links, ...). + public func allocatedSizeOfDirectory(at directoryURL: URL) throws -> UInt64 { + + // The error handler simply stores the error and stops traversal + var enumeratorError: Error? = nil + func errorHandler(_: URL, error: Error) -> Bool { + enumeratorError = error + return false + } + + // We have to enumerate all directory contents, including subdirectories. + let enumerator = self.enumerator(at: directoryURL, + includingPropertiesForKeys: Array(allocatedSizeResourceKeys), + options: [], + errorHandler: errorHandler)! + + // We'll sum up content size here: + var accumulatedSize: UInt64 = 0 + + // Perform the traversal. + for item in enumerator { + + // Bail out on errors from the errorHandler. + if enumeratorError != nil { break } + + // Add up individual file sizes. + let contentItemURL = item as! URL + accumulatedSize += try contentItemURL.regularFileAllocatedSize() + } + + // Rethrow errors from errorHandler. + if let error = enumeratorError { throw error } + + return accumulatedSize + } +} + + +fileprivate let allocatedSizeResourceKeys: Set = [ + .isRegularFileKey, + .fileAllocatedSizeKey, + .totalFileAllocatedSizeKey, +] + + +fileprivate extension URL { + + func regularFileAllocatedSize() throws -> UInt64 { + let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys) + + // We only look at regular files. + guard resourceValues.isRegularFile ?? false else { + return 0 + } + + // To get the file's size we first try the most comprehensive value in terms of what + // the file may use on disk. This includes metadata, compression (on file system + // level) and block size. + // In case totalFileAllocatedSize is unavailable we use the fallback value (excluding + // meta data and compression) This value should always be available. + return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0) + } +} From 3e66e9512bb6ca4e9ed356875fe977a690c4673b Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 29 Jul 2019 22:06:34 +0200 Subject: [PATCH 162/165] Extract useful properties --- BlockV/Core/Data Pool/Regions/Region.swift | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/BlockV/Core/Data Pool/Regions/Region.swift b/BlockV/Core/Data Pool/Regions/Region.swift index 7d619b62..d5bbf17c 100644 --- a/BlockV/Core/Data Pool/Regions/Region.swift +++ b/BlockV/Core/Data Pool/Regions/Region.swift @@ -433,6 +433,17 @@ public class Region { return mapped } + + /// Directory containing all region caches. + static var recommendedCacheDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("regions") + + /// Cache filename. + lazy var cacheFilename = self.stateKey.replacingOccurrences(of: ":", with: "_") + + /// File where cache will be stored. + lazy var cacheFile = Region.recommendedCacheDirectory + .appendingPathComponent(cacheFilename) + .appendingPathExtension("json") /// Load objects from local storage. func loadFromCache() -> Promise { @@ -443,14 +454,9 @@ public class Region { // get filename let startTime = Date.timeIntervalSinceReferenceDate - let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") - - // get temporary file location - let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) - .appendingPathExtension("json") // read data - guard let data = try? Data(contentsOf: file) else { + guard let data = try? Data(contentsOf: self.cacheFile) else { printBV(error: ("[DataPool > Region] Unable to read cached data")) resolver.fulfill_() return @@ -527,20 +533,13 @@ public class Region { return } - // get filename - let filename = self.stateKey.replacingOccurrences(of: ":", with: "_") - - // get temporary file location - let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename) - .appendingPathExtension("json") - // make sure folder exists - try? FileManager.default.createDirectory(at: file.deletingLastPathComponent(), + try? FileManager.default.createDirectory(at: self.cacheFile.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) // write file do { - try data.write(to: file) + try data.write(to: self.cacheFile) } catch let err { printBV(error: ("[DataPool > Region] Unable to save data to disk: " + err.localizedDescription)) return From 7f1c5805c9fd204829da30fb0fef32a80e9c64db Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 29 Jul 2019 22:06:56 +0200 Subject: [PATCH 163/165] Add recommended cache directory property --- BlockV/Face/Resources/DataDownloader.swift | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/BlockV/Face/Resources/DataDownloader.swift b/BlockV/Face/Resources/DataDownloader.swift index b6265ec1..a8e108fd 100644 --- a/BlockV/Face/Resources/DataDownloader.swift +++ b/BlockV/Face/Resources/DataDownloader.swift @@ -39,6 +39,19 @@ public class DataDownloader: DataDownloading { public let session: URLSession private let _impl: _DataDownloader + public static var recommendedCacheDirectory: URL { + + let directory = FileManager.SearchPathDirectory.cachesDirectory + let domain = FileManager.SearchPathDomainMask.userDomainMask + + let directoryURLs = FileManager.default.urls(for: directory, in: domain) + + let destinationURL = directoryURLs.first! + .appendingPathComponent("face_data") + .appendingPathComponent("resources") + return destinationURL + } + /// A closure executed once a download request has successfully completed in order to determine where to move the /// temporary file written to during the download process. The closure takes two arguments: the temporary file URL /// and the @@ -53,17 +66,10 @@ public class DataDownloader: DataDownloading { let hash = url.path.md5 - let directory = FileManager.SearchPathDirectory.cachesDirectory - let domain = FileManager.SearchPathDomainMask.userDomainMask - - let directoryURLs = FileManager.default.urls(for: directory, in: domain) - let destinationURL = directoryURLs.first! - .appendingPathComponent("face_data") - .appendingPathComponent("resources") + return recommendedCacheDirectory .appendingPathComponent(hash) .appendingPathComponent(url.lastTwoPathComponents) - return destinationURL } /// Returns a default configuration which has a `nil` set as a `urlCache`. From bcbd29ae0e115fcd0b661bd54b792c8f7fc60fa9 Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Mon, 29 Jul 2019 22:07:09 +0200 Subject: [PATCH 164/165] Add debug section --- BlockV/Core/BLOCKv.swift | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/BlockV/Core/BLOCKv.swift b/BlockV/Core/BLOCKv.swift index 4ec8154e..e2073313 100644 --- a/BlockV/Core/BLOCKv.swift +++ b/BlockV/Core/BLOCKv.swift @@ -104,8 +104,8 @@ public final class BLOCKv { ImageCache.shared.costLimit = ImageCache.defaultCostLimit() // configure http cache (store unprocessed image data at the http level) - DataLoader.sharedUrlCache.memoryCapacity = 50 // 50 MB - DataLoader.sharedUrlCache.diskCapacity = 180 // 180 MB + DataLoader.sharedUrlCache.memoryCapacity = 80 * 1024 * 1024 // 80 MB + DataLoader.sharedUrlCache.diskCapacity = 180 // 180 MB // handle session launch if self.isLoggedIn { @@ -350,3 +350,29 @@ func printBV(info string: String) { func printBV(error string: String) { print("\nBV SDK >>> Error: \(string)") } + +extension BLOCKv { + + public enum Debug { + + //// Returns the cache size of the face data resource disk caches. + public static var faceDataResourceCacheSize: UInt64? { + return try? FileManager.default.allocatedSizeOfDirectory(at: DataDownloader.recommendedCacheDirectory) + } + + /// Returns the cache size of all data pool region disk caches. + public static var regionCacheSize: UInt64? { + return try? FileManager.default.allocatedSizeOfDirectory(at: Region.recommendedCacheDirectory) + } + + /// Clears all disk caches. + public static func clearCache() { + ImageCache.shared.removeAll() + DataLoader.sharedUrlCache.removeAllCachedResponses() + try? FileManager.default.removeItem(at: DataDownloader.recommendedCacheDirectory) + try? FileManager.default.removeItem(at: Region.recommendedCacheDirectory) + } + + } + +} From 7099556dffcbe7d982ce5567e2718113ad436c3a Mon Sep 17 00:00:00 2001 From: Cameron McOnie Date: Tue, 30 Jul 2019 09:21:03 +0200 Subject: [PATCH 165/165] Fix docs --- BlockV/Face/Extensions/FileManager+Etx.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/BlockV/Face/Extensions/FileManager+Etx.swift b/BlockV/Face/Extensions/FileManager+Etx.swift index 58e7076c..af4ad170 100644 --- a/BlockV/Face/Extensions/FileManager+Etx.swift +++ b/BlockV/Face/Extensions/FileManager+Etx.swift @@ -17,34 +17,34 @@ public extension FileManager { /// directories, hard links, ...). public func allocatedSizeOfDirectory(at directoryURL: URL) throws -> UInt64 { - // The error handler simply stores the error and stops traversal + // error handler simply stores the error and stops traversal var enumeratorError: Error? = nil func errorHandler(_: URL, error: Error) -> Bool { enumeratorError = error return false } - // We have to enumerate all directory contents, including subdirectories. + // enumerate all directory contents, including subdirectories let enumerator = self.enumerator(at: directoryURL, includingPropertiesForKeys: Array(allocatedSizeResourceKeys), options: [], errorHandler: errorHandler)! - // We'll sum up content size here: + // sum up content size here: var accumulatedSize: UInt64 = 0 - // Perform the traversal. + // perform the traversal for item in enumerator { - // Bail out on errors from the errorHandler. + // bail out on errors from the errorHandler if enumeratorError != nil { break } - // Add up individual file sizes. + // add up individual file sizes let contentItemURL = item as! URL accumulatedSize += try contentItemURL.regularFileAllocatedSize() } - // Rethrow errors from errorHandler. + // rethrow errors from errorHandler if let error = enumeratorError { throw error } return accumulatedSize @@ -64,7 +64,7 @@ fileprivate extension URL { func regularFileAllocatedSize() throws -> UInt64 { let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys) - // We only look at regular files. + // only look at regular files guard resourceValues.isRegularFile ?? false else { return 0 }