- 原文作者 : Kevin McNeish
对于自定义控件来说,在不破坏原有的消息机制的前提下,如何响应事件通知?在本文中,我将演示一个通知代理类,通过一个简单的例子,我们用该类向已有的iOS UI控件中增加了自己的新功能:为Text View控件增加placeholder文本。
问题:缺失的Placeholder
Placeholder文本是用于在某些控件中提示用户输入指定类型的数据的良好方式。UIKit的UITextField控件的placeholder属性就是用来干这个的。例如,下图中,在Twitter的选项设置中,User Name字段就使用了placeholder文本。通过这个placeholder文本,用户可以知道应当在这里输入一个包含了@符号的Twitter用户名。而在Password字段的placeholder文本则标明该字段是必填的。
图 1 - 设置程序中的Placeholder 文本
尽管Text Filed有placeholder属性,但它的兄弟控件Text View却没有这个属性。
问题的解决
我准备这样解决这个问题:
- 创建一个UITextView的扩展,添加一个placeholder的计算属性
- 在扩展中,当TextView中没有输入任何文字的情况下,显示一个Label,如果输入有文字,则隐藏这个Label。
就像我在前面的文章中提到的,我更喜欢使用扩展,而不是子类。因为扩展允许我在App中使用“盒子之外”的UIKit控件。我需要做的仅仅是在项目中加入某个类的扩展文件,然后这个类会自动获得新的特性。
方式1:UITextViewDelegate
当用户在TextView中输入文本时,iOS有几种通知App的方式。其中一种就是通过UITextViewDelegate协议。在这个扩展中有一个对TextView的引用保存在它自己的delegate属性中。当用户输入或删除字符时,TextView的协议方法会被调用。我们可以在协议方法中加入隐藏和显示placeholder标签的代码。
这个解决方法的缺点是,Text View太过于依赖delegate属性,因为在一个UIControl中你只能注册一个委托对象。如果你需要向TextView中加入更多的委托对象,你就会比较麻烦。
方式2:NSNotificationCenter
NSNotificationCenter通过UITextViewTextDidChangeNotification通知来告诉你用户在TextView中输入或删除了某些字符。这是一种更好的选择,因为它不再依赖于delegate属性。缺点是需要向通知中心进行注销。一般,我们在对象的deinit方法中向NSNotificationCenter注销该对象。但是在Swift中,我们无法在扩展中使用deinit方法。那我们怎样才能突破这个限制呢?
创建一个通知代理
我们通过创建一个轻量级的代理对象来突破这个限制,这个代理对象代替TextView来向通知中心进行注册。
我已经在我的项目中创建了一个UITextView扩展以及一个代理类,你可以从这里下载。
双击.xcodeproj文件,用Xcode打开这个项目。在项目导航窗口中选中mmTextViewExtensions.swift文件,你可以找到这个通知代理类(如你所见,其中也包含了上一篇文章中的mmDyamicTypeExtensions类)。
下面是位于文件头部的通知代理类:
你可以看到,它是UIView子类。这样我们就可以将它当成subview添加到TextView中。addObserverForName:usingBLock:方法通知中心的方法拥有一样的签名。这个方法中只有一行代码,就是将TextView注册到通知中心并将参数传递过去,包括TextView的闭包,这样当通知发生时该闭包被执行。通知中心会返回一个NSObjectProtocol对象,稍后我们会用来进行通知的注销操作。
deinit方法也只有一行代码,仅仅是向通知中心进行对象的注销。
通知代理类是可重用的。你可以用于任何类型的通知以及你想接收通知的任何类型的对象。
然后是UITextView的扩展,在扩展中有一个placeholderLabel属性,如你所想,它引用了一个placeholder标签对象。
此外还有一个placeholder的字符串属性。在后面你将看到,所有的奇迹将在运行时发生,当placeholder属性被设置,这个计算属性的代码就会执行。
现在打开Main.storyboard文件。如图2所示,故事板中只有一个Scene,在这个Scene的顶部,有一个Text View。
图 2 - 主 scene 中包含了一个 text view
当你选择Text View,然后打开属性面板,你将看见一个Placeholder属性,它是扩展中增加的属性,这个属性的默认值是“Enter your text here!”。在设计时不会显示这段文本(要在设计时可见,我们需要花费大量的工作,因此这不是本文的主题),但在运行时它会显示。
The Notification Proxy at Run Time
运行时的通知代理
图3中的UML序列图显示了所有对象之间发生的消息传递的顺序。
图 3 - 运行时对象间消息传递的顺序
每一步的解释如下:
- 当运行时,UITextView的placeholder文本被改变时,该扩展的placeholder计算属性中的代码被触发。
- 扩展的addPlaceholderLabel方法被调用。
- addPlaceholderLabel方法创建一个placeholder标签并设置标签文本。
- placeholder标签被添加进UITextView。
- 一个通知代理对象被创建。
- 扩展调用通知代理的addObserverForName:withBlock:方法,并指定对UITextViewTextDidChangeNotification通知感兴趣。此外闭包代码会在通知发生时执行。
- 通知代理对象将UITextView注册到通知中心,同时将通知时间传递给TextView,当通知发生时,UITextView的闭包代码将被执行。
- UITextView以subview的方式添加通知代理对象。
- 当UITextViewTextDidChangeNotification通知发生,通知中心调用TextView扩展的闭包。
- UITextView扩展将placeholder标签的hidden属性设置为true和false,取决于TextView中的文本是否有内容。
11.当TextView在运行时解构时,placeholder标签和通知代理对象都被解构。 - 当通知代理对象解构时,将TextView对象从通知中心中注销。
Let’s Take it for a Test Run!
运行测试
- In Xcode’s Scheme control, select one of the simulators such as iPhone 6.
在Xcode的Scheme下拉列表中,选择一个模拟器,例如iPhone6。 - Click Xcode’s Run button.
点击Xcode的Run按钮。 - When the app appears in the Simulator, you can see the placeholder text (Figure 4).
当App在模拟器中运行后,我们可以看到placeholder文本(图4)。
图 4 - 运行时的 placeholder 文本
4.Now type some code in the text view. As soon as you begin typing, the placeholder label disappears (Figure 5).
现在在TextView中输入任意字符。此时,placeholder将消失(图5)。
图 5 -placeholder 文字消失了
如果将输入的字符删除干净,placeholder文字将重新出现。
结束语
要解决编程问题有许多方法。最好的方法是先看一下摆在你目前的选择,然后逐个分析其中的利弊。我希望你能看到本文中序列图的好处。它能让你将对象之间在运行时的交互画出来。我在编写代码时也使用了这个方法。你会发现它们能帮助你找出潜在的问题,更能帮你找出编程问题中的新的、更高效的解决方案。