Querying

Layer provides a flexible and expressive interface to query through conversations, messages, and announcements.

Note

Queries execute on the local database, and will only return results for conversations and messages where the authenticated user is a participant. Queries will not return empty conversations — a conversation must have at least one message in it in order to be written to the local database.

To demonstrate a simple example, the following queries Layer for the latest 20 messages in the given conversation:

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];
query.predicate = [LYRPredicate predicateWithProperty:@"conversation" predicateOperator:LYRPredicateOperatorIsEqualTo value:self.conversation];
query.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];
query.limit = 20;
query.offset = 0;

NSError *error;
NSOrderedSet *messages = [self.client executeQuery:query error:&error];
if (!error) {
    NSLog(@"%tu messages in conversation", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Constructing a query

An instance of an LYRQuery object is initialized with a Class object representing the class upon which the query will be performed. Querying is available on classes that conform to the LYRQueryable protocol. Currently, LYRConversation, LYRMessage, and LYRAnnouncement are the only classes which conform to the protocol.

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];

Filtering results

Queries can be filtered to only include results that match certain conditions. These constraints are expressed in terms of predicates that contain a public property (such as createdAt or isUnread), a predicate operator (such as “is equal to” or “is less than or equal to”), and a value to compare against.

The following predicate will constrain the query result to message objects that belong to the provided conversation:

query.predicate = [LYRPredicate predicateWithProperty:@"conversation" predicateOperator:LYRPredicateOperatorIsEqualTo value:conversationObject];

Properties that support querying are identified by the LYR_QUERYABLE_PROPERTY macro.

Sorting results

You can sort results by setting a value for the sortDescriptors property on LYRQuery objects. This value must be an array of NSSortDescriptor instances.

The following sort descriptor sorts results in ascending order based on the position property of LYRMessage objects:

query.sortDescriptors = @[
  [NSSortDescriptor] sortDescriptorWithKey:@"position" ascending:YES]
];

Limits and offsets

If you’re displaying results in pages (for example, if you have 100 conversations and want to split them into 5 pages containing 20 conversations each), you can also apply limit and offset values on queries. The limit configures the maximum number of objects to be returned. The offset configures the number of rows to skip in the results.

query.limit = 20;
query.offset = 0;

In the above example, every page would have a limit of 20. The first page would have an offset of 0; the second page would have an offset of 20, the third page would have an offset of 40, and so on.

Result types

Query results can be returned as object instances, object IDs, or as a count of objects matching the query.

// Fully realized objects
query.resultType = LYRQueryResultTypeObjects;

// Object identifiers
query.resultType = LYRQueryResultTypeIdentifiers;

// Count of objects
query.resultType = LYRQueryResultTypeCount;

The default type is LYRQueryResultTypeObjects.

Running a query

You can run a query by calling executeQuery:error: on LYRClient. The first argument is a LYRQuery instance; the second is a pointer to an NSError. If successful, the method will return an NSOrderedSet of objects which represent the results of the query. If an error occurs, the error pointer will be updated to an error describing why the query failed.

NSError *error;
NSOrderedSet *messages = [self.client executeQuery:query error:&error];
if (!error) {
    NSLog(@"%tu messages in conversation", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Additionally, LYRClient declares countForQuery:error: for queries with a resultType of LYRQueryResultTypeCount. This method returns an NSUInteger:

NSError *error;
NSUInteger countOfMessages = [self.client countForQuery:query error:&error];
if (!error) {
    NSLog(@"%tu messages in conversation", countOfMessages);
} else {
    NSLog(@"Query failed with error %@", error);
}

Query controller

On iOS, we provide LYRQueryController, which can be used to manage the results from a LYRQuery and easily feed it into a UITableView or UICollectionView. The query controller is conceptually similar to NSFetchedResultsController and provides the following functionality:

  • Executes the query and caches the result set
  • Notifies its delegate (see below) when objects in its result set change
  • Notifies its delegate when new objects are synced/created that match the query

The following demonstrates creating a LYRQueryController that can be used to display a list of LYRConversation objects in a UITableView:

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
LYRQueryController *queryController = [self.client queryControllerWithQuery:query error:nil];
queryController.delegate = self;
NSError *error;
BOOL success = [queryController execute:&error];
if (success) {
    NSLog(@"Query fetched %tu conversation objects", [queryController numberOfObjectsInSection:0]);
} else {
    NSLog(@"Query failed with error %@", error);
}

DataSource

UITableView

The numberOfObjectsInSection: method on the query controller returns the number of objects in the result set, and can be used for the UITableViewDataSource method:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.queryController numberOfObjectsInSection:section];
}

