iMessage Developer


When iMessage applications were announced during the WWDC 2016 keynote, I didn’t expect too much. Stickers, message effects, and maybe access to the most common APIs. Generally, features that only big players will make good use of. Thankfully, I was wrong because the reality is completely different.

According to messages, there are no restrictions at all. Sure, your application can still be rejected during the App Store review. But, if you wish to, technically you can implement any regular iOS application as an iMessage application. Even things like in-app purchases. I think it’s amazing because it opens up a whole new market for developers.

I spent some time exploring this topic and built a Checkers board game which uses the new messages API to send moves between players. It was featured by Apple on release day of iOS 10! Then I asked on /r/iosprogramming if anyone is interested in a tutorial about the new iMessage and there it is.

Before you start

There are some facts you should know first.

iMessage applications can be developed and distributed in two ways:

  • As an extension to an existing application
  • As an extension which is an independent application

This is significant because you can create iMessage only, standalone apps. It’s not necessary to have a parent application like for other types of extensions.

New messages can be received on:

  • iPhone and iPad with iOS 10
  • Mac with macOS Sierra
  • Apple Watch with watchOS 3

Each previous version of these operating systems has fallback support — the message is delivered as a static image. Additionally, you can provide a URL to your back-end service and allow users to interact with the message from the web browser.

 

The important thing is: as a developer you don’t have access to the content of the conversation. So forget about bots that reply to commands.

In terms of testing the application. There is no option to send a message from the iOS simulator to physical devices, neither in the opposite way.

First extension

To start, launch XCode, select File → New → Project, then choose iMessage Application template from the iOS menu. Then provide Product Name, project location and and confirm your options.

 

Similar to a single view iOS application, you will see: default view controller, main interface storyboard, assets directory, information property list, linked Messages framework, and product binary. Nothing too unusual, so let’s look into the details.

 

Messages view controller

MessagesViewController inherits from MSMessagesAppViewController. It’s a new class which is a child of UIViewController.

The first characteristic is that the view of messages view controller can be presented in two styles: compact and expanded.

 

You can get the active presentation style by checking the presentationStyle property. To request a new presentation style, call the requestPresentationStyle function. Furthermore, there are also two functions invoked at the beginning and end of style transition that can be overridden: willTransition and didTransition.

The activeConversation property of the messages view controller gives you access to the active conversation. As I said before, you can’t access the content of messages other than those created by your extension.

 

Messages extension has its lifecycle and you are able to override the callback functions like didBecomeActive in the messages view controller. It works very similar to the well-known viewDidLoad from UIViewController. A full list of lifecycle functions possible to override is available under the “Managing the Extension’s State” section in the MSMessagesAppViewController documentation.

There is another set of functions to track when a user taps on the message bubble, e.g. didSelect, didReceive. Again the full list is in the MSMessagesAppViewController documentation under the “Tracking Messages” section.

One thing which is not clearly enough explained in the documentation, in my opinion, is information that tracking message functions are not called if the extension is inactive and the user taps on the message bubble.

 

Please pay special attention to this! As you can see, the visual difference isn’t obvious at first sight. It’s very easy to miss when the extension process was terminated. It’s enough if the user switched between extensions or went back to the conversations screen!

 

Because in the expanded view a user doesn’t see the message bubble, you may want to request the compact view or dismiss the view to show the bubble. After dismissing, you can still insert a new message without an error but the process will be terminated so the extension will change the state to inactive!

Be careful about view constraints too. Even if you center an element in the middle of the screen in your storyboard it doesn’t mean it will be centered in the extension view. The reason behind this is that the extension view has a top bar with conversation participants and a bottom bar with the text field for the text message. And both have different height.

 

As you probably know, each view controller has the superview which is a parent for other views. There is no difference here. Two presentation styles don’t mean that you have two separate views. It’s still the same view.

I recommend using the setup similar to the example provided by Apple. Use two UIViewControllers for each presentation style and instantiate them in your MSMessagesAppViewController. The biggest advantage of this solution is that you don’t have one huge class responsible for everything and you can easily pass data to your messages using the delegate pattern and protocols.

class CompactViewController: UIViewController {
// ...
}
 
class ExpandedViewController: UIViewController {
// ...
}
 
class MessagesViewController: MSMessagesAppViewController {
override func willBecomeActive(with conversation: MSConversation) {
super.willBecomeActive(with: conversation)
 
presentVC(for: conversation, with: presentationStyle)
}
 
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
guard let conversation = activeConversation else {
fatalError("Expected the active conversation")
}
 
presentVC(for: conversation, with: presentationStyle)
}
 
private func presentVC(for conversation: MSConversation, with presentationStyle: MSMessagesAppPresentationStyle) {
let controller: UIViewController
 
if presentationStyle == .compact {
controller = instantiateCompactVC()
} else {
controller = instantiateExpandedVC()
}
 
addChildViewController(controller)
 
// ...constraints and view setup...
 
view.addSubview(controller.view)
controller.didMove(toParentViewController: self)
}
 
