探索实现iOS异步回调更加优雅的解决方案

前言

iOS的回调机制

在iOS开发中,回调机制的实现主要有两种:

  1. 利用代理设计模式,制定好一套回调协议,在需要进行向外回调的实例中添加代理属性,当要进行回调的时候,直接调用自身代理属性的回调方法。在iOS官方API中大多数都是采用这套方案。
  2. 利用闭包(block),采用函数式编程思想,将一段代码作为参数传入方法中,在需要向外回调时,直接调用之前传进来的代码。
    由于使用第一种方法,代码量相对较多,而且代理关系容易变得很复杂,所以一般建议使用第二种方法进行回调,灵活方便。

MVVM与MVP中的回调

在使用MVVMMVP架构时,ViewController其实在充当着View的角色,当ViewController中有用户事件触发时,其立即将事件流传递给ViewModel(MVVM)Presenter(MVP),然后对事件进行处理、转换,如将用户按下按键的事件转换成向服务器发送HTTP请求事件,然后当接收到HTTP响应后再把事件转换为改变ViewController界面控件显示状态的事件,形成了一套事件的流式转换,所以,我们能看到,在这一条事件流转换的过程中,ViewController充当着事件的最初发送者以及最后接收者,ViewModelPresenter充当了事件的处理、转换者,在这过程中,必然要使用到回调机制。

响应式、函数式框架中的回调

一些第三方的响应式函数式框架,如ReactiveCocoaRxSwift等,能够更加优雅地实现MVVMMVP架构,在对事件回调的解决中,可谓是体现了代码的艺术之美,将事件流高度抽象,让开发者能够方便快捷地实现事件的转换。不仅如此,这些框架还提供了异步的解决方案,使开发者能够设定事件在流中能够跨线程传输。

设计iOS异步回调更加优雅的轮子

我受ReactiveCocoa以及RxSwift的启发,然后自己试着去实现一套iOS异步回调的解决方案,使得我在使用MVVMMVP架构小规模项目时没必要再使用ReactiveCocoaRxSwift等较为大型的框架,直接套上这小轮子即可~

原理

下面就是这个解决方案的执行逻辑图:

探索实现iOS异步回调更加优雅的解决方案_第1张图片

如图,这个轮子的名字我起作 Command,它可以横跨两层,分别是 上层以及 底层上层即对应了 MVVMMVP架构中的 View层, 底层对应的是 MVVM中的 ViewModel层, MVP中的 Presenter层。
这两层分工明确,上层接收到用户的反馈事件,并将反馈事件所携带的初始数据传入底层,并且,将底层处理好的结果数据反映到界面上去,如修改某个空间的显示状态等;底层则专注于事件的处理、加工、转换,如用户点击了某个按钮,上层将接受到的点击事件传递给了底层,底层先将按钮的点击事件转换为网络请求事件,当网络请求完毕后,底层再将请求响应事件进行解析,转换成上层的显示事件,最后回调到上层,刷新显示。
这就是上面所提到的例子的逻辑图:
探索实现iOS异步回调更加优雅的解决方案_第2张图片

Command的作用就是这两层的Binder(粘合剂),专门负责沟通这两层,实现事件、数据的环形流向。

使用

简单的例子

在介绍Command的源码前首先来看下如何使用Command实现MVVM架构模式:

ViewModel

class ViewModel {
    //  Commands
    let loginCommand: Command<(String?, String?), Bool>
    
    init() {
        self.loginCommand = Command { nameAndPassword, effect in
            let userName = nameAndPassword.0
            let password = nameAndPassword.1
            effect.onNext(userName == "Tangent" && password == "123")
            effect.onCompleted()
            return NopCancelable()
        }
    }
}

ViewModel类有一Command属性loginCommand,它的泛型参数为一个数据类型都为String的二元元组,代表用户登录时输入的用户名以及密码,另一个泛型参数为一个布尔类型值,代表用户登录是否成功;在loginCommand构造时,我们利用构造函数中闭包参数中的参数值nameAndPassword来接受用户的事件数据输入,然后进行相应的处理,最后再通过闭包参数的的参数值effect将结果发送出去。

ViewController

/**
   当用户按下登录按钮所触发的方法
 */
