Skip to content

YapDatabaseModifiedNotification

Robbie Hanson edited this page Nov 10, 2019 · 19 revisions

Answering that age old question: "What's new?"

YapDatabase simplifies many aspects of database development. It provides a simple API. It has a straight-forward concurrency model that's 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.

 

YapDatabase + UI

There are 2 features that make it dead simple to keep the User Interface in-sync with the data layer.

  1. Long-Lived Read Transactions
  2. 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.

 

What changed?

A YapDatabaseModifiedNotification allows you to see what changed in a particular commit. For example:

override func 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.
  dbConnection.beginLongLivedReadTrans()

  // Register for notifications of changes to the database.
  let nc = NotificationCenter.default
  nc.addObserver( self,
	      selector: #selector(self.yapDatabaseModified(notification:)),
	          name: Notification.Name.YapDatabaseModified,
	        object: nil)
}

@objc func yapDatabaseModified(notification: 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.
  let notifications = dbConnection.beginLongLivedReadTransaction()

  // Update views if needed
  if dbConnection.hasChange(forKey: productId, inCollection: "products", in: 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.

 

Simple API's

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:

// Query for any change to a collection

func hasChange(forCollection collection: String, in notifications: [Notification]) -> Bool

// Query for a change to a particular key/collection tuple

func hasChange(forKey key: String, inCollection collection: String, in notifications: [Notification]) -> Bool

// Query for a change to a particular set of keys in a collection

func hasChange(forAnyKeys keys: Set<String>, inCollection collection: String, in notifications: [Notification]) -> Bool

You'll notice that all the methods take an array of notifications. This is designed to match the array 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.

 

Extensions Too

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.

override func 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.
  dbConnection.beginLongLivedReadTransaction()
  
  // And register for notifications when the database changes.
  // We will be notified on the main-thread,
  // and we then move our stable data-source from
  // the existing state to an updated state.
  let nc = NotificationCenter.default
  nc.addObserver( self,
	      selector: #selector(self.yapDatabaseModified(notification:)),
	          name: Notification.Name.YapDatabaseModified,
	        object: nil)
  
  // We can do all kinds of cool stuff with the mappings object.
  // See the views article for more information.
  initializeMappings()
}

func initializeMappings() {
  
  dbConnection.read {(transaction) in
    
    guard let _ = transaction.ext("sales") as? YapDatabaseViewTransaction else{
      // The View isn't ready yet
      return
    }
    
    // The view may have a whole bunch of groups.
    // In this example, we just want to look at a single group (1 section).
    let groups = [ "bestSellers" ]
    mappings = YapDatabaseViewMappings(groups: groups, view: "sales")
    
    // One-time initialization
    mappings?.update(with: transaction)
  }
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return mappings?numberOfItems(inSection: UInt(section)) ?? 0
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

  var item: SaleItem? = nil
  dbConnection.read {(transaction) in
    if let mappings = self.mappings,
       let viewTransaction = transaction.ext("sales") as: YapDatabaseViewTransaction {
      item = viewTransaction.object(at: indexPath, with: mappings)
    }     
  }

  return self.cellForItem(item)
}

@objc func yapDatabaseModified(notification: 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.
  let notifications = dbConnection.beginLongLivedReadTransaction()

  guard let mappings = mappings else {
    initializeMappings()
    tableView.reloadData()
    return
  }
  
  guard let ext = dbConnection.ext("sales") as? YapDatabaseViewConnection else{
    return
  }
  
  // What changed in my tableView?
  let (sectionChanges, rowChanges) = ext.getChanges(forNotifications: notifications, withMappings: mappings)
  
  if (sectionChanges.count == 0) && (rowChanges.count == 0) {
    // There aren't any changes that affect our tableView!
    return
  }
  
  // Familiar with NSFetchedResultsController?
  // Then this should look pretty familiar

  tableView.beginUpdates()
	for rowChange in rowChanges {
		switch rowChange.type {
			 
			case .delete:
				tableView.deleteRows(at: [rowChange.indexPath!], with: .automatic)
			
			case .insert:
				tableView.insertRows(at: [rowChange.newIndexPath!], with: .automatic)
			 
			case .move:
				tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
			
			case .update:
				tableView.reloadRows(at: [rowChange.indexPath!], with: .automatic)
			
			default:
				break
		}
	}
	tableView.endUpdates()
}

 

YapDatabaseModifiedNotification Internals

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.

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 : <number of snapshot, incremented per read-write transaction w/modification>,
///   YapDatabaseConnectionKey : <YapDatabaseConnection instance that made the modification(s)>,
///   YapDatabaseExtensionsKey : <Dictionary 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.
static let YapDatabaseModified: NSNotification.Name

static let YapDatabaseSnapshotKey: String
static let YapDatabaseConnectionKey: String
static let YapDatabaseExtensionsKey: String
static let YapDatabaseCustomKey: String

static let YapDatabaseObjectChangesKey: String
static let YapDatabaseMetadataChangesKey: String
static let YapDatabaseInsertedKeysKey: String
static let YapDatabaseRemovedKeysKey: String
static let YapDatabaseRemovedCollectionsKey: String
static let YapDatabaseAllKeysRemovedKey: String

The snapshot is a UInt64 that gets incremented every-time a read-write transaction is executed (which actually makes a change to the database).

let number = notification.userInfo?[YapDatabaseSnapshotKey] as? NSNumber
let snapshotOfCommit = number?.uint64Value

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 a YapDatabase instance to see what the most recent commit number is. And you can query a YapDatabaseConnection instance to see what commit number it's on.

The connection tells you what connection was used to make the change. This is, again, generally only useful for debugging purposes.

let committer = notification.userInfo?[YapDatabaseConnectionKey] as? YapDatabaseConnection

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 particular 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.

 

Adding custom information to a YapDatabaseModifiedNotification

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:

bgConnection.asyncReadWrite {(transaction) in

  transaction.removeAllObjects(inCollection: collectionId)

  let customInfo = [USER_MANUALLY_CLEARED_COLLECTION_ID: collectionId]
  transaction.yapDatabaseModifiedNotificationCustomObject = customInfo
}

Once you receive the commit notification(s), you can then inspect them for your custom extended info.

@objc func yapDatabaseModified(notification: 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.
  let notifications = dbConnection.beginLongLivedReadTransaction()
  
  // Look for my extended info
  var userManuallyClearedThisCollection = false
  for notification in notifications {
    
    if let customInfo = notification.userInfo?[YapDatabaseCustomKey] as? [String: String] {
      
      if let collectionId = customInfo[USER_MANUALLY_CLEARED_COLLECTION_ID] {
        if collectionId == self.collectionId {
          userManuallyClearedThisCollection = true
        }
      }
    }
  }
    
  // ...
}

You can use this technique to include any additional information about the commit that may be useful. There are many possibilities.