Interactive Messages in iOS 10

iOS 10 SDK introuduces the new Messages framework which lets app developers create extensions to interact with the Messages app. Today I am going to illustrate how interactive messages can be sent using Messages framework. We will create an extension which lets two users play the classic tic tac toe game using the Messages app.

What are interactive messages?

The following screenshot will give you an idea of how an interactive message bubble looks like. You can embed an image or video in an interactive message.

Creating Message Extension

The capability to add functionality to Messages app is made available through extensions. So first we will open Xcode, create a new Xcode project and choose the Message extension template.

You’ll observe that a new project is created with an additional MessageExtension target. We are going to be doing all our work for today on this target.

MSMessagesAppViewController

MessagesViewController.swift holds the MessagesViewController class which is a subclass of MSMessagesAppViewController. This class is the entry point and control center for your extension.

Presentation Styles

There are two presentation styles for message extensions.

  1. Compact — This style is shown initially when the extension is launched from the app drawer.
  2. Expanded — You can click on the expand arrow in the compact style to transition to the expanded style. It can also be triggered when the sender clicks on one of the interactive messages.

App Logic

We are only letting the user play this game from the expanded presentation style. So you can click on a message, place your marker ( x or 0). As soon as the marker is placed the message which comprises of an image showing the current state of the game board, is inserted into the input field. The user has the control of whether he wants to send the message or not. When the receiver receives the message, he can click on it and make his move. After a user makes his move, the image showing the new state will be inserted into the input field.

We will keep the compact presentation style simple by adding only a label.

Project Overview

  1. GameBaordVC — This view controller is presented in the expanded presentation style. This handles all the functionality related to playing the game. The view managed by GameBoardVC has a collectionview. Tapping on the cells lets the user place x or 0 marks on the cells.
  2. CollapsedVC — This view controller is presented in the compact presentation style
  3. GameStateVM — This view model will hold the information about the current state of the game board and make a decision regarding the outcome of the game.

Presenting the Message Extension UI

MSMessagesAppViewController notifies various state changes of the extension through the following methods.

When the extension is launched the following methods are called in the indicated order.

1. func willBecomeActive(with conversation: MSConversation)
2. func didBecomeActive(with conversation: MSConversation)
3. func  viewWillAppear()
4. func viewDidAppear()

MSConversation object represents a message conversation. In the methods listed above, the conversation object represents a conversation that will be started.

Below is the code snippet for presenting the extension.

private func presentViewController(for conversation: MSConversation, with presentationStyle: MSMessagesAppPresentationStyle) {
// Determine the controller to present.
let controller: UIViewController
if presentationStyle == .compact {
controller = instantiateVCWithIdentifier(identifier: "StateStickersVC")
}
else {
let gameBoardVM = GameStateVM(message: conversation.selectedMessage)
controller = instantiateVCWithIdentifier(identifier: "GameBoardVC")
let gameBoardVC = controller as! GameBoardVC
gameBoardVC.gameStateVM = gameBoardVM
gameBoardVC.delegate = self
}
removeAllChildViewControllers()
embedViewController(viewController: controller)
}

A message extension can also be launched by directly tapping on an interactive message. This launches the extension in expanded presentation style.

In the above function if the presentation stlye is expanded we intitlaize the GameBoardVC, add it as a child view controller of the MessagesViewController and embed it in the view of the extension.

We also remove any previous instances of the views of controllers presented in expanded or compact style from the view heirarchy.

We associate an instance of GameStateVM with the GameBaordVC.

We initialize the GameStateVM with selected message of the current conversation.

convenience init(message: MSMessage?) {
guard let messageURL = message?.url else {
self.init(queryItems: [])
return
}
guard let urlComponents = NSURLComponents(url: messageURL, resolvingAgainstBaseURL: false), queryItems = urlComponents.queryItems else {
self.init(queryItems: [])
return
}
self.init(queryItems: queryItems)
}
init(queryItems: [URLQueryItem]) {
gameStateArray = [Int](repeating: 0, count: (rowCount * 2) + 2)
guard queryItems.count > 0 else {
player = 1
self.queryItems = [URLQueryItem](repeating: URLQueryItem(name: "0", value: "0"), count: rowCount * rowCount)
return
}
for i in 0...queryItems.count - 1 {
let queryItem = queryItems[i]
let player = Int(queryItem.name)
if i == queryItems.count - 1 {
self.player = player! == 1 ? -1 : 1
}
else {
self.queryItems.append(queryItem)
}
}
for index in 0...self.queryItems.count - 1 {
changeGameArrayWithRow(row: index / rowCount, column: index % rowCount, player: Int(self.queryItems[index].value!)!)
}
}

The only data you can transfer while sending messages using Message extension is by passing a url to the MSMessage we compose to send to the other end. If you need to pass any other information you will have to use APIs. We need to pass the current state of the baord. To keep things simple, we will pass the game board state as a row major ordered array with the MSMessage. We are going to pass this as queryItems: [URLQueryItem]? of NSURLComponents. So in convenience init(message: MSMessage?) we check if the selected message to which we have to reply has queryItems. If not we initialize it to the initial state of the game board using self.init(queryItems: []).

We have to be aware of three instance variables of GameStateVM

