Skip to content
Dr. Michael 'Mickey' Lauer edited this page Apr 11, 2018 · 94 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 table. 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 an esoteric SQL query string. It's even easier. You simply provide a block of code.

Code ?!? Like I can use regular objective-c/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:

YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withRowBlock:
    ^NSString *(YapDatabaseReadTransaction *transaction,
                NSString *collection, NSString *key, id object, id metadata)
{
    // 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 ([object isKindOfClass:[BNBook class]])
        return @"books";
    if ([object isKindOfClass:[BNMagazine class]])
        return @"magazines";

    return nil; // exclude from view
};

When you add or update rows 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:

YapDatabaseViewSorting *sorting = [YapDatabaseViewSorting withRowBlock:
    ^(YapDatabaseReadTransaction *transaction, NSString *group,
      NSString *collection1, NSString *key1, id obj1, id meta1
      NSString *collection2, NSString *key2, id obj2, id meta2)
{
    // 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 NSComparisonResult, just like a compare method.

    if ([group isEqualToString:@"books"])
        return [obj1 compareBookByTitleThenAuthor:obj2];
    else
        return [obj1 compareMagazineByMonthThenTitle:obj2];
};

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.

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 NSDictionary:
@{
    @"books"     : @[ {@"fiction",@"key24"}, {@"fantasy",@"key7"}, {@"mystery",@"key11"} ],
    @"magazines" : @[ {@"gossip",@"key60"}, {@"science",@"key49"}, {@"travel",@"key82"} ]
};

In reality, it's a bit more complicated. Technically it stores ordered array's of int64_t 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.

Initialization Tips

Let's take a closer look at the various init methods for YapDatabaseViewGrouping.

@interface YapDatabaseViewGrouping : NSObject

typedef NSString* _Nullable (^YapDatabaseViewGroupingWithKeyBlock)
             (YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key);

typedef NSString* _Nullable (^YapDatabaseViewGroupingWithObjectBlock)
             (YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object);

typedef NSString* _Nullable (^YapDatabaseViewGroupingWithMetadataBlock)
             (YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, _Nullable id metadata);

typedef NSString* _Nullable (^YapDatabaseViewGroupingWithRowBlock)
             (YapDatabaseReadTransaction *transaction, NSString *collection, NSString *key, id object, _Nullable id metadata);

+ (instancetype)withKeyBlock:(YapDatabaseViewGroupingWithKeyBlock)block;
+ (instancetype)withObjectBlock:(YapDatabaseViewGroupingWithObjectBlock)block;
+ (instancetype)withMetadataBlock:(YapDatabaseViewGroupingWithMetadataBlock)block;
+ (instancetype)withRowBlock:(YapDatabaseViewGroupingWithRowBlock)block;

// ...

We already saw [YapDatabaseViewGrouping withRowBlock:] used above. But what are those other methods?

It all has to do with optimizations. Let's take another look at our previous grouping implementation:

YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withRowBlock:
    ^NSString *(YapDatabaseReadTransaction *transaction,
                NSString *collection, NSString *key, id object, id metadata)
{
    // 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 ([object isKindOfClass:[BNBook class]])
        return @"books";
    if ([object isKindOfClass:[BNMagazine class]])
        return @"magazines";

    return nil; // exclude from view
}];

You'll notice that the block doesn't inspect the metadata. Sorting is entirely dependent on the object. So what happens if we make a change to the metadata for a particular row, but we don't change the object? Will the view have to go through the trouble of re-checking the index of the item within the view, and invoking our sorting block needlessly?

In this case, the answer is YES. But we can fix that. We can simply change our grouping init method to inform the view that metadata doesn't impact sorting:

YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withObjectBlock:
    ^NSString *(YapDatabaseReadTransaction *transaction,
                NSString *collection, NSString *key, id object)
{
    // 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 ([object isKindOfClass:[BNBook class]])
        return @"books";
    if ([object isKindOfClass:[BNMagazine class]])
        return @"magazines";

    return nil; // exclude from view
};

And voilà! The view can now make various optimizations since it knows that metadata doesn't affect the grouping of a row. In fact, you could make the exact same change to the sorting.

Simply use the init method that requires the minimum amount of information that you need, and YapDatabaseView will automatically optimize when it runs your blocks.

Full init example

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

YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withRowBlock:
    ^NSString *(YapDatabaseReadTransaction *transaction,
                NSString *collection, NSString *key, id object, id metadata)
{
    // 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 ([object isKindOfClass:[BNBook class]])
        return @"books";
    if ([object isKindOfClass:[BNMagazine class]])
        return @"magazines";

    return nil; // exclude from view
}];

YapDatabaseViewSorting *sorting = [YapDatabaseViewSorting withObjectBlock:
    ^(YapDatabaseReadTransaction *transaction, NSString *group,
      NSString *collection1, NSString *key1, id obj1,
      NSString *collection2, NSString *key2, id obj2)
{
    // 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 NSComparisonResult, just like a compare method.

    if ([group isEqualToString:@"books"])
        return [obj1 compareBookByTitleThenAuthor:obj2];
    else
        return [obj1 compareMagazineByMonthThenTitle:obj2];
}];

YapDatabaseView *databaseView =
    [[YapDatabaseView alloc] initWithGrouping:grouping
                                      sorting:sorting];

Now that we have a view instance, it's time to plug it into the database system.

Registering a view

A view is an extension to YapDatabase. In fact, it is one of several standard extensions that ship with YapDatabase. You are even free to create your own.

Extensions are completely optional. You can use zero of them, or a ton of them. You can even create multiple views.

For example, you might use one view to order 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.