func buttonTap(sender: AnyObject) {
    _ = self.viewModel.loginCommand.execute((userNameTextField.text, passwordTextField.text))
}

当用户按下登录按钮时,触发loginCommandexecute方法,并将用户输入的用户名以及密码传入Command中

override func viewDidLoad() {
    super.viewDidLoad()
        
    //  装载视图
    self.setupViews()
    //  布局视图
    self.layoutViews()

    self.viewModel.loginCommand.handle(next: { result in
        //  登录验证后的回调
        print(result ? "登录成功" : "登录失败")
    })
}

我们在ViewController中的viewDidLoad方法中进行loginCommand的回调处理,需调用loginCommandhandle方法,传入处理闭包。

异步回调

以上,Command的大致使用方法就是这样,但是,上面展示的使用只是针对于同步的单线程(在主线程中),如果我们进行异步的操作,如果说指定处理数据所在的线程,或者指定处理回调所在的线程,我们可以在execute方法以及handle方法的参数中指定所在线程队列:

//  指定回调处理所在的线程队列
self.viewModel.loginCommand.handle(on:DispatchQueue.main, next: { result in /* ... */ })

//  指定数据在处理时所在的线程队列
_ = self.viewModel.loginCommand.execute((userName, password), on: DispatchQueue.global())

取消操作

在有必要时,你可以取消正在进行的数据处理操作,这时我们需要了解一个协议Cancelable,我们使用它的方法cancel来取消一个Command所正在进行的操作。
在我们执行了execute方法后,我们会接收到一个返回值,这个返回值是Cancelable协议的实例,我们可以通过它来进行对指定Command操作的取消:

let cancelable = self.viewModel.loginCommand.execute((userName, password))
        
//  某个时候
cancelable.cancel()

在取消操作的时候,我希望能进行一些取消前有必要的动作,比如回收资源、关闭文件流等等,我们可以在构建Command的时候在其构造函数的闭包参数中传入一个返回指定操作的Cancelable:

self.loginCommand = Command { nameAndPassword, effect in
    //  login ...
    return TodoCancelable {
        //  Todo ...
    }
}

上面我们传入了一个TodoCancelable类型的Cancelable,我们可以在构造它的时候传入要在取消时执行的动作,也可以通过它的方法addTodo添加动作。
假如我们不需要在取消时做任何动作,我们可以直接返回一个NopCancelable实例,就像刚刚在上面所展示的简单例子一样。

源码编写

Event

//  MARK: - Event
enum Event {
    case next(Element)
    case error(Error)
    case completed
}

extension Event {
    var element: Element? {
        switch self {
        case let .next(element):
            return element
        default:
            return nil
        }
    }
}

顾名思义,它代表的是回调处理的事件,分别有nexterrorcompleted

  • next: 表明command已经完成了一项任务,并把完成的结果通过枚举关联值传递出来
  • error: 表明在执行的过程中有异常抛出了,并把异常类通过枚举关联值传递出来
  • completed: 表明command整个任务处理已经完成

注:当error、completed事件发出时,command会自动调用cancelable进行取消操作

Cancelable

//  MARK: - Cancelable
protocol Cancelable {
    
    var isCancel: Bool { get }
    
    func cancel()
}

class NopCancelable: Cancelable {
    
    var isCancel: Bool = false
    
    func cancel() {
        objc_sync_enter(self)
        isCancel = true
        objc_sync_exit(self)
    }
}

class TodoCancelable: Cancelable {
    
    var isCancel: Bool = false
    
    private var todos: [() -> ()] = []
    
    func addTodo(_ todo: @escaping () -> ()) {
        self.todos.append(todo)
    }
    
    func cancel() {
        objc_sync_enter(self)
        isCancel = true
        for todo in self.todos { todo() }
        objc_sync_exit(self)
    }
    
    init() {}
    
    init(_ todo: @escaping () -> ()) {
        self.todos.append(todo)
    }
    
}

这里有两个类实现了Cancelable协议,他们的作用我在上面已经说明了,在调用cancel方法的时候也进行了同步的操作,做到了线程的安全。

Effect

//  MARK: - Effect
protocol EffectType {
    
    associatedtype E
    
    func on(_ event: Event)
}

extension EffectType {
    
    final func onNext(_ element: E) {
        self.on(.next(element))
    }
    