The objectAtIndexPath: method fetches an object from the result set, and can similarly be used for the UITableViewDataSource method:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    LYRConversation *conversation = [self.queryController objectAtIndexPath:indexPath];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"<CELL_IDENTIFIER>"];
    /**
     Configure cell for conversation
     */
    return cell;
}

UICollectionView

The numberOfObjectsInSection: method on the query controller returns the number of objects in the result set, and can be used for the UICollectionViewDataSource methods. Because of the various ways to utilize UICollectionView you may have your own implementation regarding items and sections. One common design pattern (and what we chose to use within our Atlas framework) is to represent each message as a separate section with a single cell, regardless of the number of message parts. However, you could also return a cell for each message part depending on your requirements.

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    // Each message is represented by one cell no matter how many parts it has.
    return 1;
}

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    // Each message is contained within it's own section.
    return [self.queryController numberOfObjectsInSection:0];
}

The objectAtIndexPath: method fetches an object from the result set, and can similarly be used for the UICollectionViewDataSource method:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    LYRMessage *message = [self.queryController objectAtIndexPath:indexPath];
    UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithIdentifier:@"<CELL_IDENTIFIER>" forIndexPath:indexPath];
    /**
     Configure cell for message
     */
    return cell;
}

Note

Currently there is only one section, at index 0.

Delegate

LYRQueryController declares the LYRQueryControllerDelegate protocol. LYRQueryController observes LYRClientObjectsDidChangeNotification to respond to changes to Layer model objects during synchronization. When changes occur to objects in the controller’s result, or new objects which match the controller’s query are created/synced, the controller will inform its delegate. The delegate will then be able to update its UI in response to these changes.

You can configure which changes will be emitted by the query controller using the updatableProperties property of LYRQueryController. By default, this property is set to nil which means all property changes will emit update notifications to the delegate. Defining specific properties may enhance performance by suppressing the delivery of uninteresting update notifications to the delegate and the subsequent reloading of table and collection view cells. For example, you may wish to limit the updatable properties to isSent if your UI does not render delivery and read receipts.

    self.queryController.updatableProperties = [NSSet setWithObjects:@"isSent", @"sentAt", nil];

UITableView

The following represents the ideal implementation of the LYRQueryControllerDelegate methods for a UITableViewController. This implementation will handle animating a UITableView in response to changes on Layer model objects:

- (void)queryControllerWillChangeContent:(LYRQueryController *)queryController
{
    [self.tableView beginUpdates];
}

- (void)queryController:(LYRQueryController *)controller
        didChangeObject:(id)object
            atIndexPath:(NSIndexPath *)indexPath
          forChangeType:(LYRQueryControllerChangeType)type
           newIndexPath:(NSIndexPath *)newIndexPath
{
    switch (type) {
        case LYRQueryControllerChangeTypeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
                                  withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case LYRQueryControllerChangeTypeUpdate:
            [self.tableView reloadRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case LYRQueryControllerChangeTypeMove:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
                                  withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case LYRQueryControllerChangeTypeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath]
                                  withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        default:
            break;
    }
}

- (void)queryControllerDidChangeContent:(LYRQueryController *)queryController
{
    [self.tableView endUpdates];
}

UICollectionView

The following represents an implementation of the LYRQueryControllerDelegate methods for a UIViewController that includes a UICollectionView. This implementation will handle animating a UICollectionView in response to changes on Layer model objects. Because UICollectionView uses the performBatchUpdates method instead of beginUpdates and endUpdates you will need to handle changes differently than with a UITableView:

