Skip to content
Dr. Mickey Lauer edited this page Oct 28, 2020 · 96 revisions

There's an extension for that.

Most of us are used to "regular" databases. That is, we're used to tables, columns, indexes, and SQL queries. Which explains the general reaction when we hear the term "key-value database":

But I can't use that! I need my SQL queries. Because I need to sort my data for display in a table view.

Relax. YapDatabase has you covered.

 

What are views?

Imagine you want to display your data in a tableView. How would you go about it? You would start by answering the following questions:

  • Do you want to display all your data, or just a subset of it?
  • Do you want to group it into sections?
  • How do you want to sort the objects?

In SQL this translates into:

  • WHERE ... (filter)
  • GROUP BY ... (group)
  • ORDER BY ... (sort)

A view provides the same functionality. In fact, the term "view" is meant to describe a particular way of "viewing" your data.

When you create a view you start by answering the 3 questions above. But you don't do so with esoteric SQL syntax. It's even easier. You simply provide a block of code.

Code ?!? Like I can use regular swift code ?!?

Yup. For example, the sorting block is kinda like writing a compare method. It's both easy and powerful because you have access to any code you need. (So if you've ever wanted to sort strings using Apple's localized string compare methods, then you're in luck.) More on this in a moment.

A view is also persistent.

You set it up once and you're done. Any modifications to the database are automatically handled by the view. That is, you can modify the database just as you always have, and the view will automatically update itself according to the changes you make (insertions, deletions, updates, etc).

If you're familiar with Core Data, then you can also think of a view like a NSFetchedResultsController. And in fact a view also has a notification mechanism that tells you when something changed, and exactly what indexes were added, deleted, moved, etc. Which makes it a breeze to animate changes to your tableView / collectionView.

 

Initializing a view

As mentioned above, there are 3 questions you need to answer when creating a view. These translate into 2 separate blocks of code that answer these questions:

  1. filter & group block
  2. sort block

Let's start with block number 1. The "grouping" block:

let grouping = YapDatabaseViewGrouping.withObjectBlock {
  (transaction, collection, key, obj: Any) -> String? in

  // The parameters to the block come from a row in the database.
  // You can inspect the parameters, and determine if the row is included in the view.
  // And if so, you can decide which "group" it belongs to.

  if let _ = obj as Book {
    return "books"
  }
  if let _ = obj as Magazine {
    return "magazines"
  }

  return nil // exclude from view
}

When you add or update objects in the database, the view automatically invokes the grouping block. As you can see, the grouping block is being passed the information from the row. Your grouping block can then inspect the row and determine if it should be a part of the view. If not, your grouping block simply returns 'nil' and the object is excluded from the view (removing it if needed).

Otherwise your grouping block returns a group, which can be any string you want. Once the view knows what group the row belongs to, it then needs to determine the index/position of the row within the group.

This is where block number 2 comes in. The "sorting" block:

let sorting = YapDatabaseViewSorting.withObjectBlock {
  (transaction, group,
   collection1, key1, obj1: Any,
   collection2, key2, obj2: Any,) -> ComparisonResult in

  // The "group" parameter comes from your grouping block.
  // The other parameters are from 2 different rows,
  // both of which are part of the given "group".
  // 
  // Simply compare the 2 rows however you want,
  // and return a ComparisonResult, just like a compare method.

  if group == "books" {
    let book1 = obj1 as! Book
    let book2 = obj2 as! Book
    return book1.compareBookByTitleThenAuthor(book2)
  }
  else {
    let mag1 = obj1 as! Magazine
    let mag2 = obj2 as! Magazine
    return mag1.compareMagazineByMonthThenTitle(mag2) 
  }
}

With the sorting block, the view can easily compare a new or updated row with existing rows in the same group. And thus the view will efficiently update itself whenever need be.

Note: Both YapDatabaseViewGrouping & YapDatabaseViewSorting have multiple initializers:

  • withKeyBlock {(transaction, collection, key) in /* ... */ }
  • withObjectBlock {(transaction, collection, key, obj) in /* ... */}
  • withMetadataBlock {(transactino, collection, key, metadata) in /* ... */}
  • withRowBlock {(transaction, collection, key, obj, metadata) in /* ... */}

Just choose the option that gives you the minimum amount of information you need, and the view will optimize the rest for you.

 

