Key-Value Observing

http://nshipster.com/key-value-observing/

Ask anyone who's been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa. It's awkward, verbose, and confusing. And worst of all, its terrible API belies one of the most compelling features of the framework.

When dealing with complicated, stateful systems, dutiful book-keeping is essential for maintaining sanity. Lest the left hand not know what the right hand doeth, objects need some way to publish and subscribe to state changes over time.

In Objective-C and Cocoa, there are a number of ways that these events are communicated, each with varying degrees of formality and coupling:

  • NSNotification & NSNotificationCenter provide a centralized hub through which any part of an application may notify and be notified of changes from any other part of the application. The only requirement is to know what to look for, specifically in the name of the notification. For example, UIApplicationDidReceiveMemoryWarningNotificationsignals a low memory environment in an application.
  • Key-Value Observing allows for ad-hoc, evented introspection between specific object instances by listening for changes on a particular key path. For example, a UIProgressViewmight observe the numberOfBytesRead of a network request to derive and update its ownprogress property.
  • Delegates are a popular pattern for signaling events over a fixed set of methods to a designated handler. For example, UIScrollView sends scrollViewDidScroll: to its delegate each time its scroll offset changes.
  • Callbacks of various sorts, whether block properties like NSOperation -completionBlock, which trigger after isFinished == YES, or C function pointers passed as hooks into functions like SCNetworkReachabilitySetCallback(3).

Of all of these methods, Key-Value Observing is arguably the least well-understood. So this week, NSHipster will endeavor to provide some much-needed clarification and notion of best practices to this situation. To the casual observer, this may seem an exercise in futility, but subscribers to this publication know better.


<NSKeyValueObserving>, or KVO, is an informal protocol that defines a common mechanism for observing and notifying state changes between objects. As an informal protocol, you won't see classes bragging about their conformance to it (it's just implicitly assumed for all subclasses ofNSObject).

The main value proposition of KVO is rather compelling: any object can subscribe to be notified about state changes in any other object. Most of this is built-in, automatic, and transparent.

For context, similar manifestations of this observer pattern are the secret sauce of most modern Javascript frameworks, such as Backbone.js and Ember.js.

Subscribing

Objects can have observers added for a particular key path, which, as described in the KVC operators article, are dot-separated keys that specify a sequence of properties. Most of the time with KVO, these are just the top-level properties on the object.

The method used to add an observer is –addObserver:forKeyPath:options:context::

- (void)addObserver:(NSObject *)observer 
         forKeyPath:(NSString *)keyPath 
            options:(NSKeyValueObservingOptions)options 
            context:(void *)context
  • observer: The object to register for KVO notifications. The observer must implement the key-value observing method observeValueForKeyPath:ofObject:change:context:.
  • keyPath: The key path, relative to the receiver, of the property to observe. This value must not be nil.
  • options: A combination of the NSKeyValueObservingOptions values that specifies what is included in observation notifications. For possible values, see "NSKeyValueObservingOptions".
  • context: Arbitrary data that is passed to observer inobserveValueForKeyPath:ofObject:change:context:.

Yuck. What makes this API so unsightly is the fact that those last two parameters are almost always 0and NULL, respectively.

options refers to a bitmask of NSKeyValueObservingOptions. Pay particular attention toNSKeyValueObservingOptionNew & NSKeyValueObservingOptionOld as those are the options you'll most likely use, if any. Feel free to skim overNSKeyValueObservingOptionInitial & NSKeyValueObservingOptionPrior:

NSKeyValueObservingOptions

  • NSKeyValueObservingOptionNew: Indicates that the change dictionary should provide the new attribute value, if applicable.
  • NSKeyValueObservingOptionOld: Indicates that the change dictionary should contain the old attribute value, if applicable.
  • NSKeyValueObservingOptionInitial: If specified, a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain anNSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer'sobserveValueForKeyPath:ofObject:change:context: method. When this option is used with addObserver:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
  • NSKeyValueObservingOptionPrior: Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is@YES, but never contains an NSKeyValueChangeNewKey entry. When this option is specified the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified. You can use this option when the observer's own key-value observing-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving anobserveValueForKeyPath:ofObject:change:context: message after the change.)

tl;dr: These options allow an object to get the values before and after the change. In practice, this is usually not necessary, since the new value is generally available from the current value of the property.

