iOS 中的 Delegate 设计模式

最近写的 iOS/Swift 代码比较多,很久没动笔又没怎么写过技术类的文章。所以决定从小小的总结一下 Delegate 模式开始写一写,因此这也是一篇入门文章。

What is Delegate ?

我想很多同学第一次遇见 Delegate 这个词是在写 tableView 的时候。那时候我也只是每每需要用的时候就照着这种模式写上 tableView.delegate = self, tableView.dataSource = self 也并不理解究竟是什么意思。

应该说 delegate 是 Cocoa/CocoaTouch 中的一种设计模式,正如前面提到的 tableView 一样,Cocoa 框架中很多地方都用到了这种设计模式。这种设计模式中我们可以将一个对象的一些功能委托给另一个对象来实现。因此使用 delegate 模式可以帮助我们设计出更松耦合的代码。

delegate 使用起来非常简单。假设我们现在需要一个电视机和一个音响,我们想要将电视机播放声音的职责委托给外置的音响来实现,我们就可以这样编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 首先定义一个 Protocol 协议
protocol PlayAudioDelegate {
func playAudio()
}
// 编写一个遵循 PlayAudioDelegate 协议的音响对象,实现协议中的方法
class Speaker: PlayAudioDelegate {
func playAudio() {
printLog("Start Play!")
}
}
// 编写电视机
class TV {
// delegate 可以为任何遵守 PlayAudioDelegate 协议的对象
var delegate: PlayAudioDelegate?
func play() {
// 当 delegate 对象存在时,调用对象的 playAudio 方法
delegate?.playAudio()
}
}
let tv = TV()
let speaker = Speaker()
tv.delegate = speaker
tv.play() // printLog 将输出 "Start Play!"

如上,使用起来并不复杂,但还是存在一些问题的。

可选

上面的例子里,我们使用 PlayAudioDelegate 这个 Protocol 来定义了一些方法,我们也知道 Swift 中当让一个类遵循某个协议的时候,这个类中必须将 Protocol 中定义的所有属性和方法实现,否则编译器将报错。

但实际情况中,我们可能只需要当前类实现 Protocol 中的部分方法,就好像在使用 tableView 中,我们并不是想要每一次都需要将 UITableViewDelegate 和 UITableViewDataSource 中所有方法都全部实现。

我们有两种方法来实现 Protocol 中的可选。

@objc

一种是借用 @objc 关键字来实现。我们可以在定义 Protocol 时在前面加上 @objc 关键字,然后在协议中需要定义为可选的方法前也加上 @objc 和 optional 来定义一个可选方法。如:

1
2
3
4
@objc protocol PlayMediaDelegate {
@objc optional func playAudio()
@objc optional func playVideo()
}

这样在需要类遵循 PlayMediaDelegate 协议时其中的 playAudio() 和 playVideo() 方法就不再是必须要实现的。

extension Protocol

第二种方法,也是我个人更常用的方法,就是将协议扩展,给出协议中方法的默认实现来达到不需要在遵循 protocol 的类中必须实现所有方法的效果。

我们使用 Swift 中的 extension 来对 Protocol 进行扩展。

1
2
3
4
5
6
7
8
9
10
11
// MARK: - PlayMediaDelegate Protocol
protocol PlayMediaDelegate {
func playAudio()
}
// MARK: - Extension PlayMediaDelegate Protocol
extension PlayMediaDelegate {
func playAudio() {
printLog("Play Audio Now")
}
}

这样,我们通过在扩展中给出需要可选方法的默认实现,也可以达到效果。在使用时,遵循当前 Protocol 的类在不实现方法时将使用 extension 中的默认实现。当然,你也可以再类中给出这个方法重新实现。

weak

倘若你对内存管理和 ARC 有一定的理解,你可能会发现我们在上面的代码中可能存在一些问题( 内存管理和 ARC 这里就不展开了)。
为了避免循环引用,我们可能会通过尝试在 delegate 前加上 weak 来声明当前类不负责保持 delegate 这个对象,让它的销毁由外部控制。从而利用 ARC 的这个特性帮助我们完成内存管理。如将上面的例子改成:

1
2
3
4
5
6
7
protocol PlayAudioDelegate {
func playAudio()
}
class TV {
weak var delegate: PlayAudioDelegate?
}

但很快就会发现编译器报错了。

为什么?
Apple 爸爸的这篇文档 里,我们可以知道:

1) Swift 中的 Protocol 是可以 class, struct, enum 这些类型都可以遵循的
2) 而 struct, enum 并不通过 ARC 来管理内存,所以 ARC 中的 weak 并不能用来修饰

那应该怎么办?

答案是将 Protocol 限制为 Class-Only Protocol,也就是只能被 class 而不能被其他类型遵循的协议。

1
2
3
4
5
6
7
protocol PlayAudioDelegate: class {
func playAudio()
}
class TV {
weak var delegate: PlayAudioDelegate?
}

这样就可以啦。

此外,为什么 class TV 中的 delegate 我们用了 optional 类型,因为我们使用了 weak 来修饰 delegate, 这就有可能导致 delegate 在使用过程中变为 nil,所以我们需要使用 optional 类型。否则编译器也将报 “ ‘weak’ variable shoule have optional type ‘…?’ “ 错。

See you

Created by ROC Zhang on 2016-10-24.
Copyright © 2016 ROC Zhang. All rights reserved.