如果您更愿意阅读中文,可以点击这里。
键值观察模式对于 Cocoa 和 Cocoa Touch 编程非常重要。您添加一个观察者,观察值的变化,当您完成时移除它。
然而,如果您不正确使用它,可能会导致崩溃。
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x100102560 of class Foo was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x100104990> (
<NSKeyValueObservance 0x100104770: Observer: 0x100102f30, Key path: name, Options: <New: YES, Old: YES, Prior: NO> Context: 0x0, Property: 0x100100340>
)'
*** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <Bar 0x100202de0> for the key path "name" from <Foo 0x100202ac0> because it is not registered as an observer.'
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<Bar: 0x1002000b0>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: name
Observed object: <Foo: 0x100200080>
Change: { kind = 1; new = 1; old = 0; }
Context: 0x0'
尽管默认 API 的可用性和安全性很高,但 KVO 仍然很重要。作为开发者,您可能只想使用一些简单的 API 实现目标。这就是 YJSafeKVO 出现的原因。它有 3 种模式
如果 A 观察 B 的名称变化,则调用
[A observeTarget:B keyPath:@"name" updates:^(id A, id B, id _Nullable newValue) {
// Update A based on newValue...
}];
这种阅读自然语义,或者您也可以简单地使用 PACK
宏。(推荐)
[A observe:PACK(B, name) updates:^(id A, id B, id _Nullable newValue) {
// Update A based on newValue...
}];
A 被视为“观察者”或“订阅者”。B 被视为被观察的“目标”。
bindTo: / boundTo
它可以用于将目标绑定到订阅者。当值发生变化时,它会自动将目标的 key path 从订阅者的 key path 设置。
[PACK(foo, name) bindTo:PACK(bar, name)];
调用 -bindTo:
后,每当 foo 的名称改变时,它会将新值发送到 bar 的名称。它可以被认为是 [source bindTo:subscriber]
。
它还支持双向数据流。
[PACK(foo, name) boundTo:PACK(bar, name)];
调用 -boundTo:
后,每当 bar 的名称改变时,新值将发送到 foo 的名称。它可以被认为是 [subscriber boundTo:source]
。
请注意:订阅方向必须是单向的(例如,如果调用 [A bindTo:B]
,则永远不要调用 [A boundTo:B]
或 [B bindTo:A]
来指定相同的 keyPath),否则会导致无限循环。
now
在 [PACK(foo, name) bindTo:PACK(bar, name)]
之后,只有当未来的 foo 的名称改变时,bar 的名称才会获得新值。如果您需要立即将 foo 的当前名称设置到 bar 的名称上,只需在最后调用 -now
即可。
[[PACK(foo, name) bindTo:PACK(bar, name)] now]
filter
您可以通过返回 YES 或 NO 来决定是否接收新值。
[[[PACK(foo, name) bindTo:PACK(bar, name)]
filter:BOOL^(NSString * _Nullable newName){
return newName.length > 5;
}]
now];
convert
您可以将数据流转换并返回适合订阅者的新值。
[[[PACK(foo, name) bindTo:PACK(bar, name)]
convert:id _Nullable^(NSString * _Nullable newName){
return [newName uppercaseString];
}]
now];
通过使用 convert:
,您还可以绑定两种类型不同的 keyPath。
[[[PACK(foo, name) bindTo:PACK(bar, hidden)]
convert:id _Nullable^(NSString * _Nullable newName){
return newName.length ? @NO : @YES;
}]
now];
applied
applied:
将在应用新值时执行块。
[[[PACK(foo, name) bindTo:PACK(bar, name)]
applied:^{
NSLog(@"bar just get a new name.");
}]
now];
get together
您可以将它们嵌套在一起以形成一个复杂的绑定。
[[[[[[[[PACK(bar, name) boundTo:PACK(foo, name)]
applied:^{
NSLog(@"^^^^^^^^^^");
}] filter:^BOOL(NSString *name) {
return name.length > 5;
}] convert:^id _Nullable(NSString *name) {
return [name uppercaseString];
}] filter:^BOOL(NSString *name) {
return [name hasPrefix:@"ABC"];
}] convert:^id _Nullable(NSString *name) {
return [name lowercaseString];
}] applied:^{
NSLog(@"@@@@@@@@@@");
}] now];
combineLatest:reduce
但是,如果最终的输出结果由多个变化因素决定,则可以使用 combineLatest:reduce:
,这将从多个来源提取变化并将它们归约为一个单一的值。
[PACK(clown, name) combineLatest:@[ PACK(foo, name), PACK(bar, name) ]
reduce:^id(NSString *fooName, NSString *barName) {
return fooName && barName ? [fooName stringByAppendingString:barName] : nil;
}];
订阅模式也支持 -cutOff:
,用于切断观察者的键路径与目标键路径之间的绑定关系。
直接发布值变化。
[PACK(foo, name) post:^(NSString * _Nullable name) {
if (name) NSLog(@"foo has changed a new name: %@.", name);
}];
如果您需要立即获取foo的当前名称,请确保最后调用 -now
。
当foo设置新值时,foo被视为发送者,它会将变化发送到块中。也可以通过调用 [PACK(foo, name) stop]
停止发布值变化。
我应该担心在对象被释放之前移除观察者,以防止崩溃吗?
不用!不需要额外的工作。选择您喜欢的模式,YJSafeKVO
会处理剩余的部分。它只是工作。
以下是一个表示 YJSafeKVO
的图树。
Target/Source
|
Subscriber Manager
|
|--------------------------------------|
Subscriber1 (weak) Subscriber2 (weak) ...
| |
Porter Manager Porter Manager
|----------|-----------| |-----|-----
Porter1 Porter2 Porter3 ... Porter4 ...
| | | |
(block) (block) (block) (block)
目标或发送者
目标或发送者是值变化的来源,它始终位于KVO链的顶部。
订阅者
调用 "-observeTarget:" 或 "-observe:" 的对象应被视为观察者,因为它实际上是真正想要观察和处理值变化的对象。为了避免概念混淆,我使用了“订阅者”这个词。
Porter
Porter 在 KVO 过程中生成,其任务是向想要处理值变化的对象传递值变化。Porter 通过一个块传递变化。
Porter 管理器
管理 Porter 的对象。通常由订阅者或发送者拥有。
订阅者管理器
管理订阅者的对象。通常由目标拥有。与 Porter 管理器不同,订阅者管理器仅对订阅者使用弱引用。
如果目标或发送者被释放,图树将消失。如果在进行观察前某个订阅者被释放,则图树的该分支将消失。
如果要在任何一个对象被释放之前停止观察,您可以手动调用 -unobserve.
、-cutOff:
或 -stop
来停止观察。
使用块很容易造成保留循环。
[self observe:PACK(self.foo, name) updates:^(id receiver, id target, id _Nullable newName) {
NSLog(@"%@", self); // Retain cycle
}];
要解决这个问题:将 receiver
变量更改为 self
。无需额外的 __weak
。
[self observe:PACK(self.foo, name) updates:^(id self, id foo, id _Nullable newName) {
NSLog(@"%@", self); // No retain cycle because using self as an local variable.
}];
例如,如果您的观察属性在一个线程上设置为新值,并且您期望在主线程上执行的回调块中用新值更新 UI。您可以使用扩展 API 来指定 NSOperationQueue
参数。
[self observe:PACK(self.foo, name)
options:NSKeyValueObservingOptionNew
queue:[NSOperationQueue mainQueue]
changes:^(id receiver, id target, NSDictionary *change) {
// Callback on main thread
}
如果您熟悉-addObserverForName:object:queue:usingBlock:
在NSNotificationCenter
的使用,那么使用此API没有障碍。
“观察”和“发布”之间没有太大区别,因为它们共享相同的图树。在YJSafeKVO
中,“观察”被视为“全能模式”,因为无论其他模式能够做什么,“观察”都可以做到。以下是一个视图控制器观察网络连接状态并在状态改变时做出一系列改变的示例。
[self observe:PACK(reachability, networkReachabilityStatus) updates:^(MyViewController *self, AFNetworkReachabilityManager *reachability, NSValue *newValue) {
AFNetworkReachabilityStatus status = [newValue integerValue];
BOOL connected = (status == AFNetworkReachabilityStatusReachableViaWWAN || status == AFNetworkReachabilityStatusReachableViaWiFi);
self.label.text = connected ? @"Conntected" : @"Disconnected";
self.button.enable = connected;
self.view.backgroundColor = connected ? UIColor.whiteColor : UIColor.grayColor;
...
}];
使用“订阅”的原因是希望一种状态完全由其他状态绑定和决定,因此它会自动改变值,而不是由开发者手动设置。
键值观察是 Cocoa 编程的模式。任何作为 NSObject 子类的对象都会免费获得它。这也意味着此功能不适用于 Swift 的 struct,以及其根类不是 NSObject 的类对象。
观察
foo.observe(PACK(bar, "name")) { (_, _, newValue) in
print("\(newValue)")
}
订阅
PACK(foo, "name").boundTo(PACK(bar, "name"))
构建复杂的绑定
PACK(foo, "name").boundTo(PACK(bar, "name"))
.taken { (newValue) -> Bool in
if let name = newValue as? String {
return name.characters.count > 3
}
return false
}
.convert { (newValue) -> AnyObject in
let name = newValue as! String
return name.uppercaseString
}
.applied {
print("value updated.")
}
.now()
bar.name = "Bar" // foo.name is not receiving "Bar"
bar.name = "Barrrr" // foo.name is "BARRRR"
YJSafeKVO 需要至少 Xcode 7.3 才能使用 NS_SWIFT_NAME
,因此它可以公开 Swift API,并感觉更 Swift。
YJSafeKVO 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile。
pod "YJSafeKVO"
转到终端并运行 pod install
,然后 #import <YJSafeKVO/YJSafeKVO.h>
到项目中的 ProjectName-Prefix.pch
文件。
huang-kun, [email protected]
YJSafeKVO 适用于 MIT 许可协议。有关更多信息,请参阅 LICENSE 文件。