YJSafeKVO 3.1.4

YJSafeKVO 3.1.4

TestsTested
LangLanguage Obj-CObjective C
License MIT
ReleasedLast Release Aug 2016

Maintained by huang-kun.



YJSafeKVO 3.1.4

Introduction

如果你更倾向于阅读中文,可以点击这里


Problems

The key value observing pattern is really important for the Cocoa and Cocoa Touch programming. You add an observer, observe the value changes, remove it when you finish.

However, if you not use it correctly, the results are basically crashes.

*** 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'


YJSafeKVO's Pattern

Despite the usability and safefy from default APIs, KVO is still important. As a developer, you probably just want to use some simple APIs to achieve the goal. Here comes YJSafeKVO. There are 3 patterns:

  • Observing
  • Subscribing
  • Posting


Observing

If A observes the change of B's name, then call:

[A observeTarget:B keyPath:@"name" updates:^(id A, id B, id _Nullable newValue) {
    // Update A based on newValue...
}];

Reading this is much natural semantically, or you can simply using PACK macro. (Recommended)

[A observe:PACK(B, name) updates:^(id A, id B, id _Nullable newValue) {
    // Update A based on newValue...
}];

A is considered as "Observer", or "Subscriber". B is considered as observed "Target".


Subscribing

bindTo: / boundTo:

It is availalbe for binding target to subscriber. When value changes, it will set changes from target's key path to subscriber's key path automatically. It looks like this:

[PACK(foo, name) bindTo:PACK(bar, name)];

After calling -bindTo:, whenever foo's name changed, it sends new value to bar's name. It can be considered as [source bindTo:subscriber].

It also have support backward data flow.

[PACK(foo, name) boundTo:PACK(bar, name)];

After calling -boundTo:, whenever bar's name changed, the new value will be sent to foo's name. It can be considered as [subscriber boundTo:source].

REMEMBER: The subscribing direction must be single direction (e.g. if you call [A bindTo:B], then never call [A boundTo:B] or [B bindTo:A] for same keyPath), otherwise it causes infinite loop.

now

After [PACK(foo, name) bindTo:PACK(bar, name)], the bar's name will get new value only if foo's name changes in the future. If you need foo's current name and set it to bar's name immediately, just calling -now in the end.

[[PACK(foo, name) bindTo:PACK(bar, name)] now]

filter:

You can filter the new value by returning YES or NO to decide whether you want to receive the new value.

[[[PACK(foo, name) bindTo:PACK(bar, name)] 
  filter:BOOL^(NSString * _Nullable newName){
      return newName.length > 5;
  }]
  now];

convert:

You can convert the data flow and return the new value which is suitable for subscriber.

[[[PACK(foo, name) bindTo:PACK(bar, name)] 
  convert:id _Nullable^(NSString * _Nullable newName){
      return [newName uppercaseString];
  }]
  now];

By using convert:, you also can bind two keyPath with different type.

[[[PACK(foo, name) bindTo:PACK(bar, hidden)] 
  convert:id _Nullable^(NSString * _Nullable newName){
      return newName.length ? @NO : @YES;
  }]
  now];

applied:

applied: will perform block each time when new value is applied.

[[[PACK(foo, name) bindTo:PACK(bar, name)] 
  applied:^{
      NSLog(@"bar just get a new name.");
  }]
  now];

get together

You can nest them together to make a complex binding.