The next step after creating an instance of a view (as we did above), is to "register" it with the database. Once registered, the extension becomes a part of the database, and it can automatically update itself. Plus we can query it to get information.

There is a full wiki article that discusses Extensions if you want more information. In this article, we're just going to briefly review the registration process in order to continue our discussion on views.

The registration process is fairly simple. It uses a simple naming convention:

[database registerExtension:databaseView withName:@"order"];

As mentioned above, you might actually use multiple view instances. To do this you'd simply use a different name during the registration process:

[database registerExtension:anotherDatabaseView withName:@"best-sellers"];

View attributes

All extensions, including views, share 3 core attributes:

  • Automatic
  • Transactional
  • Persistent (optional)

First, a view updates itself automatically. Let's look at a concrete example:

This is what your code looks like before you start using extensions:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
    // without extensions:
    [transaction setObject:book forKey:book.isbn inCollection:@"books"];
}];

And this is what your code looks like after you start using extensions:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){
    // with extensions:
    [transaction setObject:book forKey:book.isbn inCollection:@"books"];
    
    // And... nothing else to do !
    // The registered extensions are automatically notified of the change,
    // and automatically update themselves.
}];

As you can see, there are no differences. So if you already have a bunch of code, you don't have to worry about making a ton of changes throughout your application.

Extensions work seamlessly behind the scenes. Once an instance of an extension is plugged into the database, it takes part in the transactions. So you can query an extension directly for information. Plus an extension is inherently part of a readwrite transaction, without you having to do anything special. Any time you make changes to the database, all active extensions are automatically notified of the change you made. This is how views can automatically update themselves. When you invoke setObject:forKey:inCollection:, the view is automatically invoked with the change so that it may update itself.

But how can a view update itself?

Well, when you initialized the view, you provided blocks of code. Thus the view already has the code it needs to run whenever you modify the database! So if you insert a new object, it can run your grouping and sorting blocks to figure out where the item fits within the view. And if you modify an object, it can run your grouping and sorting blocks to figure out if the item's position within the view changed. In other words, by providing those 2 simple blocks, you gave it all the information it needs to perform its job automatically.

This also means that all transaction rules that apply to the database also apply to a view. As you already know, a read-only transaction gives you a snapshot-in-time of the database. From within the transaction, the database is immutable, and won't change even if another connection is making changes. The same rules apply to a view. Similarly, you know that a read-write transaction is atomic. Again, this applies to views as well. Changes that you make affect the database and views in one atomic operation.

Finally, views can be persistent. They store the groups and the sorted array(s) of rowids to an internal sqlite table. So when you re-launch the app, the view is immediately ready to go as the rows are already grouped and sorted.

Note: A view can also be configured to be non-persistent, meaning it will only store its information in memory. You can configure this via the isPersistent property of YapDatabaseViewOptions. A non-persistent view is handy for situations where your view is temporary, and it doesn't makes sense to write it to the disk.

Accessing a view

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

[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){

    objectForRow = [[transaction ext:@"order"] objectAtIndex:indexPath.row inGroup:@"books"];
}];

Recall (from the registering a view section) that when we registered the view, we gave it the name @"order". And thus that is the name we used to access it above.

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

- (NSUInteger)numberOfGroups;
- (NSArray *)allGroups;

- (NSUInteger)numberOfKeysInGroup:(NSString *)group;
- (NSUInteger)numberOfKeysInAllGroups;

- (BOOL)getKey:(NSString **)keyPtr
    collection:(NSString **)collectionPtr
       atIndex:(NSUInteger)index
       inGroup:(NSString *)group;

- (BOOL)getGroup:(NSString **)groupPtr
           index:(NSUInteger *)indexPtr
          forKey:(NSString *)key
    inCollection:(NSString *)collection;

- (void)enumerateKeysInGroup:(NSString *)group
                  usingBlock:(void (^)(NSString *collection, NSString *key, NSUInteger index, BOOL *stop))block;

- (id)objectAtIndex:(NSUInteger)index inGroup:(NSString *)group;

For the full API, please see YapDatabaseViewTransaction.h

Animating updates in TableViews & CollectionViews

YapDatabase simplifies many aspects of database development. It provides a simple 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 & views.

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.

This is where YapDatabase really shines. Views were designed to be the data source for UI components. Inclusive in this design is a simple mechanism that makes it dead-simple to animate changes to tableViews and collectionViews.

Here's an overview:

  • You create a view to filter, group & sort your data, however you might want to display it in the UI.
  • The rest of the app doesn't have to know anything about your view. It simply updates the data in the database as needed. (This is important, as your UI changes won't affect your backend code.)
  • The view translates any changes that are made (per transaction) into a list of changes that are easily passed to a tableView or collectionView for animations.

There are 2 features of YapDatabase that make all this possible (and make it dead simple to keep the user interface in-sync with the data layer).

  1. Long-Lived Read Transactions
  2. YapDatabaseModifiedNotification

Each of these technologies has its own wiki article. For more information, you are encouraged to read the corresponding articles, linked above. For this article, we're just going to give an overview of the technologies as they apply to views.

Long-Lived Read Transactions

From an abstract perspective, it's easy to think in terms of transactions. We execute a read-write transaction and make some changes to the database. And after the transaction is complete we know that future transactions will see the changes. But the problem is, we don't always think in terms of transactions. This is especially true on the main thread. Consider the following code:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    __block id onSaleItem = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        onSaleItem = [[transaction ext:@"view"] objectAtIndex:indexPath.row inGroup:@"sales"];
    }];
    
    // configure and return cell...
}

At first glance, this code looks correct. In fact, this is the natural and recommended way to write this code. But what about in terms of transactions? What happens if we execute a read-write transaction on a background thread and remove a bunch of sales items? And meanwhile the main thread is chugging away, populating the tableView or scrolling the tableView, and invoking the above dataSource method?

