用 Swift 实现轻量的属性监听系统

开发 前端
本文的主要目的是解决客户端开发中对“模型的一处修改,UI 要多处更新”的问题。当然,我们要知晓解决方案的细节和思考过程,以及看到其能达到的效果。我们会用到函数式编程的思想,以及伟大的“泛型”。

[[419498]]

前言

本文的主要目的是解决客户端开发中对“模型的一处修改,UI 要多处更新”的问题。当然,我们要知晓解决方案的细节和思考过程,以及看到其能达到的效果。我们会用到函数式编程的思想,以及伟大的“泛型”。请相信我,我们并非为了使用新技术而使用新技术。如果一个问题有更好的方法去解决,那为何不替换掉旧方法呢?

正文

假如你正在写的 App 是有用户系统的,也就是用户需要管理自己的信息,如修改名字、头发颜色之类的。

单独拿名字来说,除开在修改界面,可能在系统的其他界面也会使用到它,这就涉及到在更新名字后再更新其他界面的问题。

你的第一直觉是什么呢?多半是使用通知,也就是 NSNotification。这是一种很好的办法,虽然逻辑松散,写起来有些麻烦。比如要定义一个通知名,发送通知,各界面都监听通知再处理,等等。

例如,对于如下 3 个界面,都有显示名字。通过 push,用户可以在第 3 个界面里修改名字,这就需要更新这 3 个界面的名字,不然用户 pop 返回时就会觉得奇怪。

UI

