Episode #60

Windows Azure Mobile Services Part 2

27 minutes
Published on April 2, 2013
We continue with our example chat application here and add the ability post a message, poll for updates, and receive push notifications. This episode utilizes a pod calles MessagesTableView controller to present an SMS like interface for the messages.

Episode Links

Presenting the Chat View Controller

The MessagesTableViewController docs mention that it doesn't play nicely with storyboards, so we simply create & present it with code:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([self promptIfNoUserOrContinue]) {
        NSCChatViewController *chatVC = [[NSCChatViewControlleralloc] init];
        chatVC.client = self.client;
        chatVC.room = self.rooms[indexPath.row];
        [self.navigationControllerpushViewController:chatVC animated:YES];
    }
}

Note that we have to set the client and the room that we want to use for the chat view controller.

Maintaining the User's current Room

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self setUserRoomId:@(self.room.roomId)];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self stopPolling];
    [self setUserRoomId:[NSNull null]];
}

This isn't a bullet proof solution, but will work fine enough for our needs.

- (void)setUserRoomId:(id)roomId {
    MSTable *users = [self.clientgetTable:@"Users"];
    id values = @{@"roomId": roomId, @"id": [[NSUserDefaultsstandardUserDefaults] objectForKey:@"userId"]};
    [users update:values
       completion:^(NSDictionary *item, NSError *error) {
           if (error) {
               NSLog(@"ERROR: %@", error);
           } else {
               NSLog(@"User room updated to %@", roomId);
           }
       }];
}

Note here that we're trusting the client to post a valid room id. In general you shouldn't trust your clients at all, so we'd have to add some defensive programming on the server to validate that this is a valid id.

Fetching the messages for the room

We're dynamically fetching the messages for the room by date, so we have to handle both cases: initial fetch where we want no animation, and incremental fetches that should animate in the rows and scroll to the bottom.

- (void)fetchMessages {
    [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
    NSLog(@"Checking for messages since %@", self.lastFetchDate);
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"roomId = %d and createdAt > %@", self.room.roomId, self.lastFetchDate];
    MSQuery *query = [[self messagesTable] queryWhere:predicate];
    [query readWithCompletion:^(NSArray *items, NSInteger totalCount, NSError *error) {
        self.lastFetchDate = [NSDate date];
        if (items) {
            NSLog(@"Found messages: %@", items);
            if (!self.messages) {
                self.messages = [NSMutableArray array];
                [self.messages addObjectsFromArray:[self messageArrayForDictionaries:items]];
                [self.tableView reloadData];
                [self scrollToBottomAnimated:NO];
            } else {
                NSMutableArray *messagesToAdd = [NSMutableArray array];
                for (id msgDictionary in items) {
                    NSCMessage *message = [NSCMessage messageWithDictionary:msgDictionary];
                    if (![self.messages containsObject:message]) {
                        [messagesToAdd addObject:message];
                    }
                }

                if ([messagesToAdd count] > 0) {
                    [selfinsertMessages:messagesToAdd];
                }
            }

            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        } else {
            NSLog(@"ERROR: %@", error);
            [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        }
    }];
}

This method used some supplementary methods:

  - (void)insertMessages:(NSArray *)messages {
    [self.tableView beginUpdates];
    NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:[messages count]];
    for (id msg in messages) {
        [self.messagesaddObject:msg];
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.messagescount] - 1
                                                    inSection:0];
        [indexPaths addObject:indexPath];
    }

    [self.tableView insertRowsAtIndexPaths:indexPaths
                          withRowAnimation:UITableViewRowAnimationAutomatic];
    [self.tableView endUpdates];
    [self scrollToBottomAnimated:YES];
}

- (BOOL)isOwnMessage:(NSCMessage *)message {
    return [message.userId isEqualToString:self.client.currentUser.userId];
}

- (BubbleMessageStyle)messageStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSCMessage *msg = self.messages[indexPath.row];
    return [self isOwnMessage:msg] ? BubbleMessageStyleOutgoing : BubbleMessageStyleIncoming;
}

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

- (NSString *)textForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSCMessage *msg = self.messages[indexPath.row];
    if ([selfisOwnMessage:msg]) {
        return msg.text;
    }

    NSString *text = [NSStringstringWithFormat:@"%@\n\n--%@", msg.text, msg.author];
    return text;
}