The answer is that things might get out-of-sync. At least temporarily. View controllers such as tableViews and collectionViews require a stable data source. The underlying data needs to remain in a consistent state, until the main thread is able to update the view controller and underlying data in a synchronized fashion. And if the UI is busy, this may be several run loop cycles away.

This is where long-lived read-only transactions come in.

A read-only transaction represents an immutable snapshot-in-time of the database. But the block-based transaction architecture limits the duration of the transaction, and thus limits the duration of the snapshot. Long-lived transactions allow you to "bookmark" a snapshot of the database, and ensure that all future read-only transactions use the previously "bookmarked" snapshot. Furthermore, the long-lived architecture allows you to move your "bookmarked" snapshot forward in time in a single atomic operation.

The architecture was designed to make it easy to use YapDatabase without having to worry about asynchronous read-write issues. Your main thread can move from one steady-state to another. And the code-block above will work just fine, without worrying about transaction issues.

YapDatabaseModifiedNotification

Everytime a read-write transaction makes modifications to the database, a YapDatabaseModifiedNotification is posted to the main thread. This notification is designed to help you keep the UI in-sync with the database. It contains all the juicy details on what changes were made to the database. In fact, there are API's that allow you to use the notification to find out if certain keys were changed. But views make the notification even sweeter.

Using a YapDatabaseModifiedNotification, you can ask the view for an exact list of changes, from the perspective of the view. For example, "index 5 moved to index 2" or "index 12 was deleted".

If you're familiar with Core Data and NSFetchedResultsController, this is very similar.

Full animation example

- (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, our view sorts items in a bookstore by selling rank.
    // Each book is grouped by its genre within the bookstore.
    // We only want to display a subset of genres (not every single genre).
    
    NSArray *groups = @[ @"fantasy", @"sci-fi", @"horror"];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"salesRank"];
    
    // We can do all kinds of cool stuff with the mappings object.
    // For example, we could say we only want to display the top 20 in each genre.
    // This will be covered later.
    // 
    // 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)numberOfSectionsInTableView:(UITableView *)sender
{
    return [mappings numberOfSections];
}

- (NSInteger)tableView:(UITableView *)sender numberOfRowsInSection:(NSInteger)section
{
    return [mappings numberOfItemsInSection:section];
}

- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    __block Book *book = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {

        book = [[transaction extension:@"salesRank"] objectAtIndexPath:indexPath withMappings:mappings];
    }];

    return [self cellForBook:book];
}

- (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.
    // If the UI is a bit backed up, I may jump multiple commits.
    
    NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];

    // Process the notification(s),
    // and get the change-set(s) as applies to my view and mappings configuration.

    NSArray *sectionChanges = nil;
    NSArray *rowChanges = nil;
    
    [[databaseConnection ext:@"salesRank"] getSectionChanges:&sectionChanges
                                                  rowChanges:&rowChanges
                                            forNotifications:notifications
                                                withMappings:mappings];
    
    // No need to update mappings.
    // The above method did it automatically.
    
    if ([sectionChanges count] == 0 & [rowChanges count] == 0)
    {
        // Nothing has changed that affects our tableView
        return;
    }

    // Familiar with NSFetchedResultsController?
    // Then this should look pretty familiar

    [self.tableView beginUpdates];
	
    for (YapDatabaseViewSectionChange *sectionChange in sectionChanges)
    {
        switch (sectionChange.type)
        {
            case YapDatabaseViewChangeDelete :
            {
                [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionChange.index]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
                break;
            }
            case YapDatabaseViewChangeInsert :
            {
                [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionChange.index]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
                break;
            }
        }
    }
    
    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 moveRowAtIndexPath:rowChange.indexPath toIndexPath:rowChange.newIndexPath];
                break;
            }
            case YapDatabaseViewChangeUpdate :
            {
                [self.tableView reloadRowsAtIndexPaths:@[ rowChange.indexPath ]
                                      withRowAnimation:UITableViewRowAnimationNone];
                break;
            }
        }
    }

    [self.tableView endUpdates];
}

Re-Configuring a view

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

Yup, it sure is.

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

NSString *versionTag = @"2"; // Increment me when you change the groupingBlock and/or sortingBlock
YapDatabaseView *databaseView =
    [[YapDatabaseView alloc] initWithGrouping:grouping
                                      sorting:sorting
                                   versionTag:versionTag]; // <-- versioning your view

Or, if you want to update an existing view:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){

    [[transaction ext:viewName] setGrouping:grouping sorting:sorting versionTag:versionTag];

}];

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. [NSString localizedCompare]) But what if the user changes their language, and relaunches our app?

int groupingVersion = 1;
int sortingVersion = 1;
NSString *locale = [[NSLocale currentLocale] localeIdentifier];

NSString *versionTag = [NSString stringWithFormat:@"%d-%d-%@", groupingVersion, sortingVersion, locale];

And so if you relaunch the app, and it finds itself in a different locale, the view will automatically regroup / resort itself.

On-the-fly views

This is explained in much more detail in the Extensions article. But it's worth briefly mentioning here:

You can register & unregister extensions at any time, even while you're actively using the database.

This applies to views as well. (Since a view is an extension.) So perhaps you have a rarely used viewController. And this RarelyUsedViewController depends on a specific YapDatabaseView instance. But RarelyUsedViewController is the only thing in your app that uses the specific YapDatabaseView... You might consider bringing the YapDatabaseView up on-the-fly. And then dropping it when the RarelyUsedViewController gets deallocated. Not a problem at all.

A Deeper Understanding

