1.10 在北京参加了中国首届 Swift 开发者大会,其中很多人都提到一个观点:POP (protocol oriented programming)面向协议编程。结合之前翻译组一些关于 POP 的译文,觉得 POP 将势不可挡。
环境信息:
Mac OS X 10.11.3
Xcode 7.2.1
iOS 9.2
Swift 2
正文
在看 《Mixins 比继承更好》 一文中,其中提到了 OOP 和 POP 的一些优劣势。其中 OOP 的 God object 是最让框架设计者头疼的。文中列举了一个 Burger Menu 的例子,在此我再描述一下,因为本文会细化他的实现。
完整 Demo 下载地址:
https://github.com/saitjr/STBurgerButtonManager.git
需求
假设现在正在为一款 App 设计框架,所有的一级页面左上角都有一个菜单按钮,点击以后触发同一事件:弹出警告框。
OOP
按照以前的思想,首先,我们会去给一级页面写一个公共父类: MainBaseViewController
(当然 MainBaseViewController
很有可能还会有个父类叫 BaseViewController
),然后在 MainBaseViewController
去添加菜单按钮与相应的事件。UML 图大致如下:
问题
但是在实际项目中,不是所有界面都会继承 MainBaseViewController
,可能还会去继承 UITableViewController
或者 UICollectionViewController
。
方法一:当然,也可以选择放弃使用 UITableViewController
,选择使用 UIViewController
+ UITableView
来替代,然后开心的继承 MainBaseViewController
。
方法二 :试试面向协议编程吧。
解决
在这个例子中,POP 的更有一种模块化的感觉,需要什么样的功能(行为),只需要遵循协议即可。关于为什么要将行为封装为协议的案例,可以查看文章 《Mixins 比继承更好》 。
首先创建一个 BurgerMenuManager.swift
的文件,添加一个名为 BurgerMenuManager
的协议,并为此协议添加一个初始化按钮的方法 needBurgerButton
。
protocol BurgerMenuManager { func needBurgerManager(); }
在新的 Swift 2.0 的语法中,允许协议有默认实现,这也为协议的设计提供了新思路。因为每一个菜单按钮的样式与事件都相同,所以直接给出默认实现就行,而没必要每个遵循协议的类都去实现。在当前文件下,添加以下代码:
extension BurgerMenuManager { func needBurgerManager() { let burgerButton = UIBarButtonItem(title: "菜单", style: .Plain, target: nil, action: nil) // 添加按钮到 navigation 上 } }
暂时不给按钮添加事件,先来考虑如何将 burgerButton
添加到 navigation 上。
在当前的 extension 中,没有办法使用 self
,是因为编译器并不知道会有哪些类型来遵循 BurgerMenuManager
协议,所以,只需要对遵循协议的类型进行限制即可。根据当前需求,所有遵循协议的至少都是 UIViewController
,那么,对刚才的 extension 做以下修改:
extension BurgerMenuManager where Self: UIViewController { func needBurgerManager() { ... // 现在就可以添加按钮了 self.navigationItem.leftBarButtonItem = burgerButton } }
接着,需要给按钮添加事件。想想,这还不简单,直接给 target 和 action 不就行了吗。但是这是在 extension 中,要实现起来,还真不是很方便(或许是我没找到优雅的解决方案,希望大家能参与讨论)。
首先试试直接添加 target 和 action 的方式吧:
extension BurgerMenuManager { func needBurgerManager() { let burgerButton = UIBarButtonItem(title: "菜单", style: .Plain, target: self, action: Selector("burgerItemTapped")) self.navigationItem.leftBarButtonItem = burgerButton } func burgerItemTapped { print("tapped") } }
实验之前,先在 Main.Storyboard
中,给 ViewController
Embed In 一个 Navigation View Controller 。然后让 ViewController
遵循 BurgerMenuManager
协议,并在 viewDidLoad
中初始化菜单按钮:
class ViewController: UIViewController, BurgerMenuManager { override func viewDidLoad() { super.viewDidLoad() needBurgerManager() } }
运行程序,点击菜单按钮,程序崩溃。原因: ViewController
中找不到 burgerItemTapped
方法。因为 action 不能写在 extension
中。
解决方案有两种,但核心都是给 item 绑定 block:
第一种:使用 runtime 给绑定 UIBarButtonItem
绑定 block,然后通过 block 来添加按钮的 action;
第二种: 继承 UIBarButtonItem
,添加 block 属性,然后通过 block 来添加按钮的 action;
这几种解决方案在 Objective-C 中并不陌生,即使是在 Swift 中,使用方式也大同小异。根据项目需求,可任意选择其中一种,并没有一定的写法。
如果项目中有很多 UIBarButtonItem
需要使用到 runtime 绑定的方式,那么这里也直接用这种方式就行。当然,也有可能会觉得,Swift 在有意的弱化 runtime,再大量使用或许不是很优雅,那么选择第二种方案也是不错的。
第一种(runtime):
首先给 UIBarButtonItem
添加 block。
typealias STBarButtonItemBlock = () -> () class STBarButtonItemWrapper { var block: STBarButtonItemBlock? init(block: STBarButtonItemBlock) { self.block = block } } struct STConst { static var STBarButtonItemWrapperKey = "STBarButtonItemWrapperKey" } extension UIBarButtonItem { convenience init(title: String?, style: UIBarButtonItemStyle, block: STBarButtonItemBlock) { self.init() self.title = title self.style = style target = self action = "buttonTapped" let wrapper = STBarButtonItemWrapper(block: block) objc_setAssociatedObject(self, &STConst.STBarButtonItemWrapperKey, wrapper, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } func buttonTapped() { guard let wrapper = objc_getAssociatedObject(self, &STConst.STBarButtonItemWrapperKey) as? STBarButtonItemWrapper else { return } guard let block = wrapper.block else { return } block() } }
需要注意的是, objc_setAssociatedObject
的第三个参数要是对象。所以需要将 block 放入对象中,然后使用runtime 将该对象添加为 UIBarButtonItem
成员变量。
然后,在 protocol 的默认实现中,将 needBurgerButton()
方法实现改为:
extension BurgerButtonManager where Self: UIViewController { func needBurgerButton() { let burgerButton = UIBarButtonItem(title: "菜单", style: .Plain) { print("菜单显示") } self.navigationItem.leftBarButtonItem = burgerButton } }
第二种(继承):
相比 runtime,继承的实现就简单多了。首先给继承类添加便利构造方法,用于绑定 block:
typealias STBarButtonItemBlock = ()->() class BlockBarButtonItem: UIBarButtonItem { var block: STBarButtonItemBlock? convenience init(title: String, style: UIBarButtonItemStyle, block: STBarButtonItemBlock) { self.init(title: title, style: style, target: nil, action: "buttonTapped") self.target = self self.block = block } convenience init(image: UIImage, style: UIBarButtonItemStyle, block: STBarButtonItemBlock) { self.init(image: image, style: style, target: nil, action: "buttonTapped") self.target = self self.block = block } func buttonTapped() { guard let block = block else { return } block() } }
然后,将默认实现的 needBurgerButton()
方法实现改为:
extension BurgerButtonManager where Self: UIViewController { func needBurgerButton() { let item = BlockBarButtonItem(title: "菜单", style: .Plain) { print("菜单显示") } self.navigationItem.leftBarButtonItem = item } }
最后
完整的 Demo 已上传 Github 。在完成过程中,也可翻译组的小伙伴进行了讨论。感谢 @ray16897188 , @小锅 , @靛青 。