var gameStateArray: [Int] = []
var player: Int = 0
var queryItems: [URLQueryItem] = []

queryItems: [URLQueryItem] holds the state of the game board which represents the selected message. It holds the position of x and 0 in row major order. For simplicity we are representing player x as 1 in the array and player 0 as -1.

A URLQueryItem has name and value. Integer value 1 0r -1 corresponding to the current player is converted to a string and stored in the value parameter at a given position. When the game board is empty, values and names of all query Items are initialized to 0. When the player makes a move, depending upon the integer value associated with the player we change the name value pair of the query item at the position.

queryItems will have rowCount * columnCount number of items plus 1 for indicating which player made the last move ( x or 0 ). If the player who made the last move was x(1) then we assign the current player as 0(-1) and vise versa. Our example uses a 3x3 board.

gameStateArray: [Int] will have an entry each to represent all rows, columns and the two diagonals of the game board. So number of items is rowCount + ColumnCount + 2(for diagonals).

When a player makes a move depending on whether he is x(1) or o(-1) we add his score to the row, column and diagonal representing the cell on which he made a move in the gameStateArray. For eg: If he makes a move in column 0, row 0, we have to add +1 to the score at gameStateArray[0] (gameStateArray[row], gameStateArray[rowCount + col], gameStateArray[rowCount * 2])

We compute the row and column numbers based on the position in queryItems.

This functionality is done in : func changeGameArrayWithRow(row: Int, column col: Int, player: Int)

Building the Game Board

GameBoardVC has a collection view that represents the game board. It also has a label showing whether the game is over. Data source for the collection view is queryItems of GameStateVM.

GameStateVM has a method decideWinner() -> Int which decides the outcome of the game based on the current game board.

func decideWinner() -> Int {
for state in gameStateArray {
if state == -GameBoardVC.rowCount {
return -1
}
else if state == GameBoardVC.rowCount {
return 1
}
}
return 0
}

fun decideWinner() -> Int returns 1 if x has won the game , -1 if 0 has won the game and 0 otherwise. Label indicating the progress of the game is hidden or shown accordingly.

Making a Move

On selecting any cell of the collection view, if the cell is empty we place the marker associated with the current player. If the cell is not empty we do not let the user make the move. Likewise if the user has already made his move, we restrain him from making another one.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let playedValue = gameStateVM.queryItems[indexPath.item!].value, intValue = Int(playedValue) where intValue == 0 && moveMade == false && gameStateVM.decideWinner() == 0 else {
return
}
gameStateVM.placeItemAtRow(row: indexPath.item! / gameStateVM.rowCount, column: indexPath.item! % gameStateVM.rowCount, player: gameStateVM.player)
collectionView.reloadData()
moveMade = true
let image = collectionView.takeSnapshot()
delegate?.selecteWithGameStateVM(gameStateVM: gameStateVM, image: image)
}

We also take a screenshot of the current state of the game board to be inserted into the message we send to the other player. This will visually represent the current state of the game. For this example I have extended UIView to add a method fun takeSnapshot() for this purpose.

extension UIView {
func takeSnapshot() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main().scale)
drawHierarchy(in: bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}

Sending a Message

Finally we are at the last stage of our implementation, i.e, sending the actual message.

When the player makes his move we have to notify our MessagesViewController to insert the message into the input field. For this we have to add a Delegate to GameBaordVC.

protocol GameBoardVCDelegate {
func selectWithGameStateVM(gameStateVM: GameStateVM, image: UIImage)
}
func selectWithGameStateVM(gameStateVM: GameStateVM, image: UIImage) {
guard let conversation = activeConversation else { fatalError(“Expected a conversation”) }
var components = URLComponents()
components.queryItems = gameStateVM.queryItems
components.queryItems?.append(URLQueryItem(name: “\(gameStateVM.player)”, value: “0”))
let layout = MSMessageTemplateLayout()
layout.image = image
layout.caption = “Game On”
if gameStateVM.decideWinner() != 0 {
layout.caption = “Game Over”
}
let message = MSMessage(session: self.activeConversation?.selectedMessage?.session ?? MSSession())
message.url = components.url!
message.layout = layout
conversation.insert(message, localizedChangeDescription: nil) { (error) in
}
}
dismiss()
}

We construct the NSURLComponents from the queryItems in GameStateVM. We also add a queryItem which represents the value associated with the player who made the move.

We initialize a MessageTemplateLayout() which describes the layout of the message to be inserted into the input field. The game board screenshot becomes the layout image and depending on whether the game has ended , the caption is changed.

To insert the actual message we construct an MSMessage. If there is an ongoing conversation, then it is identified by an MSSession. This session object is used to initialize the message. Otherwise we initialize a new MSSession object and associate it with the message to be sent. The template layout we constructed earlier is also associated with the MSMessage.

Finally we insert the message into the conversation using the function:

public func insert(_ message: MSMessage, localizedChangeDescription changeDescription: String?, completionHandler: ((NSError?) -> Swift.Void)? = nil)

This completes the steps required to create a simple tic tac toe game using Messages framework.

This is our final product in action.

You can watch the wwdc video for interactive messages and refer to Messages framework for detailed reference.

Please feel free to go through the source code.

Developed at Innovation Labs @ Y Media Labs