In order to get a deeper understanding of how YapDatabaseView works, and how it integrates with YapDatabase's core architecture, I'm going to walk through a few operations, and reveal what the sqlite table actually looks like at each step.

Let's start with an empty database, and then add a few objects to it:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){

    [transaction setObject:post1 forKey:@"bbt" inCollection:@"posts"];
    [transaction setObject:post2 forKey:@"xqy" inCollection:@"posts"];
    [transaction setObject:post3 forKey:@"pxy" inCollection:@"posts"];

    [transaction setObject:blue forKey:@"bgColor" inCollection:@"prefs"];
}];

Now the database has 3 Post objects, and an item for user preferences. So let's see what it looks like.

(I'm going to attempt to show the SQL table(s), but I'm going to use NSDictionary style markup to show them.)

// The sqlite database (in NSDictionary style markup):
@{
    @"database": @{ @"posts": @{ @"xqy": <post2>,
                                 @"bbt": <post1>,
                                 @"pxy": <post3>
                               },
                    @"prefs": @{ @"bgColor": <blue>
                               }
                  },
    @"yap" : @{ }
}

So the sqlite database has 2 tables. One table is named "database", which stores all the objects. And another which is named "yap", which is private, and will be discussed shortly.

By the way, you're encouraged to inspect the sqlite database file that YapDatabase creates. It's a great learning tool. You can use any sqlite file inspector to do so. There are several out there. I like this one.

And so, after the read-write transaction above, you can fetch a post like this:

[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
    Post *post = [transaction objectForKey:@"xqy" inCollection:@"posts"];
}];

Next we'll create a YapDatabaseView instance with code that looks something like this:

YapDatabaseViewGrouping *grouping = [YapDatabaseViewGrouping withObjectBlock:
    ^NSString *(YapDatabaseReadTransaction *transaction,
                NSString *collection, NSString *key, id object)
{
    if ([object isKindOfClass:[Post class]])
        return @"posts_all";

    return nil; // exclude from view
};


YapDatabaseViewSorting *sorting =
    ^(YapDatabaseReadTransaction *transaction, NSString *group,
      NSString *collection1, NSString *key1, id obj1,
      NSString *collection2, NSString *key2, id obj2)
{
    return [(Post *)obj1 comparePostsByTimestamp:(Post *)obj2];
};

YapDatabaseView *view =
    [[YapDatabaseView alloc] initWithGrouping:grouping
                                      sorting:sorting
                                   versionTag:@"theDuckSaysQuack"];

[database registerExtension:view withName:@"all"];

Invoking the synchronous registerExtension:withName method (as we did above) is the equivalent of executing a readWriteTransaction. (There are also async versions of the method.)

So after the view is registered, the database will look something like this:

// The sqlite database (in NSDictionary style markup):
@{
    @"database": @{ @"posts": @{ @"xqy": <post2>,
                                 @"bbt": <post1>,
                                 @"pxy": <post3>
                               },
                    @"prefs": @{ @"bgColor": <blue>
                               }
                  },
    @"yap" : @{ @"view_all": @{ @"versionTag": @"theDuckSaysQuack"
                              }
              },
    @"view_all" : @{ @"posts_all" : @[ ("posts","bbt"),
                                       ("posts","xqy"),
                                       ("posts","pxy")
                                     ]
                   }
}

So basically, the YapDatabaseView created another table within the sqlite database. The table name is "view_all", which is a combination of the extension type (YapDatabaseView), and the name the extension was registered with ("all").

When the registerExtension method ran, it enumerated all the posts, and invoked the groupingBlock & sortingBlock in order to calculate the ordered array. Then it stored the array to the database, within its designated sqlite table (view_all).

(Keep in the back of your mind that I'm glossing over a lot of details here. The on-disk structure of a view is a lot more advanced. It doesn't actually store one big array. It actually splits the array into pages, and stores each individual page. And it actually stores pages of rowids (int64_t), as opposed to collection/key tuples. But we can ignore these details temporarily in order to focus on some primary concepts.)

Now we quit the app and relaunch. And upon relaunching the app, we run the code that initializes our YapDatabase instance, and we also re-run the same code above in order to create and register our YapDatabaseView instance. So does the YapDatabaseView instance have to do anything?

The answer is NO. When we re-register the YapDatabaseView instance, we tell it that the versionTag is "theDuckSaysQuack". (The versionTag is used to signal whether or not the groupingBlock and/or sortingBlock has changed since the last run.) And so the view can look in the database and compare the new versionTag with the previous versionTag. And it will see that they are the same, and so the view has to do nothing. It realizes that it's already ready to go.

Next we execute a readWriteTransaction and add a new post:

[databaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction){

    [transaction setObject:post4 forKey:@"avz" inCollection:@"posts"];
}];

Remember that a readWrite transaction is atomic. So if you were to add 5 posts in a single transaction, all 5 would be added in one atomic operation. The YapDatabaseView is also a part of the transaction. And when you add a new post, the YapDatabaseView gets to participate in the atomic transaction. And it will automatically update its own tables to match the changes you're making.

So once the readWrite transaction is committed, the database will look something like this:

// The sqlite database (in NSDictionary style markup):
@{
    @"database": @{ @"posts": @{ @"xqy": <post2>,
                                 @"bbt": <post1>,
                                 @"pxy": <post3>,
                                 @"avz": <post4>
                               },
                    @"prefs": @{ @"bgColor": <blue>
                               }
                  },
    @"yap" : @{ @"view_all": @{ @"versionTag": @"theDuckSaysQuack"
                              }
              },
    @"view_all" : @{ @"posts_all" : @[ ("posts","bbt"),
                                       ("posts","xqy"),
                                       ("posts","pxy"),
                                       ("posts","avz")
                                     ]
                   }
}