Understanding Views

As you can see, it's fairly straightforward to create the blocks a view needs. But you might still be a little confused about views.

How do they work? What are they doing internally?

A view simply stores an ordered array of collection/key tuples.

It's pretty much that simple. In fact, you can conceptualize a view as a dictionary, where the keys are the groups you specified, and the values are ordered arrays of collection/key tuples. For example:

// Conceptualize a view like this Dictionary:
[
  "books": [ ("fiction","key24"), ("fantasy","key7"), ("mystery","key11") ],
  "magazines": [ ("gossip","key60"), ("science","key49") ]
]

In reality, it's a bit more complicated. Technically it stores ordered array's of Int64 rowId's... And the view needs to store the ordered array to the database, and it needs to do so efficiently. So it splits up the array into pages. It also has an efficient mechanism to figure out if a given collection/key tuple is part of a group.

But if you think about a view like the dictionary above, you get the overall idea.

Notice also that there is a difference between a collection (in the regular database) and a group (in the view). From the example above, the group named "books" actually consists of items from multiple collections ("fiction", "fantasy", "mystery", etc). Just as a SQL query can pull items from anywhere in the database, so too can a view group and sort items however it wants.

 

Full init example

We now have everything we need to code the full initialization process.

let grouping = YapDatabaseViewGrouping.withObjectBlock {
  (transaction, collection, key, obj: Any) -> String? in

  // The parameters to the block come from a row in the database.
  // You can inspect the parameters, and determine if the row is included in the view.
  // And if so, you can decide which "group" it belongs to.

  if let _ = obj as Book {
    return "books"
  }
  if let _ = obj as Magazine {
    return "magazines"
  }

  return nil // exclude from view
}

let sorting = YapDatabaseViewSorting.withObjectBlock {
  (transaction, group,
   collection1, key1, obj1: Any,
   collection2, key2, obj2: Any,) -> ComparisonResult in

  // The "group" parameter comes from your grouping block.
  // The other parameters are from 2 different rows,
  // both of which are part of the given "group".
  // 
  // Simply compare the 2 rows however you want,
  // and return a ComparisonResult, just like a compare method.

  if group == "books" {
    let book1 = obj1 as! Book
    let book2 = obj2 as! Book
    return book1.compareBookByTitleThenAuthor(book2)
  }
  else {
    let mag1 = obj1 as! Magazine
    let mag2 = obj2 as! Magazine
    return mag1.compareMagazineByMonthThenTitle(mag2) 
  }
}

// The 'versionTag' is a string that tells the View if you've
// made changes to the groupingBlock or sortingBlock since last app launch.
// If the versionTag hasn't changed, the View doesn't need to do anything.
// But if the versionTag HAS changed, the View will automatically re-populate
// itself. That is, it will:
// - flush its internal tables
// - enumerate the database, and invoke your grouping block to see
//   if the row should be included in the view
// - invoke your sorting block (as needed) to sort items in the view
// 
// In other words, if you make changes to your grouping/sorting code,
// then just change your versionTag, and the View will do the right thing.
// 
let versionTag = "2019-05-20" // <= change me if you modify grouping or sorting

let options = YapDatabaseViewOptions()

// You can optionally limit the collections your view uses.
// For example, if your View will only contain items from a few collections,
// (say "Foo" & "Bar"), then you can specify a whitelist of collections:
/*
let whitelist = Set(["Foo", "Bar"])
options.allowedCollections = YapWhitelistBlacklist(whitelist: whitelist)
*/
// What you're saying here is:
//   My View will ONLY include items from collections 'Foo' & 'Bar'.
//   So if you see rows in other collections, just act as if you invoked my
//   grouping block, and it returned 'nil'.
//   But don't actually bother invoking my grouping block.
// 
// This is most helpful when you're:
// - creating a view for the first time
// - OR re-populating a view due to a versionTag change
// - AND the database has a LOT of item in it
// - AND the view is a small subset of the entire database
// 
// This allows the View to enumerate a small subset of the database
// when initializing or re-populating.
		
let view =
  YapDatabaseAutoView(grouping: grouping,
                       sorting: sorting,
                    versionTag: versionTag,
                       options: options)
		
