KVO subscription inspired by FBKVOController and MAKVONotificationCenter, with compatibility back to iOS5.
===
You will need to have a minimum deployment target of iOS 5.0+ or OSX 10.7+, and be running under ARC, or know how to selectively enable ARC on specific files.
Here's how to get ZCRMailbox up and running:
- Drag-n-drop
- Cocoapods
- Build a framework (iOS only)
Struggling in a sea of options? Allow me to strongly suggest using Cocoapods!
Regardless of which method you choose, you can start working with ZCRMailbox by importing it where you need:
#import "ZCRMailbox.h"
This project is really just two files: ZCRMailbox.h and ZCRMailbox.m. You can find them by cloning the repo and dragging them from the Classes
directory into your Xcode project. Just make sure the "Copy items into destination group's folder (if needed)" checkbox is checked as well as whatever targets you need to use them in. Oh, and it's very unlikely, but if the names clash you may need to rename the files.
Need more info on Cocoapods? Check out their website for more information on what its used for and how to get it running.
When you're done with that, or if you're already familiar with Cocoapods, just add
pod "ZCRMailbox"
to your Podfile
, run pod install
and you're good to go!
If you're working on a serious project, it's recommended to specify at least a major and minor version in your Podfile
, ala: pod "ZCRMailbox", "~> 0.1"
.
Framework fan? Clone the repo and open the Project/ZCRMailbox.xcodeproj
up in Xcode. Change the target to Framework
and build. Navigate to the Organizer and in the "Projects" tab, locate the ZCRMailbox project. Click the minuscule arrow next to the "Derived Data" file path to open it in Finder. The correct folder should be highlighted and named something like ZCRMailbox-<GOBBLEDYGOOK>
. Navigate into there then into Build/Products/Release-<PLATFORM>
and finally you should see ZCRMailbox.framework
. Phew.
Get a drink to celebrate, then drag the framework somewhere much easier to find.
Now that you have the packaged framework you can drag it into your projects, zip it up and ship it to friends, or just keep it around for fun.
===
Create a ZCRMailbox
with a subscriber:
ZCRMailbox *mailbox = [[ZCRMailBox alloc] initWithSubscriber:subscriber]
Because the subscriber is only weakly referenced, it can be a good idea for the subscriber to keep hold of it's mailbox:
self.mailbox = [[ZCRMailbox alloc] initWithSubscriber:self];
If you need to deliver notifier messages to the subscriber on a specific queue, you can do that too:
self.mailbox.messageQueue = [NSOperationQueue mainQueue];
Once you have a mailbox you can start adding subscriptions to objects, called notifiers:
NSKeyValueObservingOptions options = NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew;
[self.mailbox subscribeTo:newsletter keyPath:@"updatedDate" options:options block:^(ZCRMessage *message) {
// Do something
}];
You can add subscriptions to as many notifiers and key-paths as you'd like, all from one mailbox!
If you prefer selectors to blocks, give one of these a try:
[self.mailbox subscribeTo:newsletter keyPath:@"updatedDate" options:options selector:@selector(newsletterDidUpdateDate)];
[self.mailbox subscribeTo:newsletter keyPath:@"posts" options:options selector:@selector(newsletterDidUpdatePost:))];
...
- (void)newsletterDidUpdateDate { // Do stuff }
- (void)newsletterDidUpdatePost:(ZCRMessage *)message { // Do stuff }
Finally, if you are migrating traditional KVO code and want a quick plug-in solution until you can get selectors and blocks working, there's this:
static void *ABCSubscriberKVOContext = &ABCSubscriberKVOContext;
...
[self.mailbox subscribeTo:newsletter keyPath:@"updatedDate" options:options context:ABCSubscriberKVOContext];
...
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == ABCSubscriberKVOContext) {
// Optionally convert this to a message
ZCRMessage *message = [[ZCRMessage alloc] initWithNotifier:object keyPath:keyPath change:change];
// Do something
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
Not very pretty, but it'll do in a pinch. The context is optional, but traditionally a good idea to make sure the notification is the one you want to receive.
When you're done with a mailbox, you can simply let it deallocate. This will automatically cancel all subscriptions.
If you need to unsubscribe but still want to work with the mailbox, you can use the unsubscribe methods:
[self.mailbox unsubscribeFrom:newsletter keyPath:@"updatedDate"];
[self.mailbox unsubscribeFrom:newsletter];
[self.mailbox unsubscribeFromAll];
===
ZCRMailbox has unit tests using XCTest, and is continuously integrated using Travis CI. To run the tests yourself, first clone the repo. You can then either run the tests from within Xcode or from the command line.
Open up Project/ZCRMailbox.xcodeproj
and make sure the scheme is set to "ZCRMailbox." Run the tests by selecting Product/Test
from the top menu or using the shortcut CMD+U
.
First install xctool. Then from within the project directory, run rake test
.
Want more details on how the sausage gets made? This section will discuss some of the implementation details of ZCRMailbox.
ZCRMailbox
weakly references the subscriber and maintains a dictionary of strongly referenced notifiers as keys for sets of_ZCRSubscription
objects_ZCRSubscription
weakly references theZCRMailbox
that created it and other details about the KVO observation.- To add and remove subscriptions,
ZCRMailbox
defers to the_ZCRPostOffice
which is shared between all mailboxes. _ZCRPostOffice
actually handles all KVO observation and observation-removal to ensure thread safety. It maintains a set of_ZCRSubscription
objects sent byZCRMailbox
instances.
- Simple
NSRecursiveLock
instances are used to maintain thread safety. - Since all subscription is handled by the single locked
_ZCRPostOffice
,ZCRMailbox
instances can be created, subscribe, and unsubscribe on different threads without having to worry.
Because KVO subscription and un-subscription is expected to be synchronous, simple locking was used instead of a dispatch queue with multiple readers and a single writer. This would not be an issue if each ZCRMailbox
actually handled its own KVO notifications, since an individual mailbox is not expected to receive a large volume of subscriptions or un-subscriptions. However, to prevent zombie KVO updates past the lifecycle of an observation, all updates are funneled through the shared _ZCRPostOffice
which must synchronously handle all subscriptions. This can create a bottleneck if a large volume of subscriptions and un-subscriptions are occurring across all ZCRMailbox
instances. The practical impact of this is unclear, but you are encouraged to profile these classes if you experience performance issues.