Notice that post4 was added to the "database" table, and the "view_all" table was automatically updated as well.

Mappings

The YapDatabaseViewMappings class assists in "mapping" from a 'group-in-a-view' to 'section-in-a-table'. You might think of it as the glue between your tableView/collectionView and the YapDatabaseView instance. In truth it's a lot more powerful than glue, and provides a bunch of useful features including:

  • Dynamic vs Static sections
  • Range Options
  • Cell-Drawing-Dependency support
  • Reversing
  • Mapping & Caching

But we'll get to that in a moment. First let's go over what mappings are, and why they exist.

Understanding Mappings

The collection/key/value component of the database is unordered. You can think of it as similar to an NSDictionary of NSDictionaries:

// If you imagine the database like this NSDictionary of NSDictionaries:
@{
    @"movies": @{ @"abc123": <NFMovie:Goldfinger>,
                  @"def456": <NFMovie:Pretty In Pink>,
                  @"xyz123": <NFMovie:GoldenEye>, ...
                },
    @"actors": @{ @"abc123": <NFActor:Pierce Brosnan>,
                  @"xyz789": <NFActor:Daniel Craig>,
                  @"klm456": <NFActor:Sean Connery>, ...
                },
}

// Then it matches your mental model of how lookups essentially work:
NFMovie *goldfinger = [transaction objectForKey:@"abc123" inCollection:@"movies"];

A YapDatabaseView allows you to sort the database (or a subset of it), however you want. You can think of it as similar to an NSDictionary of Arrays:

// If you imagine the view like this NSDictionary of NSArrays:
@{
    @"bond movies": @[ <@"movies", @"abc123">,    // <NFMovie:Goldfinger> (sorting chronologically)
                       <@"movies", @"xyz123"> ],  // <NFMovie:GoldenEye>

    @"80s movies" : @[ <@"movies", @"def456"> ],  // <NFMovie:Pretty In Pink>
    
    @"bond actors": @[ <@"actors", @"xyz789">,    // <NFActor:Daniel Craig> (sorting alphabetically)
                       <@"actors", @"abc123">,    // <NFActor:Pierce Brosnan>
                       <@"actors", @"klm456"> ]   // <NFActor:Sean Connery>, ...
}

// This should match your mental model of how lookups in a view work:
NFMovie *goldfinger = [[transaction ext:@"myView"] objectAtIndex:0 inGroup:@"bond movies"];

There is one very important thing to notice here. A view is similar to a "dictionary of arrays" and NOT an "array of arrays".

But I need an "array of arrays" for my tableView/collectionView! What's going on?

This is where mappings come in. They actually make views even more flexible. Let's look at an example:

Say we want to display a tableView in our new JamesBondViewController. Section 0 of our tableView is to be the list of all Bond movies, sorted chronologically. And section 1 is to be the list of Bond actors, sorted alphabetically.

You'll notice this doesn't quite match what we have in our existing view. There's also the "80s movies" category that we don't care about in this particular viewController. No problem. Here's how we solve it, using the existing view:

YapDatabaseViewMappings *mappings =
  [YapDatabaseViewMappings mappingsWithGroups:@[ @"bond movies", @"bond actors" ]
                                         view:@"myView"];

Here we've specified that we're only interested in 2 groups. And we've specified the order that we want them in. So "bond movies" comes before "bond actors".

And thus mappings gives us an explicit "mapping" from unordered groups in a view, to a specific order of sections in our tableView.

For many cases, this basic mapping functionality is all you'll need. But mappings are secretly powerful. For example, a single line of code could allow you to toggle between chronological order (oldest to newest), and reverse chronological order (newest to oldest):

[mappings setIsReversed:YES forGroup:@"bond movies"]; // Boom

Wait, wait... What? How the heck does that work?

The view itself doesn't change at all. (Which means no disk IO.) Only the mappings change. It works seamlessly for you because the mappings layer can sit between your code and the actual view instance. Like so:

// Our mappings is configured to reverse this section.
// So we're going to get goldeneye (not goldfinger)
NFMovie *goldeneye = [[transaction ext:@"myView"] objectAtRow:0 inSection:0 withMappings:mappings];

// Basically, the mappings layer automatically transformed the above method call into:
// [[transaction ext:@"myView"] objectAtIndex:1 inGroup:@"bond movies"];

And the mappings layer can perform a bunch of other tricks. Which are described below.

Managing Mappings

You can create a YapDatabaseViewMappings instance using either a static list of groups, or a dynamic list.

In the example above, we used a static list of groups because we knew exactly what we wanted ahead of time:

YapDatabaseViewMappings *mappings =
  [YapDatabaseViewMappings mappingsWithGroups:@[ @"bond movies", @"bond actors" ]
                                         view:@"myView"];

We can also create mappings using a dynamic list of groups. For example, say we're looking to create a tableView that displays all product inventory, grouped by department. We already have a YapDatabaseView that creates groups for InventoryItem.department, and sorts the items by InventoryItem.displayName. But the problem here is that we don't know what all the departments are ahead of time. So we use a dynamic list:

YapDatabaseViewMappings *mappings =
  [[YapDatabaseViewMappings alloc] initWithGroupFilterBlock:^(NSString *group, YapDatabaseReadTransaction *transaction){

      return YES; // include all departments

  } sortBlock:^(NSString *group1, NSString *group2, YapDatabaseReadTransaction *transaction){

      return [group1 compare:group2]; // sorted by department name

  } view:@"myView"];

After you've created the mappings, you need to initialize them once. This should look something like this:

