Exploring Apple Message Extensions

iphone

Of all the things announced at WWDC 2016, iMessage app extensions might be the most promising for companies with a mobile presence. These app extensions allow developers to add new functionality directly to the Messages app in iOS 10. Users will soon be able to transfer money, play games, send videos, or make restaurant reservations all within the context of their existing iMessage conversations.

This is a huge win for both users and mobile companies. Users will be able to consume apps and services in the context of their conversations, while companies are given a new surface to engage iOS users. Companies can meet their customers at a personal level--right in their messages and group chats, while also respecting user chat privacy. These iMessage extensions will quickly become an important part of the iOS ecosystem.

In this post, I'm going to do a quick introduction of these new APIs, as well as take you through the creation of a standalone iMessage app extension. At the end of this post, you'll have created an app extension that lives inside iMessage, fetches post from Reddit, and allows users to share Reddit posts with one another. I hope this will introduce you to the possibilities afforded with these new iMessages APIs.

Downloading Xcode 8

First things first, you'll need to be a part of the Apple Developer program and download the Xcode 8 beta, available for download in the Apple Developer Center. (Quick tip: if you open the Xcode 8 xip file and it hangs on the verify step, try running xattr -d com.apple.quarantine Xcode_8_beta.xip in the terminal.) Go ahead and install this beta now.

Creating a 'Hello World' Messages Extension

Once you have the beta installed, open it and go to new project. Create a standalone iMessage extension by going to iOS -> Application -> Messages Application.

These extensions can be released either as a standalone or bundled with your app--we're going to make a standalone application in this tutorial. Call the app anything you'd like, maybe something Reddit related since we'll be using that site's API. Before we jump in, go to your project's build settings. There is a setting there called ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES--set this to "Yes". This will make sure that all the standard Swift objects, like fundamental data types, arrays and dictionaries, are available in our extension.

Now that you have a new iMessage extension, try running it in the simulator. You'll be prompted for an app to run--Messages should be selected automatically. Select the new app drawer button in Messages, and scroll to the right. You should see your brand new Hello World app extension.

Hello World extension

Let's do a quick rundown of what we're looking at. The top half of the screen is where the familiar blue iMessage bubbles are displayed in a chat. Not a whole lot has changed up here.

The bottom half of the screen has a number of new elements. First, we have 3 new buttons next to the message input field. We have a camera icon to access our photo library, a heart icon to send live drawings, and a new app drawer button.

Tapping this app button brings up a catalog of all installed iMessage extensions. We can see our new Hello World app, along with several other example sticker packs here. Users can swipe to the left and right to switch extensions, allowing quick message insertions from different extensions. The bottom right corner has a small arrow, which allows users to take an extension into full expanded mode instead of compact mode.

Project Overview

Now, let's do a quick overview of the files Xcode generated for this new extension. First, we have the MainInterface.storyboard file. Here, you can see the layout for the messages view controller, containing a single 'Hello World' label. These storyboards function exactly as any other storyboard in Xcode. There are no specific extension components you have to use for Messages extension UI.

Next, open up MessagesViewController.swift. This contains all the logic for our app extension. You'll notice that this is a new subclass of UIViewController, MSMessagesAppViewController. This messages controller has all the same UIViewController methods with a handful of new methods and properties attached to it. A quick overview of these new methods:

  • willBecomeActive(with conversation: MSConversation): called right before your extension becomes active. This is the best place to setup your extension and restore any previous state. The conversation object is important here--it gives you the participant IDs and selected message, and allows you to insert messages or media into the user's text field.
  • didResignActive(with conversation: MSConversation): opposite of above method, called right before your extension goes out of focus.
  • didReceive(_ message: MSMessage, conversation: MSConversation): called whenever a message is received. Note that your extension must be in focus--you will not receive this method if your extension is out of focus.
  • didStart/CancelSending(_ message: MSMessage, conversation: MSConversation): called when messages are sent or canceled. Again, your app extension must be in focus to receive this.
  • will/didTransition(to presentationStyle: MSMessagesAppPresentationStyle): called when the presentation style is changed in your app. If needed, this would be a good place to put layout changes. Also, if your app is in focus in compact mode and a message is selected, this delegate will be called.
  • requestPresentationStyle(_ presentationStyle: MSMessagesAppPresentationStyle): allows you to switch between the compact and full screen modes for your extension.
  • activeConversation: MSConversation: contains the active conversation when your extension is in focus. As of the first beta, this property is often nil for me. I've had to store the conversation from willBecomeActive separately to access the active conversation (note: will try to keep this post up-to-date as betas become more advanced)
  • presentationStyle: MSMessagesAppPresentationStyle: either compact, or expanded
  • dismiss(): dismisses your extension and brings the keyboard back up.

Creating a Data Fetcher