[[[[[[[[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:

However, if your final result is determined by more than one changing factor, you can use combineLatest:reduce:, which will take changes from multiple sources and reduce them into a single value.

[PACK(clown, name) combineLatest:@[ PACK(foo, name), PACK(bar, name) ]
                          reduce:^id(NSString *fooName, NSString *barName) {
    return fooName && barName ? [fooName stringByAppendingString:barName] : nil;
}];

Subscribing pattern also supports -cutOff: for cutting off the binding relationship between subscriber's key path and target's key path.


Posting

Posting value changes directly.

[PACK(foo, name) post:^(NSString * _Nullable name) {
    if (name) NSLog(@"foo has changed a new name: %@.", name);
}];

If you need foo's current name immediately, remember to call -now in the end.

The foo is consider as a sender, when foo's name sets new value, it sends changes to the block. Also calling [PACK(foo, name) stop] to stop posting value changes.


There is one more thing

Should I worry about removing observer before object is deallocated, so I can prevent crashes ?

No! No extra work is required. Choose the pattern you like, and YJSafeKVO takes care the rest. It just work.


Philosophy

Graph

Here is a graph tree showing YJSafeKVO.

                            Target/Source
                                  |
                          Subscriber Manager
                                  |
              |--------------------------------------|
          Subscriber1 (weak)                    Subscriber2 (weak)   ...
              |                                      |
        Porter Manager                         Porter Manager
   |----------|-----------|                    |-----|-----
Porter1    Porter2     Porter3  ...         Porter4      ...
   |          |           |                    |
(block)    (block)     (block)              (block)


Roles

Target or Sender

Target or Sender is the source that value changes from. It always stay at the top of KVO chain.

Subscriber

The object which calls "-observeTarget:" or "-observe:" should be treated as the observer, because it is the one who really wants to observe and handles the value change. To try not to confuse the concept, I use "subscriber" instead.

Porter

Porter will be generated during KVO process and its job is to deliver the value changes to the object who wants to handle. Porter carries changes via a block.

Porter Manager

The object managing the porters. It usually owned by subscriber or sender.

Subscriber Manager

The object managing the subscribers. It usually owned by target. Unlike porter manager, the subscriber manager holds subscribers weakly.


Consequence

If target or sender is deallocated, the graph tree is gone. If one of subscribers is deallocated before target, only that branch of the graph tree is gone.

If you want to stop observation when you finish observing before any of them is deallocated, you can manually call -unobserve.., -cutOff: or -stop to stop observing.


Tips

Avoid retain cycle

It easily to cause retain cycle by using block.

[self observe:PACK(self.foo, name) updates:^(id receiver, id target, id _Nullable newName) {
    NSLog(@"%@", self); // Retain cycle
}];

To solve the issue: change receiver variable to self. No need extra __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.
}];


Deal with threads

For example if your observed property is being set with new value on one thread, and you expect to update UI with new value in the callback block executed on main thread. You can use the extended API for specifing a NSOperationQueue parameter.

[self observe:PACK(self.foo, name)
      options:NSKeyValueObservingOptionNew
        queue:[NSOperationQueue mainQueue]
      changes:^(id receiver, id target, NSDictionary *change) {
    // Callback on main thread
}

If you are familiar with -addObserverForName:object:queue:usingBlock: for NSNotificationCenter, then there is no barrier for using this API.


Allodoxaphobia: "Observing", "Subscribing" or "Posting" ???

There is not much differences between "Observing" and "Posting" because they share the same graph tree. The "Observing" is treated as "Omnipotent Pattern" in YJSafeKVO because whatever any other patterns can do, "Observing" can do as well. Here is an example for a view controller observing network conntection status and make a batch of changes when status is changed.

[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;
    ...
}];

The reason for using "Subscribing" is for the idea that you want one state is completely binded and decided by other states, so it will change value automatically rather than manually set by developer.


Swift Compatibility

The key value observing is the pattern from Cocoa programming. Any object as subclass of NSObject will get it for free. It also means this feature is not applied for Swift's struct, and for it's class object which root class is not NSObject.

Observing:

foo.observe(PACK(bar, "name")) { (_, _, newValue) in
    print("\(newValue)")
}

Subscribing:

PACK(foo, "name").boundTo(PACK(bar, "name"))

Build a complex binding:

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" 


Requirement

YJSafeKVO needs at least Xcode 7.3 for NS_SWIFT_NAME avaliable, so it can expose APIs for swift and feels more swifty.


Installation

YJSafeKVO is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod "YJSafeKVO"

Go to terminal and run pod install, then #import <YJSafeKVO/YJSafeKVO.h> into project's ProjectName-Prefix.pch file.


Author

huang-kun, [email protected]


License

YJSafeKVO is available under the MIT license. See the LICENSE file for more info.