Speaking of collaboration, an iMessage app wouldn’t be complete without an additional social feature: voting. When sharing a message with friends, each collaborator can vote on homes they like and see who else has voted on them. This gives the trip organizer a bird’s-eye indication of the most popular listing, greatly simplifying the decision of which one to book.

Developing with the Messages Framework

Personalizing the experience

Apple is fully committed to privacy, and so are we. It’s something that everyone wants, but it also raises certain engineering challenges. The main implication as applied to iMessage apps is the inability to acquire any contact information of conversation participants at all. We could only get the participants’ UUIDs, which can then be used as placeholder labels, that iOS replaces with user names inside the conversation thread. However, this is not ideal for us as we would like to know a little bit more information about participants to be able to access their recently viewed listings and Wish Lists, among other things.

It turns out we can solve the problem by requiring everyone to be logged in to the main Airbnb app. By creating a shared App Group, we can use the keychain to share data between the main app and its iMessage counterpart. Once they are logged in, we can fetch all the required information in the extension and use it to personalize the UI, and make network calls.

A small note on testing. When we started using the shared keychain to personalize the experience, we noticed we could no longer use the iOS Simulator to test group conversations. This is because, even though you can impersonate the current participant either as the Simulator default Kate Bell or John Appleseed, they still use the same instance of the keychain and therefore are considered to be the same runtime user. To work around this limitation, we had to always test on device.

Race conditions when sending messages

Human race conditions happen when multiple users interact with the same message at the same time. Consider the following example: Jie shares a listing with Michelle and Noah. They both open the interactive message, vote, and send a reply back. If Michelle’s message arrives last, it will overwrite Noah’s message. Additionally, as all the messages from the same session are collapsed into a single bubble, one has no way of accessing previous replies, resulting in data loss.

Why does this happen? Well, the Messages Framework conveniently lets you attach a URL to an MSMessage instance. This enables us to add URL parameters and share state in a single message session among all participants. If multiple replies are composed in parallel, only one of them will land in the conversation. Hence, every other message will be overwritten and information in them will unfortunately be lost.

URL safeguards

From a participant’s perspective, the message can be in 3 states — unopened, opened in edit mode, and staged (i.e. waiting for them to hit “Send”). We don’t have to do anything about the first case, but we can be smart about the other two.

When you interact with the message by tapping on its bubble in the conversation thread, it becomes a selected message in the active conversation. MSMessagesAppViewController requests the transition to the expanded presentation style by invoking the method on its delegate:

func willTransitionToPresentationStyle(presentationStyle: MSMessagesAppPresentationStyle)

At this point, the iMessage app displays an expanded view of the currently selected message. In this state, a new message for the same session may arrive, likely containing updated data in the URL. MSMessagesAppViewController then notifies the app by calling:

func didReceiveMessage(message: MSMessage, conversation: MSConversation)

This updates the current presentation with the new incoming data. This approach solves the problem of overriding valid data by keeping the current message up to date with incoming replies before it is sent.

The third use case arises when the message is staged in the input box and is ready to be sent. While the message is staged, a new message may arrive which will again trigger the didReceiveMessage callback. The recommended approach would be to read the URL of the incoming message, merge its data with the current message’s URL, then re-stage the current message. Re-staging is achieved by simply inserting the message into the current conversation thread a second time; the insert method on MSConversation will replace the old message in the input box.

While the two approaches above will greatly assist in keeping data safe, they don’t mitigate the case when two or more messages are transmitted at the same time. It is extremely hard to prevent data loss here, since they are now outside of the message’s lifecycle.

Server-side resources

Using server-side resources to persist data is the most reliable way to maintain data integrity, and is also the suggested approach by Apple. We started out with URLs and eventually transitioned to server-side resources. Message URLs are treated like immutable tokens and so once created, don’t change during the entire session. This makes them inherently great to be used as unique IDs of their counterpart server-side resource, on which we save message state. All clients write data to the same resource, and every time a message is opened, they re-fetch the data and update the UI.

When should data be persisted?

There is a delicate balance to maintain when persisting message state on the server. As previously mentioned, MSConversation allows new messages to be inserted into the input field using the method:

func insert(message: MSMessage, completionHandler: ((Error?) -> Void)? = nil)

It does not, however, send the message. A participant must explicitly send the message to actually commit that action. In our case, they need to do that to send a vote.

Apple doesn’t expect data to be persisted on the server until the message is actually staged and sent. However, this poses the risk of data loss as there’s no guarantee a network request to store data would be successful. After some brainstorming, we decided it would be more reliable to persist state data before the message is staged and sent, making sure that remote data resources are always up to date.

Fallbacks

Those not on iOS 10 will unfortunately not be able to enjoy this rich messaging experience. When the iMessage app sends a message, unsupported devices will receive two separate messages — an image and a URL. We still try to provide all participants with the best Airbnb experience possible, by forming URLs in such a way that, when opened in the browser, we know they’re part of an iMessage conversation. For this specific scenario we display listing details as usual, but additionally also allow voting on it!

Consequently, voting data is made available back to the users in the iOS 10 conversation thread. By generating push notifications server-side to notify iOS 10 users that new data is available, we maintain the real-time nature of the conversation flow for everyone.

App Discoverability

Before leaving you to try out the app yourself, we’d like to address one more challenge we faced: app discoverability. Hopefully by reading this article, you now are fully aware of the new capabilities we are introducing to the Airbnb app, but not everyone will be! We want to make it extremely easy for everyone to see that this new iMessage app is available. However, there are no APIs to easily deep link from the parent app into the iMessage settings screen to enable the corresponding app extension. One has to first open Messages, tap to open the app drawer, go to the iMessage App Store, select the “Manage” tab, then enable the app.

We attempted to mitigate this lack of discoverability by introducing an educational prompt right after sharing a listing — the most common action in the main app that relates to sending messages. Our hope is that this promotes the iMessage app right when it is needed the most!