Button -> Delegate -> ActionHandler.

Components will go through delegate. Delegate will choose how to handle it.
This commit is contained in:
Scott Pfeil 2022-08-08 17:45:27 -04:00
parent bf14282eb5
commit 4d72b31a51
15 changed files with 71 additions and 126 deletions

View File

@ -30,21 +30,21 @@ open class ActionActionsHandler: MVMCoreJSONActionHandlerProtocol {
}
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionActionsModel else { return }
if model.concurrent {
// TODO: inspect warning.
await withThrowingTaskGroup(of: Void.self) { group in
for action in model.actions {
group.addTask{
try await MVMCoreActionHandler.shared()?.handleAction(with: action, additionalData: additionalData, delegateObject: delegateObject)
try await MVMCoreActionHandler.handleActionCheckingDelegate(with: action, additionalData: additionalData, delegateObject: delegateObject)
}
}
}
} else {
for action in model.actions {
try Task.checkCancellation()
try await MVMCoreActionHandler.shared()?.handleAction(with: action, additionalData: additionalData, delegateObject: delegateObject)
try await MVMCoreActionHandler.handleActionCheckingDelegate(with: action, additionalData: additionalData, delegateObject: delegateObject)
}
}
}

View File

@ -16,11 +16,11 @@ open class ActionBackHandler: MVMCoreJSONActionHandlerProtocol {
// Legacy code will use the old handler function and break the task chain here.
closure(JSON, additionalData)
} else {
try await performAction(model, delegateObject: delegateObject, additionalData: additionalData)
try await execute(with: model, delegateObject: delegateObject, additionalData: additionalData)
}
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
await withCheckedContinuation { continuation in
Task(priority: .userInitiated) {
await MVMCoreNavigationHandler.shared()?.removeCurrentViewController(true, completionHandler: {

View File

@ -11,7 +11,7 @@ import Foundation
open class ActionCallHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionCallModel else { return }
// https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/PhoneLinks/PhoneLinks.html#//apple_ref/doc/uid/TP40007899-CH6-SW1
try await ActionOpenUrlHandler.openURL(with: "tel://\(model.callNumber)")

View File

@ -15,6 +15,6 @@ open class ActionCancelHandler: MVMCoreJSONActionHandlerProtocol {
delegateObject?.actionDelegate?.handleCancel?(JSON, additionalData: additionalData)
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
}
}

View File

@ -26,7 +26,7 @@ open class ActionContactHandler: NSObject, MVMCoreActionHandlerProtocol, CNConta
}
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) async throws {
guard let model = model as? ActionContactModel else { return }
switch model.approach {

View File

@ -9,8 +9,8 @@
import Foundation
public protocol ActionDelegateProtocol: MVMCoreActionDelegateProtocol {
/// Allows the delegate to cancel the action.
func shouldPerform(action: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) -> Bool
/// Asks the delegate to perform the action.
func performAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) async throws
/// Allows the delegate to handle any custom actions that are not registered with the Action Handler.
func handlesUnknownAction(for model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws -> Bool
@ -18,8 +18,8 @@ public protocol ActionDelegateProtocol: MVMCoreActionDelegateProtocol {
public extension ActionDelegateProtocol {
func shouldPerform(action: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) -> Bool {
return true
func performAction(with model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) async throws {
try await MVMCoreActionHandler.shared()?.handleAction(with: model, additionalData: additionalData, delegateObject: delegateObject)
}
func handlesUnknownAction(for model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws -> Bool {

View File

@ -11,5 +11,5 @@ import Foundation
open class ActionNoopHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {}
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {}
}

View File

@ -49,7 +49,7 @@ open class ActionOpenPageHandler: MVMCoreJSONActionHandlerProtocol {
}
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionOpenPageModel else { return }
do {
let json = try MVMCoreActionHandler.convertActionToJSON(model)

View File

@ -24,7 +24,7 @@ extension String {
open class ActionOpenSMSHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionOpenSMSModel else { return }
// https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/SMSLinks/SMSLinks.html#:~:text=Note%3A%20SMS%20text%20links%20are,number%20of%20the%20SMS%20message.
let string = try "sms:\(model.phoneNumber)&body=\(model.message ?? "")".addingPercentEncodingThrowable(withAllowedCharacters: CharacterSet.urlQueryAllowed)

View File

@ -63,10 +63,10 @@ open class ActionOpenUrlHandler: MVMCoreJSONActionHandlerProtocol {
}
open func performAction(with JSON: [AnyHashable : Any], model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
try await performAction(model, delegateObject: delegateObject, additionalData: additionalData)
try await execute(with: model, delegateObject: delegateObject, additionalData: additionalData)
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionOpenUrlModel else { return }
// Try loading the app url first, otherwise fall back to browser url.

View File

@ -17,10 +17,10 @@ open class ActionPreviousSubmitHandler: MVMCoreJSONActionHandlerProtocol {
// Conform to MVMCoreJSONActionHandlerProtocol To allow for legacy handleOpenPage delegate
open func performAction(with JSON: [AnyHashable : Any], model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
json = JSON
try await performAction(model, delegateObject: delegateObject, additionalData: additionalData)
try await execute(with: model, delegateObject: delegateObject, additionalData: additionalData)
}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let loadObject = (delegateObject?.actionDelegate as? MVMCoreViewControllerProtocol)?.loadObject,
let previousRequest = loadObject?.requestParameters else { return }

View File

@ -11,7 +11,7 @@ import Foundation
open class ActionRestartHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionRestartModel else { return }
let _: Void = try await withCheckedThrowingContinuation { continuation in

View File

@ -11,7 +11,7 @@ import Foundation
open class ActionSettingHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
try await ActionOpenUrlHandler.openURL(with: await UIApplication.openSettingsURLString)
}
}

View File

@ -11,7 +11,7 @@ open class ActionShareHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) async throws {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) async throws {
guard let model = model as? ActionShareModel else { return }
var shareData: [Any]
switch model.sharedType {

View File

@ -11,10 +11,8 @@ import Foundation
/// Handlers that can be registered and used by the MVMCoreActionHandler to handle actions.
public protocol MVMCoreActionHandlerProtocol: ModelHandlerProtocol {
init()
/// Legacy function to handle actions.
func handleAction(_ model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?)
func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws
func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws
}
/// Protocol used to bridge legacy, non model based code. Allows us to keep the original json intact and not lose key values during decode/encode.
@ -23,16 +21,6 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol {
func performAction(with JSON: [AnyHashable : Any], model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws
}
extension MVMCoreActionHandlerProtocol {
public func handleAction(_ model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) {
}
public func performAction(_ model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
handleAction(model, additionalData: additionalData, delegateObject: delegateObject)
}
}
@objc open class MVMCoreActionHandler: NSObject {
enum ActionError: MVMError, CustomStringConvertible {
@ -50,6 +38,8 @@ extension MVMCoreActionHandlerProtocol {
}
}
private let jsonKey = "MVMCore.JSON"
/// Returns the action handler stored in the MVMCoreObject
@objc(sharedActionHandler)
public static func shared() -> Self? {
@ -93,11 +83,6 @@ extension MVMCoreActionHandlerProtocol {
}
}
/// Handles the error by calling actionDelegate handleActionError, else ActionHandler defaultHandleActionError.
open func handle(errorObject: MVMCoreErrorObject, delegateObject: DelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) {
delegateObject?.actionDelegate?.handleActionError?(errorObject, additionalData: additionalData) ?? defaultHandleActionError(errorObject, additionalData: additionalData)
}
/// Returns a common description for the error location.
@objc public static func getErrorLocation(with delegate: MVMCoreActionDelegateProtocol?, actionType: String) -> String {
return "\(String(describing: delegate))_\(actionType)"
@ -105,30 +90,49 @@ extension MVMCoreActionHandlerProtocol {
// MARK: - Action Handling
/// Convenience function for letting the actionDelegate handle the action, else the handler handles the action.
public static func handleActionCheckingDelegate(with model: ActionModelProtocol, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) async throws {
if let closure = (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.performAction {
try await closure(model, additionalData, delegateObject)
} else {
try await MVMCoreActionHandler.shared()?.handleAction(with: model, additionalData: additionalData, delegateObject: delegateObject)
}
}
/// Convenience function for letting the actionDelegate handle the action, else the handler handles the action.
@discardableResult
public static func asyncHandleActionCheckingDelegate(with model: ActionModelProtocol, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> Task<Void, Error> {
return Task(priority: .userInitiated) {
try await handleActionCheckingDelegate(with: model, additionalData: additionalData, delegateObject: delegateObject)
}
}
/// Handle an action with the given model.
open func handleAction(with model: ActionModelProtocol, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) async throws {
try Task.checkCancellation()
var additionalData = MVMCoreActionHandler.setUUID(additionalData: additionalData)
let json = try additionalData?.removeValue(forKey: jsonKey) as? [AnyHashable : Any] ?? MVMCoreActionHandler.convertActionToJSON(model)
let additionalData = MVMCoreActionHandler.setUUID(additionalData: additionalData)
// Allow the delegate to intercept.
guard (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.shouldPerform(action: model, additionalData: additionalData, delegateObject: delegateObject) ?? true else { return }
// Log the action
delegateObject?.actionDelegate?.logAction?(withActionInformation: json, additionalData: additionalData)
MVMCoreActionHandler.log(string: "Begin Action: \(model.actionType)", additionalData: additionalData)
defer {
MVMCoreActionHandler.log(string: "End Action: \(model.actionType)", additionalData: additionalData)
}
// Log the action
logAction(with: model, additionalData: additionalData, delegateObject: delegateObject)
do {
MVMCoreActionHandler.log(string: "Begin Action: \(model.actionType)", additionalData: additionalData)
let handlerType = try ModelRegistry.getHandler(model) as! MVMCoreActionHandlerProtocol.Type
let handler = handlerType.init()
try await handler.performAction(model, delegateObject: delegateObject, additionalData: additionalData)
if let handler = handler as? MVMCoreJSONActionHandlerProtocol {
// Needed until we can remove legacy delegate functions.
try await handler.performAction(with: json, model: model, delegateObject: delegateObject, additionalData: additionalData)
} else {
try await handler.execute(with: model, delegateObject: delegateObject, additionalData: additionalData)
}
} catch ModelRegistry.Error.handlerNotMapped {
try Task.checkCancellation()
// Allows custom handling if there no handler for the action.
guard try await handleUnregisteredAction(with: model, json: model.toJSON()!, additionalData: additionalData, delegateObject: delegateObject) else {
guard try await handleUnregisteredAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject) else {
MVMCoreActionHandler.log(string: "Failed Action Unknown", additionalData: additionalData)
throw ActionError.unknownAction(type: model.actionType)
}
@ -138,37 +142,6 @@ extension MVMCoreActionHandlerProtocol {
}
}
/// Performs the action as a task and returns immediately.
@discardableResult
open func asyncHandleAction(with model: ActionModelProtocol, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) -> Task<Void, Error> {
let task = Task(priority: .userInitiated) {
try Task.checkCancellation()
do {
let json = try MVMCoreActionHandler.convertActionToJSON(model)
try await handleAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject)
} catch {
try Task.checkCancellation()
let errorObject = getActionErrorObject(for: error, actionType: model.actionType, delegateObject: delegateObject)
handle(errorObject: errorObject, delegateObject: delegateObject, additionalData: additionalData)
}
}
Task {
//try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
//task.cancel()
}
Task {
let result = await task.result
do {
try result.get()
print("ActionHandler: task done")
} catch {
print("ActionHandler: \(error)")
}
}
return task
}
// MARK: - Subclassables
/// Subclass to log the action was fired.
@ -177,6 +150,11 @@ extension MVMCoreActionHandlerProtocol {
delegateObject?.actionDelegate?.logAction?(withActionInformation: model.toJSON(), additionalData: additionalData)
}
open func defaultHandle(error: Error, model: ActionModelProtocol, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject?) {
let errorObject = getActionErrorObject(for: error, actionType: model.actionType, delegateObject: delegateObject)
defaultHandleActionError(errorObject, additionalData: additionalData)
}
/// Logs the error.
@objc open func defaultHandleActionError(_ error: MVMCoreErrorObject, additionalData: [AnyHashable: Any]?) {
guard error.logError else { return }
@ -186,11 +164,8 @@ extension MVMCoreActionHandlerProtocol {
// MARK: - Legacy Holdovers
static public func setUUID(additionalData: [AnyHashable: Any]?) -> [AnyHashable: Any]? {
var additionalData = additionalData ?? [:]
if additionalData.optionalStringForKey("Action-UUID") == nil {
additionalData["Action-UUID"] = UUID().uuidString
}
return additionalData
guard getUUID(additionalData: additionalData) == nil else { return additionalData }
return additionalData.dictionaryAdding(key: "Action-UUID", value: UUID().uuidString)
}
static public func getUUID(additionalData: [AnyHashable: Any]?) -> String? {
@ -212,7 +187,12 @@ extension MVMCoreActionHandlerProtocol {
throw ModelRegistry.Error.keyNotFound
}
let model = try MVMCoreActionHandler.createModel(with: json, delegateObject: delegateObject)
try await handleAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject)
if let closure = (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.performAction {
// Allow newer delegates to handle calls from legacy functions
try await closure(model, additionalData, delegateObject)
} else {
try await handleAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject)
}
} catch {
let actionType = json?.optionalStringForKey(KeyActionType)
switch error {
@ -227,7 +207,7 @@ extension MVMCoreActionHandlerProtocol {
default:
MVMCoreActionHandler.log(string: "Failed Action: Error \(error)", additionalData: additionalData)
let errorObject = getActionErrorObject(for: error, actionType: actionType ?? "noAction", delegateObject: delegateObject)
handle(errorObject: errorObject, delegateObject: delegateObject, additionalData: additionalData)
delegateObject?.actionDelegate?.handleActionError?(errorObject, additionalData: additionalData) ?? defaultHandleActionError(errorObject, additionalData: additionalData)
}
}
}
@ -245,43 +225,8 @@ extension MVMCoreActionHandlerProtocol {
/// Bridges the legacy json using functions and the new model using functions.
open func handleAction(with model: ActionModelProtocol, json: [AnyHashable: Any], additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) async throws {
try Task.checkCancellation()
let additionalData = MVMCoreActionHandler.setUUID(additionalData: additionalData)
MVMCoreActionHandler.log(string: "Begin Action: type: \(model.actionType) json: \(String(describing: json))", additionalData: additionalData)
defer {
MVMCoreActionHandler.log(string: "End Action: \(model.actionType)", additionalData: additionalData)
}
// Allow the delegate to intercept.
guard (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.shouldPerform(action: model, additionalData: additionalData, delegateObject: delegateObject) ?? true else {
MVMCoreActionHandler.log(string: "Action should not be performed: \(model.actionType)", additionalData: additionalData)
return
}
try Task.checkCancellation()
// Log the action
delegateObject?.actionDelegate?.logAction?(withActionInformation: json, additionalData: additionalData)
do {
MVMCoreActionHandler.log(string: "Begin Action: \(model.actionType)", additionalData: additionalData)
let handlerType = try ModelRegistry.getHandler(model) as! MVMCoreActionHandlerProtocol.Type
let handler = handlerType.init()
if let handler = handler as? MVMCoreJSONActionHandlerProtocol {
try await handler.performAction(with: json, model: model, delegateObject: delegateObject, additionalData: additionalData)
} else {
try await handler.performAction(model, delegateObject: delegateObject, additionalData: additionalData)
}
} catch ModelRegistry.Error.handlerNotMapped {
try Task.checkCancellation()
// Allows custom handling if there no handler for the action.
guard try await handleUnregisteredAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject) else {
MVMCoreActionHandler.log(string: "Failed Action Unknown", additionalData: additionalData)
throw ActionError.unknownAction(type: model.actionType)
}
} catch {
MVMCoreActionHandler.log(string: "Failed Action \(error)", additionalData: additionalData)
throw error
}
let additionalData = additionalData.dictionaryAdding(key: jsonKey, value: json)
try await handleAction(with: model, additionalData: additionalData, delegateObject: delegateObject)
}
/// Subclass to handle and any actions where a handler was not registered. Checks with the delegate handlesUnknownAction function