- (void)viewDidLoad
{
    // ...

    // Freeze our databaseConnection on the current commit.
    // This gives us a snapshot-in-time of the database,
    // and thus a stable data source for our UI thread.
    [databaseConnection beginLongLivedReadTransaction];
    
    // Initialize our mappings.
    // Note that we do this after we've started our database longLived transaction.
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
    
        // Calling this for the first time will initialize the mappings,
        // and will allow mappings to cache certain information
        // such as the counts for each section.
        [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];

    // ...
}

Now let's look at what we need to do when we get a YapDatabaseModifiedNotification:

- (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.
    // If the UI is a bit backed up, I may jump multiple commits.

    NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];

    // If the view isn't visible, we might decide to skip the UI animation stuff.
    if ([self viewIsNotVisible])
    {
        // Since we moved our databaseConnection to a new commit,
        // we need to update the mappings too.
        [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
            [mappings updateWithTransaction:transaction];
        }];
        return;
    }

    // Process the notification(s),
    // and get the change-set(s) as applies to my view and mappings configuration.
    // 
    // Note: the getSectionChanges:rowChanges:forNotifications:withMappings: method
    // automatically invokes the equivalent of [mappings updateWithTransaction:] for you.

    NSArray *sectionChanges = nil;
    NSArray *rowChanges = nil;

    [[databaseConnection ext:@"salesRank"] getSectionChanges:&sectionChanges
                                                  rowChanges:&rowChanges
                                            forNotifications:notifications
                                                withMappings:mappings];

    // No need to update mappings.
    // The above method did it automatically.

    if ([sectionChanges count] == 0 & [rowChanges count] == 0)
    {
        // Nothing has changed that affects our tableView
        return;
    }

    // Animate tableView updates ...
    // See code samples above.
}

It's important to understand the following:

Mappings are implicitly tied to a databaseConnection's longLivedReadTransaction. That is, when you invoke [databaseConnection beginLongLivedReadTransaction] you are freezing the connection on a particular commit (a snapshot of the database at that point in time). Mappings MUST always be on the same snapshot as its corresponding databaseConnection. Which means EVERYTIME you move databaseConnection to a newer commit, you MUST also update the mappings.

If you fail to do this, you'll eventually get an exception that looks like this:

YapDatabaseViewConnection: Throwing exception: ViewConnection[0x10d56cc80, RegisteredName=order] was asked for changes, but given mismatched mappings & notifications.

Here's an example of some bad code that would cause this exception:

- (void)yapDatabaseModified:(NSNotification *)notification
{
    // Move my databaseConnection to the most recent commit.
    NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];

    // Check to see if I need to do anything
    if ( ! [[databaseConnection ext:@"MyView"] hasChangesForNotifications:notifications])
    {
        // Oh snap !
        // I moved my databaseConnection to a new commit / snapshot,
        // but I failed to move my mappings. That's BAD.
        
        return; // Danger ! There's a crash in my immediate future !
    }

    // Normal code here...
}

But if the view didn't change, then why do I need to update the mappings?

Because the method getSectionChanges:rowChanges:forNotifications:withMappings: can only guarantee it gives you the right change-set if it's in-sync with the connection. Otherwise all bets are off, the app crashes in [tableView endUpdates], and all hell breaks loose.

But fear not. There's a shortcut to quickly update mappings if you don't need the UI animation info. Here's how easy it is:

- (void)yapDatabaseModified:(NSNotification *)notification
{
    // Move my databaseConnection to the most recent commit.
    NSArray *notifications = [databaseConnection beginLongLivedReadTransaction];

    // Check to see if I need to do anything
    if ( ! [[databaseConnection ext:@"MyView"] hasChangesForNotifications:notifications])
    {
        // Sweet.
        // Just update my mappings so it's on the same snapshot as my connection, and I'm done.
        [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
            [mappings updateWithTransaction:transaction];
        }];
        return; // Good to go :)
    }

    // Normal code here...
}

Dynamic Sections

If a section is empty, should it disappear from the tableView / collectionView? This is obviously a question for you to decide. Either way, mappings can help.

A "dynamic" section is how mappings refers to a section that automatically disappears when it becomes empty. If the section later becomes non-empty, it reappears.

For example: You're making a chat application, and you're designing the tableView for the roster. You have 3 sections:

  • available
  • away
  • offline

You want the "available" and "away" sections to automatically get removed if they don't have any users in them. But you've decided you want the "offline" section to always be visible. In mappings terminology, the "available" and "away" sections are "dynamic", and the "offline" section is "static". Here's how you would configure this:

- (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];
    
    // Setup and configure our mappings
    NSArray *groups = @[@"available", @"away", @"offline"];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"roster"];

    [mappings setIsDynamicSection:YES forGroup:@"available"];
    [mappings setIsDynamicSection:YES forGroup:@"away"];
    [mappings setIsDynamicSection:NO  forGroup:@"offline"];
      
    // Initialize the mappings.
    // This will allow the mappings object to get the counts per group.
    [databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
        // One-time initialization
        [mappings updateWithTransaction:transaction];
    }];
    
    // 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];
}

Now the tricky part about dynamic sections is that section numbers change. That is, what group does section number zero refer to?

The answer depends on the current state of the "available" and "away" groups. If there are one or more available users, then section zero refers to the "available" group. If not, and there are one or more away users, then section zero refers to the "away" group. Otherwise, it refers to the "offline" group.

It's not exactly rocket science. But it gets annoying when you have to copy-and-paste the same code block in 20 different places. Luckily, the mappings object can map it for you!

- (NSInteger)numberOfSectionsInTableView:(UITableView *)sender
{
    // Could be 1, 2, or 3. Mappings can tell us.
    // (plus it has this information cached for us)
    return [mappings numberOfSections];
}