private func instantiateCompactVC() -> UIViewController {
guard let compactVC = storyboard?.instantiateViewController(withIdentifier: "CompactVC") as? CompactViewController else {
fatalError("Can't instantiate CompactViewController")
}
 
return compactVC
}
 
private func instantiateExpandedVC() -> UIViewController {
guard let expandedVC = storyboard?.instantiateViewController(withIdentifier: "ExpandedVC") as? ExpandedViewController else {
fatalError("Can't instantiate ExpandedViewController")
}
 
return expandedVC
}

}

One more thing: There is another new view controller called MSStickerBrowserViewController. I didn’t play with it so I won’t describe how it works. It is used to group sticker objects (MSSticker) in a similar way to UICollectionViewController. Each sticker is initialized with an image and can be dragged to the active conversation from the view controller.

Messages

To create a message bubble, you need to create the MSMessage object. The first thing to set up after initialization is a layout. The layout is an MSMessageTemplateLayout object and allows to specify properties like caption, image, image title or trailing caption.

 
Source: https://developer.apple.com/reference/messages/msmessagetemplatelayout

The image property can be replaced by mediaFileUrl because message bubbles don’t have to be static. Additionally, for PNG, JPEG, GIF you can use videos.

There is another useful property: shouldExpire. If it is enabled, a message will disappear after being read.

private func composeMessage() {
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "message-background.png")
layout.imageTitle = "iMessage Extension"
layout.caption = "Hello world!"
 
let message = MSMessage()
message.shouldExpire = true
message.layout = layout

}

 

Sessions

You can initialize messages in a session (MSSession). The extension can have multiple sessions, even for a single conversation. If a session is set, messages will be grouped into a single bubble.

When someone replies to the message within a session, its bubble is replaced by a summary text and a new bubble is placed as the newest thing in the conversation.

Without a session, each message will be sent as a separate bubble.

 

 

private func composeMessage() {
let session = MSSession()
 
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "message-background.png")
layout.imageTitle = "iMessage Extension"
layout.caption = "Hello world!"
 
let message = MSMessage(session: session)
message.layout = layout
message.summaryText = "Sent Hello World message"

}

 

Conversation

Conversation (MSConversation) is passed as an argument in state functions, or you can get it from activeConversation property (remember it’s optional and it can be nil) in messages view controller.

From the conversation, you can get selectedMessage which references to the message tapped by the user, or you can insert a new message by using the insert function.

private func composeMessage() {
let conversation = activeConversation
let session = conversation?.selectedMessage?.session ?? MSSession()
 
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "message-background.png")
layout.imageTitle = "iMessage Extension"
layout.caption = "Hello world!"
layout.subcaption = "Sent by /(conversation?.localParticipantIdentifier)"
 
let message = MSMessage(session: session)
message.layout = layout
message.summaryText = "Sent Hello World message"
 
conversation?.insert(message)

}

What is very important when you compose and insert the message is that the user still has to confirm it before sending by tapping on the button. There is no workaround for this and you can’t send a message programmatically from the app.

Sending custom data and web back-end

A very powerful and crucial feature is the possibility to send custom data embedded in the messages. For iOS you don’t need a web back-end, it’s all handled by Apple.

Each message has its URL property and iMessage extensions use the format of URL (key=value&key=value) to attach data to the message. Additionally, there are new generic classes in iOS 10 to make parsing URLs easier: URLComponents and URLQueryItem.

private func composeMessage() {
let conversation = activeConversation
let session = conversation?.selectedMessage?.session ?? MSSession()
 
let layout = MSMessageTemplateLayout()
layout.image = UIImage(named: "message-background.png")
layout.imageTitle = "iMessage Extension"
layout.caption = "Hello world!"
layout.subcaption = "Sent by /(conversation?.localParticipantIdentifier)"
 
var components = URLComponents()
let queryItem = URLQueryItem(name: "key", value: "value")
components.queryItems = [queryItem]
 
let message = MSMessage(session: session)
message.layout = layout
message.url = components.url
message.summaryText = "Sent Hello World message"
 
conversation?.insert(message)

}

Having a back-end is only necessary if you want to give macOS users the possibility to click on the message and interact with it from the browser. Without the back-end, a message is still properly shown as image/video with captions and icon.

Group conversations

There are no default rules as to how conversation participants interact with messages from your extension. For example, the sender can tap on his own message and change something many times if you didn’t limit this. Fortunately, there is an option to check the UUID of the message sender, UUID of the local participant in the conversation, and the array of all remote participants UUIDs. Remember that a conversation is possible for more than two users at the same time.

private func isSenderSameAsRecipient() -> Bool {
guard let conversation = activeConversation else { return false }
guard let message = conversation.selectedMessage else { return false }
 
return message.senderParticipantIdentifier == conversation.localParticipantIdentifier

}

Wrap-up

If you would like to know something I didn’t mention here, please let me know in comments and I will try to explain it. My application uses SpriteKit to manage the interface but I omitted this part and only focused on the messages API. I collected some links below related to this article.






Questions?
Click here to chat with us

Live Now
Irine

Live Now : Irine

_