let extName = "order" // <= Name we will use to access this view
database.asyncRegister(view, withName: extName) {(ready) in
			
  if !ready {
    print("Error registering \(extName) !!!")
  }
}

 

Registering a view

You can register as many different views as you want !

For example, you might use one view to sort books based on a standard sorting scheme (e.g. by genre and then by title). While another view is used exclusively for sorting the best-sellers in order of popularity. So a particular book might show up in both views.

Once registered, the view becomes a part of the database, and it will automatically update itself. The recommended strategy is:

  • Initialize your database during app launch
  • Immediately after initializing your database, create all of your extensions (such as views)
  • Register your extensions using database.asyncRegister(ext, withName:"...")

 

Accessing a view

Once registered, a view can be accessed from within any transaction by simply using the registered name.

dbConnection.read {(transaction) in

  if let viewTransaction = transaction.ext("order") as? YapDatabaseViewTransaction {
    book = viewTransaction.object(atIndex: indexPath.row, inGroup: "books") as? Book
  }
}

When when we registered the view, we gave it the name "order". So we just use this name to access it.

As you may imagine, there's a large API for working with views. Here's a small sample from YapDatabaseViewTransaction.h :

func numberOfGroups() -> UInt
func allGroups() -> [String]
func hasGroup(_ group: String) -> Bool

func numberOfItems(inGroup group: String) -> UInt
func numberOfItemsInAllGroups() -> UInt

func getCollectionKey(atIndex index: Int, inGroup group: String) -> (String, String)?

func getGroupIndex(forKey key: String, inCollection collection: String?) -> (String, Int)?

func object(at index: UInt, inGroup group: String) -> Any?

// Iteration

// block params: (collection, key, index, stop)
func iterateKeys(inGroup group: String, using block: (String, String, Int, inout Bool) -> Void

// When using Mappings:

func getCollectionKey(forRow row: Int, section: Int, withMappings mappings: YapDatabaseViewMappings) -> (String, String)?

func getCollectionKey(atIndexPath indexPath: IndexPath, withMappings mappings: YapDatabaseViewMappings) -> (String, String)?

For the full API, please see YapDatabaseViewTransaction.h

 

Re-Configuring a view

What if I later need to change my groupingBlock and/or sortingBlock? Is that possible?

Yes!

Each time your app launches, it registers the extensions it needs. So if you're using views, then upon each app launch, you're initializing your view instance, and registering it with the database system.

This is explained in much more detail in the Extensions article. But we'll review it briefly here.

The database persists various metadata about the extensions that you register. So the very first time you register a view, the database system knows its a "virgin" view, and the view automatically populates itself. That is, if you already have a database with a bunch of collection/key/values, and you add a new view, then the view will automatically enumerate over the pre-existing rows in the database and populate itself.

When you run the app a second time, the database system knows the view is being re-registered, and it doesn't have to do much. That is, the database system knows the view is already up-to-date, so it doesn't have to do much besides the basic extension registration process.

But this begs the question: If I change the groupingBlock and/or sortingBlock, how do I tell the view that I've changed them so it will automatically re-populate itself?

If you make changes to the groupingBlock and/or sortingBlock, then just change the versionTag, and the view will automatically re-populate itself.

let versionTag = "2" // Increment me when you change the groupingBlock and/or sortingBlock
let view =
  YapDatabaseAutoView(grouping: grouping
                   sorting: sorting
                versionTag: versionTag) // <-- versioning your view

Or, if you want to update an existing view:

dbConnection.asyncReadWrite {(transaction) in

  if let viewTransaction = transaction.ext("order") as? YapDatabaseViewTransaction {
    viewTransaction.setGrouping(newGrouping, sorting: newSorting, versionTag: newVersionTag)
  }
}

The view persists the versionTag in the database. So if you change it, then the view will notice the change during the registration process. And then it will automatically flush its tables, and repopulate itself.

(If you've never explicitly set the versionTag, then it's persisted as an empty string.)

Note also that because the versionTag is a string, you can do clever things with it. For example, your sortingBlock might be locale specific. (E.g. String.localizedCompare()) But what if the user changes their language, and relaunches our app?

let localeID = Locale.current.identifier
let versionTag = "5-\(localID)"

And so if you relaunch the app, and it finds itself in a different locale, the view will automatically re-group / re-sort itself.