转载

【iOS】用面向协议的思想打造菜单按钮

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 设计框架,所有的一级页面左上角都有一个菜单按钮,点击以后触发同一事件:弹出警告框。

【iOS】用面向协议的思想打造菜单按钮

OOP

按照以前的思想,首先,我们会去给一级页面写一个公共父类: MainBaseViewController (当然 MainBaseViewController 很有可能还会有个父类叫 BaseViewController ),然后在 MainBaseViewController 去添加菜单按钮与相应的事件。UML 图大致如下:

【iOS】用面向协议的思想打造菜单按钮

问题

但是在实际项目中,不是所有界面都会继承 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 , @小锅 , @靛青

原文  http://www.saitjr.com/ios/ios-burger-menu-button-pop.html
正文到此结束
Loading...