-
Notifications
You must be signed in to change notification settings - Fork 364
YapDatabaseModifiedNotification
Answering that age old question: "What's new?"
YapDatabase simplifies many aspects of database development. It provides a simple Objective-C API. It has a straight-forward concurrency model that is a pleasure to use. And it provides a number of other great features such as built-in caching.
But sometimes the database itself isn't the difficult aspect of development. Sometimes it's updating the UI and keeping it in-sync with the underlying data model.
There are 2 features that make it dead simple to keep the User Interface in-sync with the data layer.
- Long-Lived Read Transactions
- YapDatabaseModifiedNotification
The first feature is long-lived read transactions. There is a dedicated wiki article for this topic. The rest of this article assumes you've already read the LongLivedReadTransactions article.
The second feature is the YapDatabaseModifiedNotification. These notifications allow you to figure out what changed during any particular read-write transaction.
A YapDatabaseModifiedNotification allows you to see what changed in a particular commit. For example:
- (void)viewDidLoad
{
// Freeze my database connection while I draw my views and populate my tableView.
// Background operations are free to update the database using their own connection.
[databaseConnection beginLongLivedReadTransaction];
// Register for notifications of changes to the database.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModified:)
name:YapDatabaseModifiedNotification
object:database];
}
- (void)yapDatabaseModified:(NSNotification *)notification
{
// Jump to the most recent commit.
// End & Re-Begin the long-lived transaction atomically.
// Also grab all the notifications for all the commits that I jump.
NSArray *notifications = [database beginLongLivedReadTransaction];
// Update views if needed
if ([databaseConnection hasChangeForKey:productId inCollection:@"products" inNotifications:notifications]) {
[self updateProductView];
}
// ...
}As you can see, these 2 features allow you to write code in a natural manner on the main thread. Even though you have all the power and concurrency of YapDatabase throughout the rest of your app, you can easily provide a stable database connection for the main thread. And you can easily manage moving your UI from one state to another.
There is an extensive set of API's to inspect YapDatabaseModifiedNotification's. You can use it to easily query to see if anything being displayed was updated.
From YapDatabaseConnection.h:
// Query for any change to a collection
- (BOOL)hasChangeForCollection:(NSString *)collection inNotifications:(NSArray *)notifications;
- (BOOL)hasObjectChangeForCollection:(NSString *)collection inNotifications:(NSArray *)notifications;
- (BOOL)hasMetadataChangeForCollection:(NSString *)collection inNotifications:(NSArray *)notifications;
// Query for a change to a particular key/collection tuple
- (BOOL)hasChangeForKey:(NSString *)key
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;
- (BOOL)hasObjectChangeForKey:(NSString *)key
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;
- (BOOL)hasMetadataChangeForKey:(NSString *)key
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;
// Query for a change to a particular set of keys in a collection
- (BOOL)hasChangeForAnyKeys:(NSSet *)keys
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;
- (BOOL)hasObjectChangeForAnyKeys:(NSSet *)keys
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;
- (BOOL)hasMetadataChangeForAnyKeys:(NSSet *)keys
inCollection:(NSString *)collection
inNotifications:(NSArray *)notifications;You'll notice that all the methods take an array of notifications. This is designed to match the NSArray of notifications you get via beginLongLivedReadTransaction and endLongLivedReadTransaction.
The processing code is the same. Internally it doesn't matter if you pass it an array with one notification, or with 10 notifications. Multiple change-sets can always be and treated as one.
YapDatabase comes with several cool extensions. One such extension is Views, which allows you to do some pretty cool stuff. Views are particularly helpful when sorting your data for display in a tableView or collectionView.
And extensions are also integrated into the YapDatabaseModifiedNotification architecture.
In terms of views, this means that you can pass a YapDatabaseModifiedNotification to a view, and it will spit out what changed in terms of the view. That is, it can say something like this:
- The row at index 7 was moved to index 9
- A row was inserted at index 12
- The row at index 4 was deleted
In short, exactly what you need to pass to UITableView / UICollectionView in order to animate changes.
And again, it doesn't matter if you need to jump 1 commit, or 10 commits. No sweat.
- (void)viewDidLoad
{
// Freeze our connection for use on the main-thread.
// This gives us a stable data-source that won't change until we tell it to.
[databaseConnection beginLongLivedReadTransaction];
// The view may have a whole bunch of groups.
// In this example, we just want to look at a single group (1 section).
NSArray *groups = @[ @"bestSellers" ];
mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"sales"];
// We can do all kinds of cool stuff with the mappings object.
// See the views article for more information.
//
// Now initialize the mappings object.
// It will fetch and cache the counts per group/section.
[databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
// One-time initialization
[mappings updateWithTransaction:transaction];
}];
// And register for notifications when the database changes.
// Our method will be invoked on the main-thread,
// and will allow us to move our stable data-source from our existing state to an updated state.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModified:)
name:YapDatabaseModifiedNotification
object:databaseConnection.database];
}
- (NSInteger)tableView:(UITableView *)sender numberOfRowsInSection:(NSInteger)section
{
return [mappings numberOfItemsInSection:section];
}
- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
__block SaleItem *item = nil;
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
item = [[transaction extension:@"sales"] objectAtIndexPath:indexPath withMappings:mappings];
}];
return [self cellForItem:item];
}
- (void)yapDatabaseModified:(NSNotification *)notification
{
// Jump to the most recent commit.
// End & Re-Begin the long-lived transaction atomically.
// Also grab all the notifications for all the commits that I jump.
NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];
// What changed in my tableView?
NSArray *rowChanges = nil;
[[databaseConnection ext:@"salesRank"] getSectionChanges:NULL
rowChanges:&rowChanges
forNotifications:notifications
withMappings:mappings];
if ([rowChanges count] == 0)
{
// There aren't any changes that affect our tableView!
return;
}
// Familiar with NSFetchedResultsController?
// Then this should look pretty familiar
[self.tableView beginUpdates];
for (YapDatabaseViewRowChange *rowChange in rowChanges)
{
switch (rowChange.type)
{
case YapDatabaseViewChangeDelete :
{
[self.tableView deleteRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeInsert :
{
[self.tableView insertRowsAtIndexPaths:@[ rowChange.newIndexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeMove :
{
[self.tableView deleteRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ rowChange.newIndexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeUpdate :
{
[self.tableView reloadRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
}
}
[self.tableView endUpdates];
}So what exactly is inside a YapDatabaseModifiedNotification?
For the most part, if you don't care, you don't ever need to know. Because the API's (mentioned above) can translate the notifications into answers to questions you might have. Like "did this key change"?
But you're more than welcome to inspect it yourself. In fact, you may find it useful occasionally.
From YapDatabase.h:
/**
* This notification is posted following a readwrite transaction where the database was modified.
*
* The notification object will be the database instance itself.
* That is, it will be an instance of YapDatabase.
*
* The userInfo dictionary will look something like this:
* @{
* YapDatabaseSnapshotKey = <NSNumber of snapshot, incremented per read-write transaction w/modification>,
* YapDatabaseConnectionKey = <YapDatabaseConnection instance that made the modification(s)>,
* YapDatabaseExtensionsKey = <NSDictionary with individual changeset info per extension>,
* YapDatabaseCustomKey = <Optional object associated with this change, set by you>,
* }
*
* This notification is always posted to the main thread.
**/
extern NSString *const YapDatabaseModifiedNotification;
extern NSString *const YapDatabaseSnapshotKey;
extern NSString *const YapDatabaseConnectionKey;
extern NSString *const YapDatabaseExtensionsKey;
extern NSString *const YapDatabaseCustomKey;The snapshot is a uint64_t that gets incremented every-time a read-write transaction is executed (which actually makes a change to the database). So every-time the app launches, the snapshot starts at zero. And then it gets incremented as you make changes to the database.
uint64_t snapshotOfCommit = [[notification.userInfo objectForKey:YapDatabaseSnapshotKey] unsignedLongLongValue];This is generally "internal" information. But it can sometimes come in handy. Most often when debugging. Being able to log commit numbers as they get processed can sometimes give you a deeper understanding of what your UI is processing, and where that tricky bug is coming from.
Snapshot information is prevalent throughout the architecture. At any time, you can query the YapDatabase object to see what the most recent commit number is. And you can query YapDatabaseConnection objects to see what commit number they are on.
The connection tells you what connection was used to make the change. This is, again, generally only useful for debugging purposes. (Don't forget about the handy optional name property of connections.)
YapDatabaseConnection *committer = [notification.userInfo objectForKey:YapDatabaseConnectionKey];
NSLog(@"Processing commit from: %p (%@)", committer, committer.name);The YapDatabaseExtensionsKey returns a dictionary, with change-set information from each registered extension. For example, if you registered a YapDatabaseView under the name @"order", then the dictionary would have a key named @"order", and the value would be change-set information for that YapDatabaseView instance. This is internal information. But if you create your own extension for YapDatabase, then the key would come in handy.
And the YapDatabaseCustomKey is just for you! You can put whatever you want in it. See below.
It is sometimes useful to have extra information about a commit. That is, more than just the commit itself. Application specific information.
For example, we may want to know if a change was initiated by the user, or if came through application internals. Maybe we want this information in order to customize the animation(s).
Whatever the case may be, you can do so by injecting whatever custom information you may need into YapDatabaseModifiedNotification:
[backgroundConnection asyncReadWriteWithBlock:^(YapCollectionsDatabaseReadWriteTransaction *transaction){
[transaction removeAllObjectsInCollection:collectionId];
NSDictionary *transactionExtendedInfo = @{
USER_MANUALLY_CLEARED_COLLECTION_ID: collectionId };
transaction.yapDatabaseModifiedNotificationCustomObject = transactionExtendedInfo;
}];Once you receive the commit notification(s), you can then inspect them for your custom extended info.
- (void)yapDatabaseModified:(NSNotification *)notification
{
// Jump to the most recent commit.
// End & Re-Begin the long-lived transaction atomically.
// Also grab all the notifications for all the commits that I jump.
NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];
// Look for my extended info
BOOL userManuallyClearedThisCollection = NO;
for (NSNotification *notification in notifications)
{
NSDictionary *transactionExtendedInfo = [notification.userInfo objectForKey:YapDatabaseCustomKey];
if (transactionExtendedInfo)
{
NSString *aCollectionId = transactionExtendedInfo[USER_MANUALLY_CLEARED_COLLECTION_ID];
if ([aCollectionId isEqualToString:self.collectionId])
{
userManuallyClearedThisCollection = YES;
}
}
}
// ... standard boilerplate code to update tableView goes here ...
}You can use this technique to include any additional information about the commit that may be useful. There are many possibilities.