假如我们的名字放在一个叫做 UserInfo 的类里(访问和修改都使用单例),如下:

  1. class UserInfo { 
  2.  
  3.     static let sharedInstance = UserInfo() 
  4.  
  5.     struct Notification { 
  6.         static let NameChanged = "UserInfo.Notification.NameChanged" 
  7.     } 
  8.  
  9.     var name: String = "NIX" { 
  10.         didSet { 
  11.             NSNotificationCenter.defaultCenter().postNotificationName(Notification.NameChanged, object: name
  12.         } 
  13.     } 

同时我们定义了一个通知。在 name 被改变后就发出这个通知,并把 name 传出去。

三个界面分别为 FirstViewController、SecondViewController、ThirdViewController,都有一个 button 在正中间。其中前两个负责 push,最后一个点击后可以改名字。因此,对于 FirstViewController 来说:

  1. class FirstViewController: UIViewController { 
  2.  
  3.     @IBOutlet weak var nameButton: UIButton! 
  4.  
  5.     override func viewDidLoad() { 
  6.         super.viewDidLoad() 
  7.  
  8.         title = "First" 
  9.  
  10.         nameButton.setTitle(UserInfo.sharedInstance.name, forState: .Normal) 
  11.  
  12.         NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateUI:"name: UserInfo.Notification.NameChanged, object: nil) 
  13.     } 
  14.  
  15.     func updateUI(notification: NSNotification) { 
  16.         if let name = notification.object as? String { 
  17.             nameButton.setTitle(name, forState: .Normal) 
  18.         } 
  19.     } 

除了加载时设置 button 之外,我们还要监听通知,并在 name 被改变时更新 button 的 title。

SecondViewController 的代码类似 FirstViewController,不赘述。

对于 ThirdViewController,除了设置和通知外,还有一个 button 的 target-action 方法用于修改名字,也很简单:

  1. @IBAction func changeName(sender: UIButton) { 
  2.  
  3.     let alertController = UIAlertController(title: "Change name", message: nil, preferredStyle: .Alert) 
  4.  
  5.     alertController.addTextFieldWithConfigurationHandler { (textField) -> Void in 
  6.         textField.placeholder = self.nameButton.titleLabel?.text 
  7.     } 
  8.  
  9.     let action: UIAlertAction = UIAlertAction(title: "OK", style: .Default) { action -> Void in 
  10.         if let textField = alertController.textFields?.first as? UITextField { 
  11.             UserInfo.sharedInstance.name = textField.text // 更新名字 
  12.         } 
  13.     } 
  14.     alertController.addAction(action
  15.  
  16.     self.presentViewController(alertController, animated: true, completion: nil) 

似乎并不麻烦,看起来也算合理,那上面这样写有什么问题?我想答案是太重复。为了减少重复,我们来增加自己的知识,让脑神经稍微痛苦一点,好形成一些新的联结或破坏一些旧的联结。

我们可以传递闭包给 UserInfo,它将闭包存储起来,并在 name 被改变时调用这些闭包,这样闭包里的操作就会被执行了。自然,我们要在闭包里更新 UI。

这样,新的 UserInfo 如下:

  1. class UserInfo { 
  2.  
  3.     static let sharedInstance = UserInfo() 
  4.  
  5.     typealias NameListener = String -> Void 
  6.  
  7.     var nameListeners = [NameListener]() 
  8.  
  9.     class func bindNameListener(nameListener: NameListener) { 
  10.         self.sharedInstance.nameListeners.append(nameListener) 
  11.     } 
  12.  
  13.     class func bindAndFireNameListener(nameListener: NameListener) { 
  14.         bindNameListener(nameListener) 
  15.  
  16.         nameListener(self.sharedInstance.name
  17.     } 
  18.  
  19.     var name: String = "NIX" { 
  20.         didSet { 
  21.             nameListeners.map { $0(self.name) } 
  22.         } 
  23.     } 

我们删除了通知相关的代码,定义了 NameListener,增加了一个 nameListeners 用于保存监听者闭包,并实现两个类方法 bindNameListener 和 bindAndFireNameListener 来保存(并触发)监听者闭包。而在 name 的 didSet 里,我们只需要调用每个闭包即可,这里用了 map,也很直观。

那么 FirstViewController 的代码就简化为:

  1. class FirstViewController: UIViewController { 
  2.  
  3.     @IBOutlet weak var nameButton: UIButton! 
  4.  
  5.     override func viewDidLoad() { 
  6.         super.viewDidLoad() 
  7.  
  8.         title = "First" 
  9.  
  10.         UserInfo.bindAndFireNameListener { name in 
  11.             self.nameButton.setTitle(name, forState: .Normal) 
  12.         } 
  13.     } 

我们删除了通知相关的代码和 updateUI 方法,只需要将我们更新 UI 的闭包绑定到 UserInfo 即可。因为我们也需要初始设置 button,所以用了 bindAndFireNameListener。

SecondViewController 和 ThirdViewController 的修改类似 FirstViewController,不赘述。

这样一来,设置 UI 的操作和更新 UI 的操作就被很好地“融合”到一起了。代码比第一版的的逻辑性更强,VC 也更简单。

但是还有一个问题, UserInfo 里的 nameListeners 数组可能会越来越长,比如用户不断地 push/pop。虽然在有限的时间里,nameListeners 的数量不会变的非常大,程序的性能可以接受,但这毕竟是一种浪费(内存和 CPU 时间)。我们再来解决这个问题。

问题关键是我们的闭包并没有名字,我们无法将其找出并删除。例如对于 SecondViewController 来说,第一次进入它时,bindAndFireNameListener 执行了一次,如果 pop 再 push,它又执行了一次。那么,第一次被绑定的闭包其实没有任何用处了,因为第二次看到的 VC 是新生成的。如果我们能为闭包取名字,我们就能在第二次进入时用新的闭包替换旧的闭包,从而保证 nameListeners 的数量不会无限制的增长,也就不会浪费内存和 CPU 了。

为了限制 nameListeners 的无限制增长,我们可以将 nameListeners 改成 nameListenerSet,类型从 Array 改成 Set,这样绑定时就能保证其中“同一个地方添加的闭包”最多只有一个。但很不幸,我们无法将闭包 NameListener 放入 Set,因为闭包无法实现 Hashable 协议,而这正是使用 Set 所需要的。

似乎陷入困境了!

不要恐慌。虽然一个单纯的闭包无法实现 Hashable,但我们可以将其再封装一次,例如放入一个 struct 里,我们再让 struct 实现 Hashable 协议。前面刚提到过,闭包无法实现 Hashable,那么我们必然要在 struct 放入另外一个可以 Hashable 的属性来帮助我们的 struct 实现 Hashable。也就是:为闭包取一个名字。因此,我们新的 UserInfo 如下:

  1. func ==(lhs: UserInfo.NameListener, rhs: UserInfo.NameListener) -> Bool { 
  2.     return lhs.name == rhs.name 
  3.  
  4. class UserInfo { 
  5.  
  6.     static let sharedInstance = UserInfo() 
  7.  
  8.     struct NameListener: Hashable { 
  9.         let name: String 
  10.  
  11.         typealias Action = String -> Void 
  12.         let actionAction 
  13.  
  14.         var hashValue: Int { 
  15.             return name.hashValue 
  16.         } 
  17.     } 
  18.  
  19.     var nameListenerSet = Set<NameListener>() 
  20.  
  21.     class func bindNameListener(name: String, action: NameListener.Action) { 
  22.         let nameListener = NameListener(namenameactionaction
  23.  
  24.         self.sharedInstance.nameListenerSet.insert(nameListener) // TODO:需要处理同名替换 
  25.     } 
  26.  
  27.     class func bindAndFireNameListener(name: String, action: NameListener.Action) { 
  28.         bindNameListener(nameactionaction
  29.  
  30.         action(self.sharedInstance.name
  31.     } 
  32.  
  33.     var name: String = "NIX" { 
  34.         didSet { 
  35.             for nameListener in nameListenerSet { 
  36.                 nameListener.action(name
  37.             } 
  38.         } 
  39.     } 

我们设计了一个新的 struct:NameListener,它有一个 name 表明它是谁,原来的闭包就变成了 action,也很合理。为了满足 Hashable 协议,我们用 name.hashValue 来作为 struct 的 hashValue。另外,因为 Hashable 继承于 Equatable,我们也要实现一个 func ==。

另外,为了 API 更好使用,我们将 bindNameListener 与 bindAndFireNameListener 改造为接受一个 name 和一个 action 作为参数,在方法内部才“合成”一个 nameListener,这样 API 在使用时看起来会更合理,如下:

  1. UserInfo.bindAndFireNameListener("FirstViewController.nameButton") { name in 
  2.     self.nameButton.setTitle(name, forState: .Normal) 

我们只在闭包前面增加了一个闭包的“名字”而已。

最后,UserInfo 的 name 的 didSet 里要稍微修改,因为是 Set,没法 map 了,那就改成最传统的循环吧。

小结

我们面临一个“一处修改,多处更新”的问题,起初时我们用通知来实现,并无不可。之后我们想要更合理(或者更酷)一些,于是利用 Swift 的闭包特性实现了一个监听者模式。最后,我们使用包装的办法,解决了监听者可能会无限制增长的问题。

而这一切的目的,都是为了让代码更有逻辑性,并减少 VC 的代码量。

最后的最后,UserInfo 里可能会包含其他类型的属性,例如 var hairColor: UIColor,如果它也面临“一处修改,多处更新”的问题,那么我们也需要实现一个 HairColorListener 吗?

也许我们该利用 Swift 的泛型编写一个更加合理的 Listener,你说对吧?

非最终的效果请查看并运行 Demo 代码:[1]。如果你愿意的话,可以查看 git 的各个 commit 以得到整个过程。

(最终的)更好的泛型实现在分支 generic[2] 里,它的关键就是利用泛型实现一个 class Listenable 以对应任何类型的属性,它内部再实现监听系统即可。当然,我们也让监听者支持泛型(struct Listener)以便执行 action 时可以传递任意类型的参数。还有少许细节不同,例如 UserInfo 里直接使用 static 变量更方便,不需要用一个单独的单例再访问其属性。

参考资料

[1]运行 Demo 代码: https://github.com/nixzhu/PropertyListenerDemo

[2]generic: https://github.com/nixzhu/PropertyListenerDemo/tree/generic

本文转载自微信公众号「Swift社区」

责任编辑:姜华 来源: Swift社区
相关推荐

2022-02-10 19:15:18

React监听系统模式

2022-04-15 14:31:02

鸿蒙操作系统

2015-07-03 09:49:56

2024-03-14 11:06:37

JavaScript引擎探索

2024-01-05 15:32:47

鸿蒙SNTP智慧时钟

2022-04-15 11:46:09

轻量系统解耦鸿蒙操作系统

2022-02-09 19:45:41

MQTTOpenHarmon鸿蒙

2021-09-13 08:20:13

Loki日志系统

2022-02-10 15:07:10

云平台OpenHarmon系统开发

2023-04-03 15:39:31

2022-01-21 21:22:24

OpenHarmon操作系统鸿蒙

2023-04-24 15:11:51

系统开发鸿蒙

2019-11-26 09:42:36

代码开发API

2022-02-08 15:21:59

Hi3861开发鸿蒙

2023-03-24 14:39:17

鸿蒙系统开发

2022-02-09 19:31:41

Hi3861OpenHarmon鸿蒙

2024-01-08 08:23:08

OpenCV机器学习计算机视觉

2022-01-24 18:43:20

OpenHarmon操作系统鸿蒙

2023-10-31 18:32:26

WebRTC存储

2015-08-03 11:42:27

Swift汉堡式过度动画
点赞
收藏

51CTO技术栈公众号