当前位置: 首页 > 科技观察

用Swift实现一个轻量级的属性监控系统

时间:2023-03-18 20:24:54 科技观察

前言本文的主要目的是解决客户端开发中“一次修改模型,多次更新UI”的问题。当然,我们需要知道解决方案的细节和思路,看它能达到的效果。我们将使用函数式编程的思想,以及伟大的“泛型”。相信我,我们不会为了使用新技术而使用新技术。如果有更好的方法来解决问题,为什么不更换旧方法呢?Text如果你写的app有用户系统,即用户需要管理自己的信息,比如改名字,头发颜色等等。单拿名字,除了修改界面,还可能在系统的其他界面使用,这就涉及到更新名字后更新其他界面的问题。你的第一直觉是什么?可能使用通知,又名NSNotification。这是一个很好的做法,虽然逻辑松散,写起来有点繁琐。比如定义一个通知名称,发送一个通知,每个接口在处理之前都会监听通知等等。例如,对于以下三个接口,它们都有显示名称。通过push,用户可以在第三个界面修改name,这就需要更新三个界面的name,否则pop返回时用户会觉得奇怪。UI假设我们的名字放在一个叫UserInfo的类中(单例用于访问和修改),如下:"{didSet{NSNotificationCenter.defaultCenter().postNotificationName(Notification.NameChanged,object:name)}}}同时我们定义了一个通知。此通知是在名称更改后发出的,名称已传递出去。三个界面分别是FirstViewController、SecondViewController、ThirdViewController,中间各有一个按钮。其中前两个负责push,最后一个点击后可以重命名。所以,对于FirstViewController:(self,selector:"updateUI:",name:UserInfo.Notification.NameChanged,object:nil)}funcupdateUI(notification:NSNotification){ifletname=notification.objectas?String{nameButton.setTitle(name,forState:.Normal)}}}除了在按钮加载时设置按钮外,我们还需要监听通知并在名称更改时更新按钮的标题。SecondViewController的代码和FirstViewController类似,这里不再赘述。对于ThirdViewController,除了settings和notifications,还有buttontarget-actionmethod修改name,也很简单:@IBActionfuncchangeName(sender:UIButton){letalertController=UIAlertController(title:"Changename",message:无,preferredStyle:.Alert)alertController.addTextFieldWithConfigurationHandler{(textField)->VoidintextField.placeholder=self.nameButton.titleLabel?.text}letaction:UIAlertAction=UIAlertAction(title:"OK",style:.Default){action->VoidiniflettextField=alertController.textFields?.firsstas?UITextField{UserInfo.sharedInstance.name=textField.text//更新名称}}alertController.addAction(action)self.presentViewController(alertController,animated:true,completion:nil)}不好像很麻烦,看看好像也有道理,那上面的有什么问题吗?我认为答案太重复了。为了减少重复,我们增加知识,给大脑一点痛苦,以形成一些新的联系或破坏一些旧的联系。我们可以将闭包传递给UserInfo,它存储闭包并在更改名称时调用它们,这样闭包中的操作就会被执行。自然地,我们想要更新闭包内的UI。这样,新的UserInfo如下:nameListener:NameListener){bindNameListener(nameListener)nameListener(self.sharedInstance.name)}varname:String="NIX"{didSet{nameListeners.map{$0(self.name)}}}}我们删除了通知相关的代码并定义了NameListener,添加了一个nameListeners来保存监听器闭包,并实现了两个类方法bindNameListener和bindAndFireNameListener来保存(并触发)监听器闭包。在name的didSet中,我们只需要调用每个闭包即可,这里使用了map,也很直观。那么FirstViewController的代码就简化为:我们把通知和updateUI方法相关的代码都删掉了,只需要将我们更新UI的闭包绑定到UserInfo上即可。由于我们还需要对按钮进行初始设置,因此使用了bindAndFireNameListener。SecondViewController和ThirdViewController的修改与FirstViewController类似,不再赘述。这样,设置UI的操作和更新UI的操作就很好地“融合”在一起了。代码比第一个版本更有逻辑性,VC也更简单。但是还有一个问题,UserInfo中的nameListeners数组可能会越来越长,比如用户不停的push/popping。虽然在有限的时间内,nameListeners的数量不会变得很大,程序的性能还是可以接受的,但这毕竟是一种浪费(内存和CPU时间)。让我们再次解决这个问题。问题的症结在于我们的闭包没有名字,所以我们找不到它并删除它。比如对于SecondViewController,第一次进入它的时候会执行一次bindAndFireNameListener,如果再push一次pop就会重新执行。那么,第一次绑定的闭包其实是没有用的,因为第二次看到的VC是新生成的。如果我们能给闭包命名,我们就可以在第二个入口用新闭包替换旧闭包,从而保证nameListeners的数量不会无限增长,内存和CPU也不会浪费。为了限制nameListeners的无限增长,我们可以把nameListeners改成nameListenerSet,类型从Array改成Set,这样在绑定的时候就可以保证最多只有一个“同一个地方添加的闭包”。但不幸的是,我们不能将闭包NameListener放入Set中,因为闭包无法实现Hashable协议,而Hashable协议是使用Set所必需的。好像卡住了!不要恐慌。虽然简单的闭包不能实现Hashable,但是我们可以再封装一下,比如放在一个struct中,让struct实现Hashable协议。前面说了闭包不能实现Hashable,所以我们必须在struct中再放一个Hashable属性来帮助我们的struct实现Hashable。那就是:给闭包一个名字。所以我们新的UserInfo看起来像这样:StringtypealiasAction=String->Voidletaction:ActionvarhashValue:Int{returnname.hashValue}}varnameListenerSet=Set()classfuncbindNameListener(name:String,action:NameListener.Action){letnameListener=NameListener(name:name,action:action)self.sharedInstance.nameListenerSet.insert(nameListener)//TODO:需要处理同名的替换}classfuncbindAndFireNameListener(name:String,action:NameListener.Action){bindNameListener(name,action:action)action(self.sharedInstance.name)}varname:String="NIX"{didSet{fornameListenerinnameListenerSet{nameListener.action(name)}}}}我们设计了一个新的结构体:NameListener,它有一个名字来表明它是谁,原来的闭包变成了action,也很合理。为了满足Hashable协议,我们使用name.hashValue作为struct的hashValue。另外,由于Hashable继承自Equatable,我们还需要实现一个func==。另外,为了更好的使用API??,我们对bindNameListener和bindAndFireNameListener进行改造,接受一个name和一个action作为参数,只在方法内部“合成”一个nameListener,这样API在使用的时候看起来会更合理,如下:UserInfo.bindAndFireNameListener("FirstViewController.nameButton"){nameinself.nameButton.setTitle(name,forState:.Normal)}我们只在闭包前面加了一个闭包的“名字”。最后UserInfo的名字的didSet需要稍微修改一下,因为是Set,不能映射,所以改成最传统的循环。总结我们面临着“一处修改,多处更新”的问题。一开始我们是用通知来实现的,这也不是不可以。然后我们想做一些更合理(或更酷)的东西,所以我们使用Swift的闭包特性实现了一个监听器模式。最后,我们使用封装的方式来解决监听器可能无限增长的问题。这一切的目的都是为了让代码更符合逻辑,减少VC代码量。最后,UserInfo可能包含其他类型的属性,比如varhairColor:UIColor,如果它也面临“一次修改,多次更新”的问题,那我们还需要实现一个HairColorListener吗?也许我们应该用Swift为泛型写一个更合理的Listener吧?非最终效果请查看并运行Demo代码:[1]。如果需要,您可以在git中查看单个提交以获取整个过程。泛型的(最终)更好的实现在分支generic[2]中。它的关键是使用泛型实现一个类Listenable来对应任意类型的属性,然后在内部实现监听系统。当然,我们也让监听器支持泛型(structListener),这样在执行动作时可以传递任意类型的参数。还有一些细节是不同的。比如在UserInfo中直接使用静态变量会更方便,不需要使用单独的单例来访问它的属性。参考文献[1]运行Demo代码:https://github.com/nixzhu/PropertyListenerDemo[2]generic:https://github.com/nixzhu/PropertyListenerDemo/tree/generic本文转载自微信公众号》Swift社区”