首先说这不是一个新技术,它很老,老到几乎没人提起它。
这是苹果 MVC 模式下的产物,最早在没有 iPhone 的时候就已经诞生了,它是用来配合 Xcode 图形化设置界面用的——比如 NIB,当然,现在已经变成 XIB了,哦,还有 Storyboard。
现在如果说起要 bind 一个 Storyboard 中的对象到代码中,你可能查到的都是这样的:
在左侧栏点击鼠标右键打开小窗口,然后【拉线】连接代码中的声明与实际的 UIButton……
这样在程序加载这个 Storyboard 时,你图形化创建的 Button 就自动连接到了代码的声明中,比如:
1 2 |
@IBOutlet var myBox:NSBox! @IBOutlet var myButton:UIButton! |
没错,通常来说我们是这么用的,但如果我这是一个“设置”界面,里边有大量的简单控件,但要根据用户当前设置状态来改变和显示(这是个很常见的情景,毕竟哪个 App 没个偏好设置呢?),大量的 @IBOutlet 和 @IBAction 可能并不是一个好的选择,因为除了这些,你还需要在初始化代码中增加大量的配置读取代码,状态判断,然后再去更改对应控件的状态,如果选项多的话,那基本就是灾难了。
Cocoa Binding 救你命
打开 Xcode ,加载一个 Xib,选中一个控件,在右侧栏你会看到常见的属性设置界面:
你可能经常使用前边几个功能:
但,这个功能你肯定没用过:
如果你去网上查找,那几乎得不到什么有用的介绍——因为它很古老了。得益于 Objc 的 MVC 设计风格,实际上每一个控件都可以直接监听你对象中的变量,并自动根据它的变化来改变自身显示的,你完全不需要去初始化那个对象,也不用根据用户当前的设置来设定这个按钮是可用还是不可用,一切其实都可以是自动的,你需要提供的——不过是一个变量而已。
不过问题来了,现在已经都在用 Swift 了,怎么才能让它绑定到 Swift 代码中的变量呢?如果你声明一个变量,并用这个界面来进行绑定,那么就会遇到运行时错误:
1 |
this class is not key value coding-compliant for the key xxx |
这是由于 Swift 默认不向 Objc 暴露变量名称导致的,如果任何 #selector() 调用方法一样,我们在对应的变量前加上 @objc dynamic ,比如这样 @objc dynamic var mySetting = false ,这样,就可以绑定成功了。
注意,这里我们除了常见的 @objc 外,还追加了 dynamic 修饰,如果只使用前者,则可能在第一次调用时失败, Swift 依旧会将你声明的变量优化成静态,这样 Cocoa Binding 就不能动态监听我们的绑定了。
(取决于你的编译优化,为了保证无论如何绑定都能生效,还是应该加上 dynamic 修饰)
在绑定界面,我们选中一个 Button,比如我这个按钮控件是有选中状态的那种 ☑️,所以它有开和关两种状态,于是我就可以把它的 value 进行绑定,这里要记得先设置好 Xib 的 File's Owner 一般这个默认是空的,设置好后,我们就可以选择绑定到 File's Owner 了:
这里要使用 KeyPath 进行设置,实际上就是你变量的名字即可。现在这个按钮的状态就会自动根据变量的值进行改变了。完全不需要任何初始化设置。
额外的,除了按钮对象,你还可以绑定 Label,显然,它的 value 就是字符串对象了。值得一提的是,如果你绑定一个数组控件……比如 NSPopUpButton 这种,它实际上是有多个数据,那么你也可以在这个设置界面的下方找到 selectedIndex 这样的状态进行绑定,值类型就是我们熟悉的 Int ,如果你直接绑定 value,那值其实是一个比较复杂的 NSArrayController。
当然,如你所见,你还可以方便地绑定一个空间的 Enabled, isHidden 等等状态,但凡是能设置的状态,几乎都能和变量进行绑定,根据情况自动发生改变——再也不用繁琐的代码来操作界面了!
进阶:ValueTransformer
设置绑定的时候,会有一个默认留空的选项, ValueTransformer ,它可以将你绑定的值进行一定的处理,比如默认的,将布尔值进行反转,判断空为 true 或者 false ,除此之外,我们还可以自定义如何对值进行处理。
比如我的设置选项有 0 1 2 三个状态,但按钮选中与否是两个,我需要将它映射到 0 和 2 上,默认的 Transformer 就无能为力了。
首先,我们要继承一个 ValueTransformer ,这样就能自定义处理流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class ValueTransformer2:ValueTransformer { override func transformedValue(_ value: Any?) -> Any? { guard let a = value as? Int else {return false} return a == 2 } class override func allowsReverseTransformation() -> Bool { return true } override func reverseTransformedValue(_ value: Any?) -> Any? { guard let a = value as? Bool else {return false} return a ? 2 : 0 } } |
其中 transformedValue(_ value: Any?)-> Any? 返回的是控件需要的值,比如我的按钮,我就设置 return a == 2 这样当且仅当值为 2 时按钮状态为启用。
allowsReverseTransformation() -> Bool 规定了这个 ValueTransformer 是否能逆向转换,默认是 false ,这里我需要逆向,因为按钮选定时应该给变量设置为 2 而不是 1 ,所以我要使用 reverseTransformedValue(_ value: Any?) -> Any? 来将按钮控件传入的值转换成变量需要的值。
写好后我们需要将这个自定义的 ValueTransformer 进行注册,否则 Swift 是找不到这个对象的:
1 |
ValueTransformer.setValueTransformer(ValueTransformer2(), forName: NSValueTransformerName("ValueTransformer2")) |
我们在任何初始化界面之前的地方注册即可,注意我直接给它起名为 "ValueTransformer2" ,然后就可以在 Xcode 的 Binding 界面将 ValueTransformer 写成这个 ValueTransformer2 了,这里我故意将名称写的和类名称一致,你也可以写成其他不对应的名字。
额外地,如果你用了自己的配置管理实例,那么也可以将绑定的变量写为计算属性,这是完全没有问题的,不过值得一提的是,绑定变量的控件,是依靠监控对应变量的 didSet 来获取通知的,这是唯一触发界面更新的方式,所以,如果你使用了计算属性,那么更新了后台存储后,记得也要触发一下计算属性的 didSet ,比如
1 2 3 4 5 6 7 |
@objc var myValue:Int { get {your code...} set {do nothing} } myValue = 0 |
本文由 落格博客 原创撰写:落格博客 » Cocoa Binding 实用教程
转载请保留出处和原文链接:https://www.logcg.com/archives/3456.html