前言
iOS的回调机制
在iOS开发中,回调机制的实现主要有两种:
- 利用代理设计模式,制定好一套回调协议,在需要进行向外回调的实例中添加代理属性,当要进行回调的时候,直接调用自身代理属性的回调方法。在iOS官方API中大多数都是采用这套方案。
- 利用闭包(block),采用函数式编程思想,将一段代码作为参数传入方法中,在需要向外回调时,直接调用之前传进来的代码。
由于使用第一种方法,代码量相对较多,而且代理关系容易变得很复杂,所以一般建议使用第二种方法进行回调,灵活方便。
MVVM与MVP中的回调
在使用MVVM
或MVP
架构时,ViewController
其实在充当着View
的角色,当ViewController
中有用户事件触发时,其立即将事件流传递给ViewModel(MVVM)
或Presenter(MVP)
,然后对事件进行处理、转换,如将用户按下按键的事件转换成向服务器发送HTTP请求事件,然后当接收到HTTP响应后再把事件转换为改变ViewController
界面控件显示状态的事件,形成了一套事件的流式转换,所以,我们能看到,在这一条事件流转换的过程中,ViewController
充当着事件的最初发送者以及最后接收者,ViewModel
或Presenter
充当了事件的处理、转换者,在这过程中,必然要使用到回调机制。
响应式、函数式框架中的回调
一些第三方的响应式
、函数式
框架,如ReactiveCocoa
、RxSwift
等,能够更加优雅地实现MVVM
或MVP
架构,在对事件回调的解决中,可谓是体现了代码的艺术之美,将事件流高度抽象,让开发者能够方便快捷地实现事件的转换。不仅如此,这些框架还提供了异步的解决方案,使开发者能够设定事件在流中能够跨线程传输。
设计iOS异步回调更加优雅的轮子
我受ReactiveCocoa
以及RxSwift
的启发,然后自己试着去实现一套iOS异步回调的解决方案,使得我在使用MVVM
或MVP
架构小规模项目时没必要再使用ReactiveCocoa
、RxSwift
等较为大型的框架,直接套上这小轮子即可~
原理
下面就是这个解决方案的执行逻辑图:
如图,这个轮子的名字我起作
Command
,它可以横跨两层,分别是
上层
以及
底层
,
上层
即对应了
MVVM
与
MVP
架构中的
View
层,
底层
对应的是
MVVM
中的
ViewModel
层,
MVP
中的
Presenter
层。
这两层分工明确,上层接收到用户的反馈事件,并将反馈事件所携带的初始数据传入底层,并且,将底层处理好的结果数据反映到界面上去,如修改某个空间的显示状态等;底层则专注于事件的处理、加工、转换,如用户点击了某个按钮,上层将接受到的点击事件传递给了底层,底层先将按钮的点击事件转换为网络请求事件,当网络请求完毕后,底层再将请求响应事件进行解析,转换成上层的显示事件,最后回调到上层,刷新显示。
这就是上面所提到的例子的逻辑图:
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))
}
当用户按下登录按钮时,触发loginCommand
的execute
方法,并将用户输入的用户名以及密码传入Command中
override func viewDidLoad() {
super.viewDidLoad()
// 装载视图
self.setupViews()
// 布局视图
self.layoutViews()
self.viewModel.loginCommand.handle(next: { result in
// 登录验证后的回调
print(result ? "登录成功" : "登录失败")
})
}
我们在ViewController
中的viewDidLoad
方法中进行loginCommand
的回调处理,需调用loginCommand
的handle
方法,传入处理闭包。
异步回调
以上,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
}
}
}
顾名思义,它代表的是回调处理的事件,分别有next
、error
、completed
:
- 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
类,我们可以先构建一个针对于UIControl
的Command
中介类:
// 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链接