- (void)queryControllerWillChangeContent:(LYRQueryController *)queryController
{
    // Not required for UICollectionView
}

- (void)queryController:(LYRQueryController *)controller
        didChangeObject:(id)object
            atIndexPath:(NSIndexPath *)indexPath
          forChangeType:(LYRQueryControllerChangeType)type
           newIndexPath:(NSIndexPath *)newIndexPath
{
    // DataSourceChange is an example of a class you could create to wrap each set of values into a single object.
    // Store these objects in an array to access later.
    [self.objectChanges addObject:[DataSourceChange changeObjectWithType:type targetIndexPath:newIndexPath currentIndexPath:indexPath]];
}

- (void)queryControllerDidChangeContent:(LYRQueryController *)queryController
{
    if (self.objectChanges.count == 0) {
        return;
    }

    [self.collectionView performBatchUpdates:^{
        for (DataSourceChange *change in objectChanges) {
            switch (change.type) {
                case LYRQueryControllerChangeTypeInsert:
                    [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:change.targetIndexPath.row]];
                    break;

                case LYRQueryControllerChangeTypeMove:
                    [self.collectionView moveSection:change.currentIndexPath.row toSection:change.targetIndexPath.row];
                    break;

                case LYRQueryControllerChangeTypeDelete:
                    [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:change.currentIndexPath.row]];
                    break;

                case LYRQueryControllerChangeTypeUpdate:
                    [self configureCell:[self.collectionView cellForItemAtIndexPath:change.currentIndexPath] atIndexPath:change.currentIndexPath];
                    break;

                default:
                    break;
            }
        }
    } completion:^(BOOL finished) {
        [self.objectChanges removeAllObjects];
    }];
}

- (void)configureCell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    // Configure your cell here.
}

Pagination

The LYRQueryController has a concept of limiting the result set with the help of the pagination window, it is used to improve the performance of the UI components that utilize the query controller as its datasource. When a pagination window is set, the query controller will expose a subset of total set of objects that match the query to the consumer.

The window is expressed as a signed integer, where a positive value indicates a window that originates from index 0 (or the “top” of the data set) and covers the specified number of objects. Whereas the negative value exposes the result subset originating from the last index (or the “bottom” of the data set), covering the specified number of objects.

// Sets up a query controller showing the last 25 messages.
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];
query.predicate = [LYRPredicate predicateWithProperty:@"conversation"
                                predicateOperator:LYRPredicateOperatorIsEqualTo
                                value:self.conversation];
query.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];
LYRQueryController *queryController = [self.client queryControllerWithQuery:query error:nil];
queryController.paginationWindow = -25;

This is a useful mechanism to drive the “Load More” functionality, where the window can be asynchronously expanded to optimally load additional objects into the collection. When expanding the window, the query controller has to be executed through the execute: or the executeWithCompletion: method.

Updatable Properties

This concept is also a performance enhancement measure, where the LYRQueryController limits the update event emission based on the properties’ names that cause the update events whenever their value changes. For example, a query controller that is setup to fetch LYRMessage objects might trigger a lot of UI refreshes due to the mutability of the message’s properties, where we’re only interested in an update when the isUnread changes. The updates events can be reduced by adding the "isUnread" to the updatableProperties set.

// Sets up a query controller showing all conversations,
// but limits the delegate callback execution to only when
// the last messages' `isUnread` flag changes.
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
LYRQueryController *queryController = [self.client queryControllerWithQuery:query error:nil];
queryController.updatableProperties = [NSSet setWithObjects:@"lastMessage.isUnread"];

Query examples

The following examples demostrate common queries that your app might need:

Fetching all conversations

// Fetches all LYRConversation objects
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];