- (NSInteger)tableView:(UITableView *)sender numberOfRowsInSection:(NSInteger)section
{
    // We can use the cached information in the mappings object.
    return [mappings numberOfItemsInSection:section];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // The section is which group?
    // Mappings can tell us...
    NSString *group = [mappings groupForSection:indexPath.section];

    __block id object = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        
        // In fact, there is a category with useful methods for us: YapDatabaseViewTransaction (Mappings)
        // So we often don't have to even think about these things...
        object = [[transaction ext:@"view"] objectAtIndexPath:indexPath withMappings:mappings];
    }];

    // configure and return cell...
}

There are a variety of other methods in YapDatabaseViewMappings that can assist you when using dynamic sections. See file YapDatabaseViewMappings.h for more information.

Fixed Ranges

Fixed ranges are the equivalent of using a LIMIT & OFFSET in an SQL query. (Or using a limit and offset in a NSFetchedResultsController.)

For example: You have a view which sorts items by "sales rank". That is, the best selling items are at index zero, and the worst selling items are at the end. You want to display the top 20 best selling items in a tableView. Here's all you have to do:

- (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];
    
    // Setup and configure our mappings
    NSArray *groups = @[@"books"];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"salesRank"];

    YapDatabaseViewRangeOptions *rangeOptions =
        [YapDatabaseViewRangeOptions fixedRangeWithLength:20 offset:0 from:YapDatabaseViewBeginning];
    [mappings setRangeOptions:rangeOptions forGroup:@"books"];
      
    // Initialize the mappings.
    // This will allow the mappings object to get the counts per group.
    [databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
        // One-time initialization
        [mappings updateWithTransaction:transaction];
    }];
    
    // 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];
}

That's all there is to it. Mappings will automatically handle the rest. This includes edge cases, such as when there are less than 20 items available at first. But more importantly, it includes live updates. That is, the boiler-plate code for animating updates will automatically keep your tableView just the way you want it.

That is, if a new best-seller gets inserted into the number 5 spot, then the row changes you get handed during a YapDatabaseModifiedNotification will include the insert of the new item at number 5, as well as the delete of the item that was previously number 20, but has now fallen off the top 20 list.

In other words, everything just works.

Flexible Ranges

A fixed range isn't always what you want. Sometimes you need something a little more flexible...

A flexible range is similar to a fixed range, but is allowed to grow and shrink automatically based on its configuration.

Consider Apple's Messages app:

When you go into a conversation (tap on a persons name),
the messages app starts by displaying the most recent 50 messages (with the most recent at bottom).
Although there might be thousands of old messages between you and the other person,
only 50 are in the view to begin with. (This keeps the UI snappy.)
As you send and/or receive messages within the view, the length will grow.
And similarly, if you manually delete messages, the length will shrink.

You can achieve something like this in SQL using additional WHERE clauses. That is, you'd have to first figure out the timestamp for the message that's 50 back from the latest message. And then you'd write the SQL query with an additional "WHERE timestamp >= minTimestamp" clause.

A flexible range is similar to this. But easier to deal with. You can simply specify what you want (in terms of numbers), and let it handle the rest. To get the functionality of Apple's Messages app:

- (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];
    
    // Setup and configure our mappings
    NSArray *groups = @[userId];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"conversations"];

    YapDatabaseViewRangeOptions *rangeOptions =
        [YapDatabaseViewRangeOptions flexibleRangeWithLength:50 offset:0 from:YapDatabaseViewEnd];
    
    // Let's also set a max, so that if the conversation grows to some obnoxious length,
    // our UI doesn't get bogged down.
    rangeOptions.maxLength = 300;    

    // We can set a min length too.
    // So if the user goes and manually deletes most of the messages,
    // we can automatically "move backwards" and display older messages.
    rangeOptions.minLength = 20;

    [mappings setRangeOptions:rangeOptions forGroup:userId];
    
    // Initialize the mappings.
    // This will allow the mappings object to get the counts per group.
    [databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
        // One-time initialization
        [mappings updateWithTransaction:transaction];
    }];
    
    // 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];
}

There are a few interesting things to notice in the code sample.

First, the flexible range is "pinned" to the end. That is, it is configured to have offset=0 from the end.

Both fixed and flexible ranges allow you to pin the range to the beginning or end. Pinning a range to the end is convenient because it allows you to specify "what you want" without getting bogged down with the math.

This gets very helpful if want to have a background cleanup task that deletes old items from the database on some schedule. It can run without ever interfering with the UI. Even if it deletes hundreds of old items, it won't matter because you've pinned your range to the end. So the count can drop from 1,000 to 200, but your range stays put without fuss thanks to pinning.

Second, flexible ranges allow you to specify max and min lengths. If the range length grows to surpass the max length, then it will automatically start truncating items from the side opposite its pinned side. (That is, it will start acting similar to a fixed range.) And if the range length drops below the min length, then the range will attempt to automatically grow to have at least min length.

Again, you can specify "what you want" without having to code all the edge cases.

A note about offsets in flexible ranges:

Consider the following code:

YapDatabaseViewRangeOptions *rangeOptions =
    [YapDatabaseViewRangeOptions flexibleRangeWithLength:50 offset:25 from:YapDatabaseViewEnd];
[mappings setRangeOptions:rangeOptions forGroup:@"books"];

The range is pinned to the end, but it has an offset of 25. So if there are 100 items, the range displays items 25-75. This is as expected. But what happens if an item it added to the end, such that there are now 101 items?

Just as a flexible range can grow & shrink in length, its offset can grow and shrink. So a flexible range will keep the same items within its range by increasing the offset to 26. From a strict database perspective, this may seem a bit odd at first. But from a user interface perspective, its what you'd want to keep the UI consistent. The UI is displaying a certain subset of cells. We want it to continue displaying those cells even if items outside our range get added / deleted. In other words, the range stays put where you told it to be, which means the UI stays put where you told it to be.