Now that we have a basic outline of the MSMessagesAppViewController, let's get into what we'll be building. At the end of this tutorial, you'll have an app extension that allows a user to browse the top posts in a given subreddit, then share those posts with an iMessage chat group. Reddit is a good choice for a sample app since it has a straightforward and free API we can leverage. Let's start by creating a service for interacting with this Reddit API. Go ahead and create a plain swift object called RedditAPI.swift. At the top of this file, let's create a struct for storing Reddit posts. It should look something like the struct below. If you want, add additional attributes found in the Reddit API--I'm going to keep this post simple.

struct Post {
 var title: String?
 var score: NSNumber?
 var url: String?
 var domain: String?
}

Below that struct, let's make a new class for our Reddit API. Create a constant in this class containing the Reddit API link (I'm using /r/programming here)

class RedditAPI {
 static let REDDIT_API_URL = "https://www.reddit.com/r/programming.json"
}

Next, let's add a single function to this RedditAPI class that fetches Reddit posts and takes a completion closure. That closure should accept an array of Reddit post objects. The signature should look something like this: class func getTopStories(_ completionBlock: ((results: ![Post]) -> Void ))

In this function, we're going to create a request data task for the subreddit. We will need to get the default URLSession, then run a data task in that session. The outline should look like this:

class func getTopStories(_ completionBlock: ((results: ![Post]) -> Void )){
 let defaultSession = URLSession(configuration: URLSessionConfiguration.default())
 defaultSession.dataTask(with: URL(string: REDDIT_API_URL)!) { (data, response, error) in
}

Now, inside that data task, lets parse our JSON into Post objects. The code for this parsing is below:

defaultSession.dataTask(with: URL(string: REDDIT_API_URL)!) { (data, response, error) in
 guard let sData = data else { return }
 do {
 let json = try JSONSerialization.jsonObject(with: sData, options: .mutableContainers)
 guard let wrapper = json!["data"] as? NSDictionary, let children = wrapper!["children"] as? NSArray else { return }
 var posts = [Post]()
 for child in children {
 guard let childDict = child as? NSDictionary, post = childDict!["data"] as? NSDictionary else { continue }
 var redditObject = Post()
 if let score = post!["score"] as? NSNumber {
 redditObject.score = score
 }
 if let title = post!["title"] as? NSString {
 redditObject.title = String(title)
 }
 if let url = post!["url"] as? NSString {
 redditObject.url = String(url)
 }
 if let domain = post!["domain"] as? NSString {
 redditObject.domain = String(domain)
 }
 posts.append(redditObject)
 }
 completionBlock(results: posts)
 } catch {
 print(error)
 }
}.resume()

Creating the Extension Interface

Great, now we should be able to get top stories from our MSMessagesViewController. Let's go ahead and lay out what our extension will look like. Open up MainInterface.storyboard and delete the default view inside Messages View Controller. Drag in a table view and make sure the view outlet is automatically set in the storyboard. Drag in a prototype table view cell--this where we'll lay out the post cell displayed to the user. Add in 3 labels to this prototype cell and arrange them somewhat like the picture seen below. Feel free to configure the look of the cell however you want. The only change you must make for these labels is to set the number of lines on the title label to 0.

Table cell layout

Now, let's create a subclass of UITableViewCell for that storyboard prototype. Create a new Cocoa class called PostTableViewCell, subclassed from UITableViewCell. Open this new subclass and the storyboard in the assistant editor. To create outlets, control-click on each label and drag into the class. It should look something like this when you're done:

Cell outlets

Now, in the storyboard, set the prototype cell class type to PostTableViewCell. Also, set a reuse identifier for this cell. I used "post".

Custom cell class
Cell identifier

While we're in the storyboard, set the MessagesViewController as both the data source and delegate of the tableview. Also, open up MessagesViewController create an outlet to the table view we just configured in the storyboard. You should end up with a line like @IBOutlet var tableView: UITableView! in MessagesViewController.

Linking the Data Fetcher and View Controller

OK, all done with the storyboard now. Let's head back to MessagesViewController to build out the logic of our extension. We need to add two new properties to this class: one to hold the Post objects and one to hold the MSConversation we receive in willBecomeActive. (Note: as of beta 1, the activeConveration variable in MSMessagesAppViewController is often unset. That's why we'll be storing this MSConversation separately.)

var posts: ![Post]?
var savedConversation: MSConversation?

Then, go ahead and set that savedConversation object in willBecomeActive

Now, let's work on our viewDidLoad method. I'll give the code now and then step through it:

override func viewDidLoad() {
 super.viewDidLoad()
 tableView.estimatedRowHeight = 100.0
 tableView.rowHeight = UITableViewAutomaticDimension
 RedditAPI.getTopStories { ![weak self] (results) in
 self?.posts = results
 self?.tableView.reloadData()
 }
 }

First, we're going to give our tableView an estimated cell height and tell it to size the cells dynamically. Then, we call our RedditAPI function to get the top stories. When this returns, we set our posts array and reload the table data.

Configuring the Table View

Next, let's build out our table view delegate methods. I like to utilize class extensions whenever possible--they keep classes clean and easy to follow. Go ahead and declare this extension in the class:

extension MessagesViewController: UITableViewDataSource {
 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
 return posts?.count ?? 0
 }
}