    final func onCompleted() {
        self.on(.completed)
    }
    
    final func onError(_ error: Error) {
        self.on(.error(error))
    }
}

struct Effect: EffectType {
   
    typealias E = Element
    typealias EventHandler = (Event) -> ()

    let eventHandler: EventHandler
    
    init(eventHandler: @escaping EventHandler) {
        self.eventHandler = eventHandler
    }
    
    func on(_ event: Event) {
        self.eventHandler(event)
    }
}

Effect可以说是事件的发送者,我们在数据处理得出结果后,就通过它将结果发送出去。这里effect发送的事件与上面提到的Event相对应。

Command

//  MARK: - Command
protocol CommandType {
    
    associatedtype I
    associatedtype O
    
    init(_ todo: @escaping (I, Effect) throws -> Cancelable)
    
    func execute(_ input: I, on: DispatchQueue) -> Cancelable
    
    func handle(on: DispatchQueue, next: ((O) -> ())?, error: ((Error) -> ())?, completed:(() -> ())?)
}

class Command: CommandType {
    typealias I = In
    typealias O = Out
    
    let todo: (I, Effect) throws -> Cancelable
    
    var eventHandler: ((Event) -> ())?
    
    required init(_ todo: @escaping (I, Effect) throws -> Cancelable) {
        self.todo = todo
    }
    
    func execute(_ input: In, on: DispatchQueue = DispatchQueue.main) -> Cancelable {
        
        let cancelable = TodoCancelable()
        
        let effect = Effect { event in
            if cancelable.isCancel { return }
            self.eventHandler?(event)
            if case .error = event { cancelable.cancel() }
            if case .completed = event { cancelable.cancel() }
        }
        
        on.async {
            do {
                let mCancelable = try self.todo(input, effect)
                cancelable.addTodo {
                    mCancelable.cancel()
                }
            } catch let error {
                effect.onError(error)
            }
        }
        
        return cancelable
    }
    
    func handle(on: DispatchQueue = DispatchQueue.main, next: ((Out) -> ())? = nil, error: ((Error) -> ())? = nil, completed: (() -> ())? = nil) {
        let eventHandler: (Event) -> () = { event in
            on.async {
                switch event {
                case .next(let element):
                    next?(element)
                case .completed:
                    completed?()
                case .error(let err):
                    error?(err)
                }
            }
        }
        self.eventHandler = eventHandler
    }
}

这个就是重头戏,也就是这篇文章要讲的主角Command,其他它的思想挺简单,就是讲我们制定的处理数据的动作先包装并存储好,当我们要执行操作的时候,就把原本存好的动作拿出来进行调用,并传入执行的数据。
这里有几点需要拿出来探讨下:

  • 关于Effect以及异步的探讨: command中effect的加入可能会使得它的使用难度有所增加,因为结果并不是在todo闭包中直接返回,而是通过todo闭包中的effect参数将结果发送出去,其实这样设计的思想是为了切合异步多线程,而如果我们在设计command时只针对单线程环境,就大可不必那么麻烦,直接在todo闭包中计算完,返回即可。
  • 关于Cancelable的探讨: 我们在执行完command的execute方法后,其会返回一个Cancelable,而我们在构建Command传入todo闭包时,要求我们要传入一个Cancelable,但是,其两者并不指向同一实例。因为考虑到存在异步调用的情况,所以在todo执行完毕之前,可能execute已经返回了Cancelable了,所以我在execute调用的时候就创建了一个TodoCancelable,然后当todo返回Cancelable时,将其与上面的TodoCancelable进行绑定,使它们能够做到同步取消。
  • 关于同步执行的探讨: 如果我们没有指定command特定的执行或回调线程,其默认是运行在主线程上,但是,这并不代表当我们执行execute方法时,todo的内容就会立即执行并回调出来,而后再继续执行execute方法往下的内容,因为源码中,就算todo执行时在主线程队列中,但是它是被添加到GCD中的,意思是todo的执行会在下一次事件循环到来时才会去执行。

到此,Command的源代码就展示完毕了。

拓展

首先,我们利用函数式编程,将command植入到每个对象中:

//  MARK: - Commands
struct Commands {
    
    let base: Base
    
    init(_ base: Base) {
        self.base = base
    }
}

protocol CommandCompatible {
    