Cell Drawing Dependencies

If you've ever encountered this problem before, this one will make you smile.

There are times when the drawing of cell B is dependent upon cell A. That is, in order to draw certain components of a cell, we need to know something about the neighboring cell.

Consider Apple's Messages app:

If more than a certain amount of time has elapsed between a message and the previous message,
then a timestamp is drawn. The timestamp is actually drawn at the top of a cell.
So cell-B would draw a timestamp at the top of its cell if cell-A represented a message
that was sent/received say 3 hours prior to cell-B's message.

Another example can be found in Skype's app:

The first message from a particular user is displayed with the name and avatar.
Subsequent messages from the same user (without other messages in between)
are displayed with the same background color, but don't display the name or avatar.
In this manner, multiple consecutive messages from the same user get displayed cleanly
without taking up too much space.

Sometimes these inter-cell-dependencies aren't a problem. That is, if you follow these rules:

  1. Messages cannot be deleted
  2. New messages always get appended to the end

Violate either of these rules, and you may need to redraw multiple cells to keep the UI consistent. And with the new wave of apps that support syncing between multiple devices, these rules become harder to follow. This is where "cell drawing dependencies" come in to save the day.

You can actually tell the mappings object that you have such dependencies. And it will automatically emit proper row updates to make sure your UI stays consistent:

- (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];
    
    // Setup and configure our mappings
    NSArray *groups = @[currentUserId];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"messages"];

    // Our timestamp calculations depend upon the previous cell
    [mappings setCellDrawingDependencyForNeighboringCellWithOffset:-1 forGroup:currentUserId];
    
    // Initialize the mappings.
    // This will allow the mappings object to get the counts per group.
    [databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
        // One-time initialization
        [mappings updateWithTransaction:transaction];
    }];
    
    // 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];
}

For example, say you have items in the table like this:

  • You there? (5 PM)
  • Hi (8 PM) [timestamp header]
  • You done coding yet?? (8:01 PM)

Then the user manually deletes the "Hi" message. When your boiler plate update code requests the list of row changes, it will include both the row delete for the "Hi" message, and also a row update for the "You done coding yet??" message. So your tableView will automatically update to become this (for free!):

  • You there? (5 PM)
  • You done coding yet?? (8:01 PM) [timestamp header]

Reverse

If you need to display items from a view, but in the opposite order, then the reverse option can help.

For example: You have a view which sorts items by "sales rank". That is, the best selling items are at index zero, and the worst selling items are at the end. You want to display the 20 worst selling items in a tableView. (Candidates for the clearance section to make room for new inventory.) BUT, you want the #1 worst selling item to be at the TOP of the tableView. This is actually the opposite order from how they appear in the "sales rank" view.

You many notice this example is similar to the one from the fixed ranges section.

Here's all you need to do:

- (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];
    
    // Setup and configure our mappings
    NSArray *groups = @[@"books"];
    mappings = [[YapDatabaseViewMappings alloc] initWithGroups:groups view:@"salesRank"];

    YapDatabaseViewRangeOptions *rangeOptions =
        [YapDatabaseViewRangeOptions fixedRangeWithLength:20 offset:0 from:YapDatabaseViewEnd];
    [mappings setRangeOptions:rangeOptions forGroup:@"books"];
    
    [mappings setIsReversed:YES forGroup:@"books"];

    // Initialize the mappings.
    // This will allow the mappings object to get the counts per group.
    [databaseConnection readWithBlock:(YapDatabaseReadTransaction *transaction){
        // One-time initialization
        [mappings updateWithTransaction:transaction];
    }];
    
    // 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];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // We're looking at the end of the view, and in reverse...
    // So what is the actual view index?
    // This is getting confusing. Good thing mappings can translate all this stuff for us.
    NSUInteger ydbViewIndex = [mappings indexForRow:indexPath.row inSection:indexPath.section];

    __block id object = nil;
    [databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction){
        
        // In fact, there is a category with useful methods for us: YapDatabaseViewTransaction (Mappings)
        // So we often don't have to even think about these things...
        object = [[transaction ext:@"salesRank"] objectAtIndexPath:indexPath withMappings:mappings];
    }];

    // configure and return cell...
}

One of the nice things about the reverse option is that order matters. This allows you to configure your mappings in the way that makes the most sense to you. For example:

// The following code
- (void)viewDidLoad
{
    // ...

    YapDatabaseViewRangeOptions *rangeOptions =
        [YapDatabaseViewRangeOptions fixedRangeWithLength:20 offset:0 from:YapDatabaseViewEnd]; // <- 1st
    [mappings setRangeOptions:rangeOptions forGroup:@"books"];
    
    [mappings setIsReversed:YES forGroup:@"books"]; // <- 2nd

    // ...
}

// IS EQUIVALENT TO:

- (void)viewDidLoad
{
    // ...

    [mappings setIsReversed:YES forGroup:@"books"]; // <- 1st

    YapDatabaseViewRangeOptions *rangeOptions =
        [YapDatabaseViewRangeOptions fixedRangeWithLength:20 offset:0 from:YapDatabaseViewBeginning]; // <- 2nd
    [mappings setRangeOptions:rangeOptions forGroup:@"books"];

    // ...
}

Once you set a group to be reversed, you are free to configure it (from that point forward) as if it was reversed. Both configuration techniques are equivalent. It's simply a matter of how you visualize it.

  1. I visualize getting the last 20 items from the view, and then reversing them.
  2. I visualize reversing the entire view, and then getting the first 20.

So you're free to configure mappings in the same manner that you visualize it.

You can’t perform that action at this time.