Great, now we are informing the table how many cells to create. We just need to create and populate these cells, and we should be in good shape. Add this method to the data source delegate:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
 let tvc = tableView.dequeueReusableCell(withIdentifier: "post") as! PostTableViewCell
 if let post = posts?![indexPath.row] {
 tvc.title.text = post.title ?? "No Title"
 if let score = post.score {
 tvc.scoreLabel.text = "score: \(score)"
 }
 tvc.urlLabel.text = post.domain ?? "No URL"
 }
 return tvc
}

If you run the extension now, you should see a table view with the top Reddit posts from a given subreddit!

Initial Reddit browsing extension

Creating and Inserting Messages

To enable user interaction with this table, we'll need to add another extension to our message view controller. Below the data source delegate, go ahead and add a table view delegate extension:

extension MessagesViewController: UITableViewDelegate {
 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
 // cell selection code here
 }
}

In this function, we're going to create a new message with the post information and insert it into the user's iMessage input field.

func tableView(_ tableView: UITableView, didSelecRowAt indexPath: IndexPath) {
 guard let post = posts?[indexPath.row] else { return }
 let message = MSMessage()
 let layout = MSMessageTemplateLayout()
 layout.caption = post.title
 if let score = post.score {
 layout.subcaption = "\(score)"
 }
 layout.trailingSubcaption = post.domain
 if let postUrl = post.url, url = URL(string: postUrl) {
 message.url = url
 }
 message.layout = layout
 savedConversation?.insert(message, localizedChangeDescription: "Reddit Post", completionHandler: nil)
}

Let's step through this code. First, we find the post that the user has selected. Then, we create a new message and layout object to go with it. You can see the breakdown of a MSMessageTemplateLayout below:

Message template layout

The MSMessageTemplateLayout gives us a number of layout elements to work with. We can have an image, audio, or video object, along with a title, subtitle, captions and subcaptions. We'll only use a portion of these elements to display the post title, score, and URL.

With this MSMessageTemplateLayout set up, we then attach it to our MSMessage object. Now, we can insert the message into the conversation. We call conversation.insert() to put the object in the user's iMessage input field.

conversation.insert() is a pretty simple function--it takes an MSMessage, along with a completion handler, and inserts the message into the user's input field. This completion handler should accept an NSError optional. If there was an error inserting the message, then an NSError object will be given to the completion handler. Otherwise, the error optional will be nil. Since we aren't doing any real error checking and there are no additional actions we need to take after inserting a message, we can skip this handler.

Easy enough! If you run the app, you should be able to see the following:

Sending message from an extension

Adding Message Interaction

Let's add in some code that will allow us to interact with posted messages. When a user clicks one of our messages, let's open up a SFSafariViewController to display it. We'll need to implement this in two locations: willTransition and willBecomeActive. We need to use both of these methods because either one could be called. If our extension is inactive when the user selects a message, then willBecomeActive will be called. If the extension is active when the message is tapped, then the user is in compact mode and willTransition is called.

Let's first go to the top of our MessagesViewController and add a new property for this view controller: var safariViewController: SFSafariViewController?. Then, let's go to willBecomeActive and add in this logic:

override func willBecomeActive(with conversation: MSConversation) {
 savedConversation = conversation
 safariViewController?.dismiss(animated: true, completion: nil)
 if let url = conversation.selectedMessage?.url {
 safariViewController = SFSafariViewController(url: url)
 present(safariViewController!, animated: true, completion: nil)
 }
}

First, we save the conversation for later so we can insert messages. Then, we dismiss the Safari view controller in case it's already presented. We check the conversation to see if we have a selected message with a URL object attached to it. Assuming we do, we create a new Safari view controller and then show it. Now let's look at the code for willTransition.

override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
 guard presentationStyle == .expanded else { return }
 if let message = activeConversation?.selectedMessage, url = message.url {
 safariViewController = SFSafariViewController(url: url)
 present(safariViewController!, animated: true, completion: nil)
 }
}

First, let's check the presentation style. If the new style is compact, that means the user was in the expanded mode and then collapsed the extension. So, we'll just return in that case. Then, we do exactly the same thing we did in willBecomeActive: we check for an activeConversation selectedMessage URL, and if it is present, we show it in a Safari view controller. That's it! When you build and run the code, you should now be able to select posted messages to view the link in our extension.

Extension with browser

There are many opportunities for further functionality here. You could attach images to the messages in MSMessageTemplateLayout, add in proper error checking and handling, write additional Reddit browsing functionality, or even link this app extension with an existing full iOS application. I hope you enjoyed this quick tutorial and can't wait to see what's built using iMessage extensions!

About the Author

Will Emmanuel
Will Emmanuel is a consultant based in Washington DC. He works in CapTech's Service Integration practice area, and specializes in iOS development and cloud technologies.