NSError *error = nil;
NSOrderedSet *conversations = [self.client executeQuery:query error:&error];
if (conversations) {
    NSLog(@"%tu conversations", conversations.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Fetching a conversation with a particular ID

// Fetches conversation with a specific identifier
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
query.predicate = [LYRPredicate predicateWithProperty:@"identifier" predicateOperator:LYRPredicateOperatorIsEqualTo value:identifier];
NSError *error = nil;
LYRConversation *conversation = [[self.layerClient executeQuery:query error:&error] firstObject];

Fetching all conversations with a certain set of participants

// Fetches all conversations between the authenticated user and the supplied user
NSArray *participants = @[ self.client.authenticatedUser.userID, @"<USER_ID>" ];
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
query.predicate = [LYRPredicate predicateWithProperty:@"participants" predicateOperator:LYRPredicateOperatorIsEqualTo value:participants];

NSError *error = nil;
NSOrderedSet *conversations = [self.client executeQuery:query error:&error];
if (!error) {
    NSLog(@"%tu conversations with participants %@", conversations.count, participants);
} else {
    NSLog(@"Query failed with error %@", error);
}

Fetching all messages

// Fetches all LYRMessage objects
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];

NSError *error;
NSOrderedSet *messages = [self.client executeQuery:query error:&error];
if (messages) {
    NSLog(@"%tu messages", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Counting unread messages

// Fetches the count of all unread messages for the authenticated user
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];

// Messages must be unread
LYRPredicate *unreadPredicate =[LYRPredicate predicateWithProperty:@"isUnread" predicateOperator:LYRPredicateOperatorIsEqualTo value:@(YES)];

// Messages must not be sent by the authenticated user
LYRPredicate *userPredicate = [LYRPredicate predicateWithProperty:@"sender.userID" predicateOperator:LYRPredicateOperatorIsNotEqualTo value:self.client.authenticatedUser.userID];

query.predicate = [LYRCompoundPredicate compoundPredicateWithType:LYRCompoundPredicateTypeAnd subpredicates:@[unreadPredicate, userPredicate]];
query.resultType = LYRQueryResultTypeCount;
NSError *error = nil;
NSUInteger unreadMessageCount = [self.client countForQuery:query error:&error];

Fetching all messages in a particular conversation

// Fetches all messages for a given conversation
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];
query.predicate = [LYRPredicate predicateWithProperty:@"conversation" predicateOperator:LYRPredicateOperatorIsEqualTo value:self.conversation];
query.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"position" ascending:YES]];