    associatedtype CompatibleType
    
    var cm: Commands { get set }
}

extension CommandCompatible {
    
    typealias T = Self
    
    var cm: Commands {
        get {
            return Commands(self)
        }
        
        set {
            //  保证能够修改cm中的属性,如object.cm.xxxCommand = xxx
        }
    }
}

extension NSObject: CommandCompatible {}

由于在扩展(extension)中无法添加存储属性,我们需要利用到关联对象:

//  MARK: - AssociatedObject
extension NSObject {
    func addObjectProperty(_ object: AnyObject, key: UnsafeRawPointer) {
        objc_setAssociatedObject(self, key, object, .OBJC_ASSOCIATION_RETAIN)
    }
    
    func getProperty(_ key: UnsafeRawPointer) -> Any {
        return objc_getAssociatedObject(self, key)
    }
}

对按钮的拓展

按钮属于UIControl类,我们可以先构建一个针对于UIControlCommand中介类:

//  MARK: - ControlEventCommand
class ControlEventCommand {
    
    let command: Command
    let control: UIControl
    let event: UIControlEvents
    
    init(control: UIControl, command: Command, event: UIControlEvents) {
        self.control = control
        self.command = command
        self.event = event
        control.addTarget(self, action: #selector(ControlEventCommand.onEvent), for: event)
    }
    
    deinit {
        self.control.removeTarget(self, action: #selector(ControlEventCommand.onEvent), for: self.event)
    }
    
    @objc func onEvent() {
        _ = self.command.execute(())
    }
    
}

然后我们就可以对某个按钮的点击链接到command中:

fileprivate var tapCommandKey = 0
extension Commands where Base: UIButton {
    var tapCommand: Command? {
        set {
            if let value = newValue {
                let cec = ControlEventCommand(control: base, command: value, event: .touchUpInside)
                base.addObjectProperty(cec, key: &tapCommandKey)
            }
        }
        
        get {
            let cec = base.getProperty(&tapCommandKey) as? ControlEventCommand
            return cec?.command
        }
    }
}

怎么使用呢?

ViewModel

class ViewModel {
    
    let buttonTapCommand: Command
    
    init() {
        self.buttonTapCommand = Command { _, effect in
            //  todo ...
            return NopCancelable()
        }
    }
}

ViewController

override func viewDidLoad() {
    super.viewDidLoad()
        
    //  装载视图
    self.setupViews()
    //  布局视图
    self.layoutViews()

    self.uploadButton.cm.tapCommand = self.viewModel.buttonTapCommand
        
    self.viewModel.buttonTapCommand.handle(next: { output in
        //  todo...
    })
}

对UITextField的拓展

我们想要在textField中的文字改变是就立即触发command的操作,首先我们知道,想要监听textField文字的输入,我们可以通过通知机制,所以我们可以构造一个针对于通知的command中介者:

//  MARK: - NotificationCommand
class NotificationCommand {
    
    let command: Command
    let mapper: (NSNotification) -> T
    
    init(object: Any, notificationName: NSNotification.Name?, command: Command, mapper: @escaping (NSNotification) -> T) {
        self.command = command
        self.mapper = mapper
        NotificationCenter.default.addObserver(self, selector: #selector(NotificationCommand.handleNotification(notification:)), name: notificationName, object: object)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func handleNotification(notification: NSNotification) {
        _ = self.command.execute(self.mapper(notification))
    }
}

然后我们就可以对textField进行扩展了:

fileprivate var textCommandKey = 0
extension Commands where Base: UITextField {
    var textCommand: Command? {
        set {
            if let value = newValue {
                let notificationCommand = NotificationCommand(object: base, notificationName: NSNotification.Name.UITextFieldTextDidChange, command: value, mapper: { notification in
                    (notification.object as! UITextField).text ?? ""
                })
                base.addObjectProperty(notificationCommand, key: &textCommandKey)
            }
        }
        
        get {
            let notificationCommand = base.getProperty(&textCommandKey) as? NotificationCommand
            return notificationCommand?.command
        }
    }
}

用法也与上面的按钮拓展差不多,就不多说了。

参考资料

RxSwift -- Github链接
ReactiveCocoa -- Github链接

你可能感兴趣的:(探索实现iOS异步回调更加优雅的解决方案)