Markus Bergh

Adding an iMessage extension to your iPhone app

Adding an iMessage extension to your iPhone app

I have recently migrated my first iPhone app to Swift. It was written in Objective-C more than seven years ago and it really needed an update. The app communicates with a public API and lists upcoming concerts in Stockholm.

It also has integration with Spotify, so that Spotify premium users can listen to songs by the artists who will be performing, easily while browsing around.

Now that the app is rebuilt, I thought it would be fun to try out the new apps for iMessage.

iMessage apps

The most used apps for iMessage are probably stickers apps, where one would drag and drop stickers of any kind into the conversation. However I wanted to build something that was connected to my own app instead.

You can develop two kinds of apps for iMessage:

  1. Standalone, which will live on its own in Messages
  2. Extension, which allows users to access features of your iOS app from within Messages

I decided to build an extension since I wanted to re-use the code I already have in my iOS app. My idea of the extension was to list all concerts, as in the main application, in a slimmer look.

When user taps an upcoming event in the list, it will be converted into a message bubble that can be sent for sharing.

Get started

To add an iMessage extension in Xcode, you use "File → New → Target".

This will add some default content to your host application; a view controller, asset directory, information property list and the main interface storyboard.

Presentation styles

When running your newly added extension in the iPhone simulator, you will soon notice it can have two states of presentation, compact and expanded. Those can be requested by calling the requestPresentationStyle function. When the presentation changes, willTransition and didTransition are invoked which you can override.

I chose to have my app initially expand, and afterwards load and present data to the user. To do that I added a tap gesture for the view in viewDidLoad.

// MessagesViewController.swift
...
let tapExpandExtension

override func viewDidLoad() {
  super.viewDidLoad()

  tapExpandExtension = UITapGestureRecognizer(target: self, action: #selector(self.expandViewForExtension))

  // Add gesture to view
  view.addGestureRecognizer(tapExpandExtension)
}

@objc func expandViewForExtension() {
  requestPresentationStyle(MSMessagesAppPresentationStyle.expanded)
}

override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
  if presentationStyle == MSMessagesAppPresentationStyle.expanded {
    // Disable gesture for now
    tapExpandExtension.isEnabled = false
  }
}

override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
  if presentationStyle == MSMessagesAppPresentationStyle.expanded {
    // Get some data to the user
    getDataForExtension()
  }
}
...

Messages

When creating a message bubble, you will use the MSMessage object together with a MSMessageTemplateLayout object to set the layout.

This allowed me to create a bubble by specifying the image, title, and captions. Note that message bubbles don't have to be static, you can include a video by setting the mediaFileUrl property of the object.

// MessagesViewController.swift

let message = MSMessage()
let layout = MSMessageTemplateLayout()

layout.caption = event.Event // `event` is a Codable data type holding information 
layout.subcaption =  event.EventDate
layout.trailingSubcaption = "Debaser"

// Download image data
var image: UIimage?
do {
  let data = try Data(contentsOf: event.ImageUrl)
  image = UIImage(data: data)
} catch {
  print(error.localizedDescription)
}

layout.image = image!

This together with didSelectRowAt for the UITableViewDelegate I can now compose a message when the user taps an item. To compose a message you will get the active conversation through the MSConversation object. Don't forget that it can be nil, and that the user has to confirm the message so you cannot send it automatically.

// MessagesViewController.swift

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  let conversation = activeConversation

  ...

  conversation?.insert(message)

  // Set the presentation style to `compact`
  requestPresentationStyle(MSMessagesAppPresentationStyle.compact)
}

Interaction with a message

What happens when the recipient will tap on your message depends on if the user has the app installed on their iPhone or not. If not, the App Store for iMessage will open and prompt the user to install the app.

If you don't do any handling at all, the iMessage app will simply just become active. It is important to remember that you have to handle it differently depending on if the app is active or not. Simply switching to another app, or navigating back to the conversations list will terminate it.

To handle an active state you can override the willSelect and didSelect methods handle accordingly. I also wrote logic in the overridden method for didBecomeActive to handle cases when the extension was not active.

// MessagesViewController.swift

override func didBecomeActive(with conversation: MSConversation) {
  guard let selectedMessage = conversation.selectedMessage else { return }

  // Do something with the selected message
}

What I wanted to happen next, was that when the user tapped on a message sent by the extension, it would present a more detailed view regarding the event.

I first thought I could simply open the url for the event in Mobile Safari but this is not allowed. You could use a SFSafariViewController, but I ended up having the host application open instead and present a modal view with the details.

To make this possible I had to add some logic in the host app delegate.

// AppDelegate.swift

func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
  if canHandleMessageEventURL(url: url) {
   // Present the details modally
  }
}

This together with a data scheme for the event, I could make the transition from extension to host application.

// MessagesViewController.swift

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  ...
  var components = URLComponents()
  let queryItemEvent = URLQueryItem(name: "eventId", value: event.EventId)
  components.queryItems = [queryItemEvent]

  message.url = components.url!
  ...
}

override func didSelect(_ message: MSMessage, conversation: MSConversation) {
  guard let messageURL = message.url, let url = URL(string: "debaser-imessage://\(url)") else {
    return
  }

  self.extensionContext?.open(url, completionHandler: nil)
}

Summary

Adding an extension, or build a standalone app for iMessage is very easy. Apple has great documentation, and examples that you can dive into. An app for iMessage is a smart feature so users can easily share the content of your app.

Just be careful with the states of the application and be aware of that there are restrictions of what you can do.

Read more