NSError *error = nil;
NSOrderedSet *messages = [self.client executeQuery:query error:&error];
if (messages) {
    NSLog(@"%tu messages in conversation", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Fetching messages sent in the past week

// Fetches all messages sent in the last week
NSDate *lastWeek = [[NSDate date] dateByAddingTimeInterval:-60*60*24*7]; // One Week Ago
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];
query.predicate = [LYRPredicate predicateWithProperty:@"sentAt" predicateOperator:LYRPredicateOperatorIsGreaterThan value:lastWeek];

NSError *error = nil;
NSOrderedSet *messages = [self.client executeQuery:query error:&error];
if (messages) {
    NSLog(@"%tu messages in conversation", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Fetching messages containing PNGs

// Fetch all messages containing PNGs
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];
query.predicate = [LYRPredicate predicateWithProperty:@"parts.MIMEType" predicateOperator:LYRPredicateOperatorIsEqualTo value:@"image/png"];

NSError *error = nil;
NSOrderedSet *messages = [self.layerClient executeQuery:query error:&error];
if (messages) {
    NSLog(@"%tu messages with image/png", messages.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Fetching message parts containing PNGs

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessagePart class]];
query.predicate = [LYRPredicate predicateWithProperty:@"MIMEType" predicateOperator:LYRPredicateOperatorIsEqualTo value:@"image/png"];

NSError *error = nil;
NSOrderedSet *messageParts = [self.layerClient executeQuery:query error:&error];
if (messageParts) {
    NSLog(@"%tu messageParts in conversation with PNGs", messageParts.count);
} else {
    NSLog(@"Query failed with error %@", error);
}

Metadata queries

Conversation metadata is queryable, which allows you to find conversations with specific properties that matter to your app, such as background color, topic, or any other type of value. The following examples demonstrate how powerful querying on metadata can be; assume that we have created the following conversations at some point in our app:

NSDictionary *bigDictionary = @{@"1": 
                                @{@"2": @"Hello",
                                  @"3" : 
                                  @{@"4" : @"Hola",
                                    @"5" : @"Hey"}},
                                @"6" : @"Hi"};
LYRConversation *convoWithNestedDictonary = [layerClient newConversationWithParticipants:participants options: @{@"first" : @{@"second" : @{@"third" : @"NestedResult"}}} error:nil];
LYRConversation *blueConvo = [layerClient newConversationWithParticipants:participants options:@{@"backgroundColor" : @"blue"} error:nil];
LYRConversation *redConvoWithTitle = [layerClient newConversationWithParticipants:participants options:@{@"backgroundColor" : @"red", @"title" : @"testUser"} error:nil];
LYRConversation *convoWithDictionary = [layerClient newConversationWithParticipants:participants options:bigDictionary error:nil];

Fetching conversations with specific metadata

This example shows a predicate that fetches conversations based on a value nested within the metadata:

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
query.predicate = [LYRPredicate predicateWithProperty:@"metadata.first.second.third" predicateOperator:LYRPredicateOperatorIsEqualTo value:@"value"];
NSOrderedSet *conversations = [self executeQuery:query error:&error]; // this will return convoWithNestedDictonary

Fetching multiple conversations with specific metadata

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
query.predicate = [LYRPredicate predicateWithProperty:@"metadata.backgroundColor" predicateOperator:LYRPredicateOperatorIsIn value:@[@"red", @"blue"]];
NSOrderedSet *conversations = [self executeQuery:query error:&error]; // this will return blueConvo and redConvoWithTitle

Compound predicates

A compound predicate consists of a set regular predicates, along with a way to join them together (known as a “conjunction operator”).

On iOS, compound predicates are built using the LYRCompoundQuery class, along with an NSArray of LYRPredicate objects and a conjunction operator represented by a LYRCompoundPredicateType.

The following example demonstrates a compound predicate that finds (the number of) messages in a certain conversation that were sent by a certain user:

LYRConversation *targetConversation = self.conversation;
NSString *senderID = @"<USER_ID>";
LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRMessage class]];

// Create the separate predicates
LYRPredicate *conversationPredicate = [LYRPredicate predicateWithProperty:@"conversation" predicateOperator:LYRPredicateOperatorIsEqualTo value:targetConversation];
LYRPredicate *userPredicate = [LYRPredicate predicateWithProperty:@"sender.userID" predicateOperator:LYRPredicateOperatorIsEqualTo value:senderID];

// Combine them into a compound predicate
query.predicate = [LYRCompoundPredicate compoundPredicateWithType:LYRCompoundPredicateTypeAnd subpredicates:@[userPredicate, conversationPredicate]];

// Run the query
NSUInteger countOfMessages = [self.layerClient countForQuery:query error:&error];
if (countOfMessages != NSUIntegerMax) {
    NSLog(@"%tu messages matching compound predicate", countOfMessages);
} else {
    NSLog(@"Query failed with error %@", error);
}

The following example demonstrates a compound predicate that finds the conversations with a metadata background color that is either “red” or “blue”, and whose metadata title has the value “testUser”:

LYRQuery *query = [LYRQuery queryWithQueryableClass:[LYRConversation class]];
LYRPredicate *predicate1 = [LYRPredicate predicateWithProperty:@"metadata.backgroundColor" predicateOperator:LYRPredicateOperatorIsIn value:@[@"red", @"blue"]];
LYRPredicate *predicate2 = [LYRPredicate predicateWithProperty:@"metadata.title" predicateOperator:LYRPredicateOperatorIsEqualTo value:@"testUser"];
LYRCompoundPredicate *compound = [LYRCompoundPredicate compoundPredicateWithType:LYRCompoundPredicateTypeAnd subpredicates:@[predicate1, predicate2]];
query.predicate = compound;

NSOrderedSet *conversations = [self executeQuery:query error:&error]; // this will give you redConvoWithTitle
Rich Content Typing indicators