As for context, this parameter is a value that can be used later to differentiate between observations of different objects with the same key path. It's a niche use case, which will be discussed a little later.

Responding

Another aspect of KVO that lends to its ugliness is the fact that there is no way to specify custom selectors to handle observations, as one might be used to from the Target-Action pattern used by controls.

Instead, all changes for observers are funneled through a single method—-observeValueForKeyPath:ofObject:change:context::

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context

Those parameters are the same as what were specified in –addObserver:forKeyPath:options:context:, with the exception of change, which are populated from whichever NSKeyValueObservingOptions options were used.

A typical implementation of this method looks something like this:

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context
{
  if ([keyPath isEqualToString:@"state"]) {
    // ...
  }
}

Depending on how many kinds of objects are being observed by a single class, this method may also introduce -isKindOfObject:-respondsToSelector:, or an equality check to contextin order to definitively identify the kind of event being passed.

Better Key Paths

Passing strings as key paths is strictly worse than using properties directly, as any typo or misspelling won't be caught by the compiler, and will cause things to not work.

A clever workaround to this is to use NSStringFromSelector and a @selector literal value:

NSStringFromSelector(@selector(isFinished))

Since @selector looks through all available selectors in the target, this won't prevent all mistakes, but it will catch most of them—including breaking changes made by Xcode automatic refactoring.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([object isKindOfClass:[NSOperation class]]) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {

        }
    } else if (...) {
        // ...
    }
}

Correct Context Declarations

What makes a good context value? Here's a suggestion:

static void * XXContext = &XXContext;

It's that simple: a static value that stores its own pointer. It means nothing on its own, which makes it rather perfect for <NSKeyValueObserving>:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([object isKindOfClass:[NSOperation class]]) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {
            if (context == XXContext) {
                // ...
            } else {
                // ...
            }
        }
    }
}

Unsubscribing

When an observer is finished listening for changes on an object, it is expected to call –removeObserver:forKeyPath:context:. This will often either be called in -observeValueForKeyPath:ofObject:change:context:, or -dealloc (or a similar destruction method).

Safe Unsubscribe with @try / @catch

Perhaps the most profound annoyance about KVO is that if you make a call to –removeObserver:forKeyPath:context: when the object is not registered as an observer (whether because it was already unregistered or not registered in the first place), it throws an exception. The kicker is that there's not built-in way to even check if an object is registered!

Which causes one to rely on a rather unfortunate cudgel @try with an unhandled @catch:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([object isKindOfClass:[NSOperation class]]) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(isFinished))]) {
            if ([object isFinished]) {
              @try {
                  [object removeObserver:self forKeyPath:NSStringFromSelector(@selector(isFinished))];
              }
              @catch (NSException * __unused exception) {}
            }
        }
    }
}

Automatic Property Notifications

KVO is made useful by its near-universal adoption. Because of this, much of the work necessary to get everything hooked up correctly is automatically taken care of by the compiler and runtime.

But this wasn't always the case. Back before @synthesize, in the days of OS X and bindings, there was a lot of boilerplate that needed to be written for getters & setters:

- (double)price {
    return _price;
}

- (void)setPrice:(double)price {
    [self willChangeValueForKey:@"price"];
    _price = price;
    [self didChangeValueForKey:@"price"];
}

However, with today's modern conveniences of Objective-C 2.0, LLVM, & Clang, everything is taken care of for you automatically. Not only are @property declarations automatically @synthesized, but even custom setter implementations don't even need willChangeValueForKey: /didChangeValueForKey:; the runtime already expects that to change the value, by convention.

Classes can opt-out of automatic KVO by overriding+automaticallyNotifiesObserversForKey: and returning NO.

But what about compound or derived values? Let's say you have an object with a @dynamic,readonly address property, which reads and formats its streetAddresslocalityregion, and postalCode?

Well, you can implement the method keyPathsForValuesAffectingAddress (or its less magical catch-all, +keyPathsForValuesAffectingValueForKey:):

+ (NSSet *)keyPathsForValuesAffectingAddress {
    return [NSSet setWithObjects:@"streetAddress", @"locality", @"region", @"postalCode", nil];
}

So there you have it: some general observations and best practices for KVO. To an enterprising NSHipster, KVO can be a powerful substrate on top of which clever and powerful abstractions can be built. Use it wisely, and understand the rules and conventions to make the most of it in your own application.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值