- (void)insertMessageRow:(NSCMessage *)msg {
    [self.messages addObject:msg];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:([self.messages count] - 1) inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}

## Sending Messages

- (void)sendPressed:(UIButton *)sender withText:(NSString *)text {
    sender.enabled = NO;

    NSCMessage *newMessage = [[NSCMessage alloc] init];
    newMessage.text = text;
    newMessage.roomId = self.room.roomId;

    [[self messagesTable] insert:[newMessage attributes]
                      completion:^(NSDictionary *item, NSError *error) {
                          if (item) {
                              NSCMessage *insertedMessage = [NSCMessage messageWithDictionary:item];
                              [self insertMessageRow:insertedMessage];
                              [self finishSend];
                          } else {
                              NSLog(@"ERROR: %@", error);
                              [[[UIAlertView alloc] initWithTitle:@"Didn't post your message"
                                                          message:[error localizedDescription]
                                                         delegate:nil
                                                cancelButtonTitle:@"OK"
                                                otherButtonTitles:nil] show];
                          }
                      }];
}

Server side script for inserting new messages & inserting push notifications

This goes on Messages -> Insert.

function insert(item, user, request) {
    item.createdAt = new Date();
    item.userId = user.userId;
    request.execute({
        success: function() {
            sendPushNotification(item);
            request.respond();
        }
    });   
}


function sendPushNotification(item) {
    var users = tables.getTable("Users");
    users.where({ roomId: item.roomId }).read({
        success: function(results) {
            results.forEach(function(u) {
                // don't send it to the sender
                if (u.userId == item.userId) return;

                // don't send it if we don't have a device token
                if (u.deviceToken === null) return;

                console.log("User: ", u);
                push.apns.send(u.deviceToken, {
                    alert: item.text,
                    payload: {
                        roomId: item.roomId
                    }
                });
            });
        }
    });
}

Reading the messages and including the username in the response

In the Messages table, Read script:

function getUsers(ids, callback) {
    var users = tables.getTable("Users");
    users.where(function(ids) {
        return this.userId in ids;
    }, ids).read({
        success: function(results) {
            callback(results);
        }
    });
}


function read(query, user, request) {
    request.execute({
        success: function(msgResults, error) {
            var userIds = msgResults.map(function(r) {
                return r.userId;
            });

            if (userIds !== undefined && userIds.length > 0) {
                console.log("User ids: " + userIds);
                getUsers(userIds, function(userResults) {
                    msgResults.forEach(function(msg) {
                        var user = userResults.filter(function(u) { return u.userId == msg.userId })[0];
                        msg.author = user.username;
                    });

                    request.respond();
                });
            } else {
                request.respond();
            }

        }
    });
}

Polling for changes

- (void)startPolling {
    // don't poll if the current device is using push notifications
    if ([[NSUserDefaults standardUserDefaults] objectForKey:@"deviceToken"]) {
        return;
    }

    self.fetchTimer = [NSTimer scheduledTimerWithTimeInterval:10
                                                       target:self
                                                     selector:@selector(fetchMessages)
                                                     userInfo:nil
                                                      repeats:YES];
}

- (void)stopPolling {
    [self.fetchTimer invalidate];
    self.fetchTimer = nil;
}

Responding to push notifications

When we get a push notification, we'll publish an NSNotification including the room id so the view controller knows to refresh the view. We'll do this in the app delegate:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    NSLog(@"Notification: %@", userInfo);
    NSNumber *roomId = userInfo[@"roomId"];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"NSCMessageReceivedNotification"
                                                       object:roomId];
}

We listen for this notification in our NSCChatViewController. When it arrives, we check the room id and if the message is intended for this room we fetch messages again.

- (void)listenForMessages {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onMessageReceived:)
                                                 name:@"NSCMessageReceivedNotification"
                                               object:nil];
}

- (void)onMessageReceived:(NSNotification *)notification {
    NSNumber *roomId = [notification object];
    if ([roomId intValue] == self.room.roomId) {
        [self fetchMessages];
    }
}
Want more? Subscribers can view all 587 episodes. New episodes are released regularly.

Subscribe to get access →

Source Code

View on GitHub Download Source