With the new iOS10, you can build iMessage applications that are directly included in iMessage and conversations. I will not emphasize in what you can do with this new feature. Apple released two nice videos during the WWDC ( video 1 and video 2/ ) and you can find explanation here : iMessage Application Guide

moules-frites-a-la-belge

The goal of this short article is to explain how interactive messages work and how to implement them in Xcode. For this purpose, we want to create a list to organize Mussels and French Fries Party ( https://en.wikipedia.org/wiki/Moules-frites ).

This article is deeply inspired by this source (even some copy and paste that you’ll probably recognize…) : https://developer.apple.com/library/prerelease/content/samplecode/IceCreamBuilder/Introduction/Intro.html

The scenario is simple :

  1. Kate : Hey John, how about a mussels and french fries party tonight ?
  2. John : Why not.
  3. Kate uses the app to generate a new list and send it to John. But she forgot the “Andalouse sauce” !!!!! andalouse_dl
  4. John can edit the list, add a new element (the “Andalouse sauce”) and send it back to Kate.

step8step9step11step12
The sources of this project can be found here : https://github.com/fredfoc/FrenchFriesAndMussels

First step : create the project

At first we want to create the iMessage project. Open Xcode 8, then New > Project.

Choose : “iMessage Application”

step1

name it (in our cases the project name is : FrenchFriesAndMussels)

step2

the folders are automatically created and you have a nice folder with few items inside. The most important one being : “MessagesViewController.swift”, inside the MessagesExtension folder.

step3

MSMessagesAppViewController

MessagesViewController.swift contains a class which is a subclass of MSMessagesAppViewController. MSMessagesAppViewController is the core of every iMessage extension. It will receive/send all interactions from/to iMessage ( https://developer.apple.com/reference/messages/msmessagesappviewcontroller ). For this purpose you have different methods which are specific to iMessage and that could remember the ones you’ll find in an app delegate for classic application. When you create the project, this class is already prepopulated with all those methods. For this article we’ll use only :

override func willBecomeActive(with conversation: MSConversation) {}

override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {}

No need to describe what they will do, their names speak for them.

MSMessagesAppPresentationStyle

The next step is to create HomeViewController. This view controller will be the first controller used in the app. What you have to know is that your app will have two presentation style :

  • .compact
  • .expanded

HomeViewController will be presented when in compact mode. It will have a simple button to create a new list.

This is how HomeViewController will look like.

step4

protocol HomeViewControllerDelegate: class {
    func homeViewControllerControllerDidSelectAdd()
}

class HomeViewController: UIViewController {
    static let storyboardIdentifier = "HomeViewController"    
    weak var delegate: HomeViewControllerDelegate?
    @IBAction func createNewParty(_ sender: AnyObject) {
        delegate?.homeViewControllerControllerDidSelectAdd()
    }
}

Delegate is a simple delegation pattern, to warn MessagesViewController that “create button” was hit.

Now to display our HomeViewController, we need to create it and add it to MessagesViewController view. For this we’ll create a private method in MessagesViewController, called :

private func presentViewController(for conversation: MSConversation, with presentationStyle: MSMessagesAppPresentationStyle) {

This method will also be used later to transition from compact to expanded mode.

private func presentViewController(for conversation: MSConversation, with presentationStyle: MSMessagesAppPresentationStyle) {
        // Determine the controller to present.
        let controller: UIViewController
        if presentationStyle == .compact {
            controller = instantiateHomeViewController()
        }
        
        // Remove any existing child controllers.
        for child in childViewControllers {
            child.willMove(toParentViewController: nil)
            child.view.removeFromSuperview()
            child.removeFromParentViewController()
        }
        
        // Embed the new controller.
        addChildViewController(controller)
        
        controller.view.frame = view.bounds
        controller.view.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(controller.view)
        
        controller.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
        controller.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
        controller.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        
        controller.didMove(toParentViewController: self)
    }

This is a quite straight forward process.

For  now if you compile your app, you should see something like this :

step5

The app in compact mode.

Add some details for our mussels party list

To add a detail of what will be in our Mussels party list, we’ll create a second view controller and add some code to MessagesViewController.

This is how DetailListViewController will look like :

step6

/**
 A delegate protocol for the `DetailListViewControllerDelegate` class.
 */
protocol DetailListViewControllerDelegate: class {
    func detailListViewController(_ controller: DetailListViewController)
}

class DetailListViewController: UIViewController {
    // MARK: Properties
    
    @IBOutlet weak var mainTitleTextView: UITextField!
    @IBOutlet weak var tableView: UITableView!
    static let storyboardIdentifier = "DetailListViewController"
    
    weak var delegate: DetailListViewControllerDelegate?
    
    var shoppingList: ShoppingListModel?
    
    @IBAction func addElement(_ sender: AnyObject) {}
    
    @IBAction func sendMessage(_ sender: AnyObject) {}
}

extension DetailListViewController: UITableViewDelegate {
}

extension DetailListViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! DetailListViewCell
        return cell
    }
}

class DetailListViewCell: UITableViewCell {
    @IBOutlet weak var label: UILabel!
}

This a simple tableview with few buttons and the same delegation pattern that we used for HomeViewController. Nothing big except that we have introduced a model : ShoppingListModel. We’ll talk about this model later. For now let’s create a simple struct called ShoppingListModel and let’s look at the impact of this new view controller in MessagesViewController.

import Foundation
import Messages

struct ShoppingListModel {
    var name = "Mussels Party"
}

Implementation of the delegation for HomeViewController :

/**
 Extends `MessagesViewController` to conform to the `HomeViewControllerDelegate`
 protocol.
 */
extension MessagesViewController: HomeViewControllerDelegate {
    func homeViewControllerControllerDidSelectAdd() {
        /*
         The user tapped 'new party button'
         Change the presentation style to `.expanded`.
         */
        requestPresentationStyle(.expanded)
    }
}

When “create” button is tapped then delegate (MessagesViewController) is called and the presentationStyle is changed to “.expanded”, which means that “willTransition” method will be triggered so we have to implement it (we could have used “didTransition”. It all depends on your constraints).

override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
        guard let conversation = activeConversation else { fatalError("Expected an active converstation") }
        
        // Present the view controller appropriate for the conversation and presentation style.
        presentViewController(for: conversation, with: presentationStyle)
    }

This is where we are happy to have a function “presentViewController” (we would have had to duplicate some code and duplication is bad…). We also have to change “presentViewController” to handle the case of “.expanded” style.

if presentationStyle == .compact {
            controller = instantiateHomeViewController()
        }
        else {
            /*
             Parse a `shoppingList` from the conversation's `selectedMessage` or
             create a new `shoppingList` if there isn't one associated with the message.
             */
            let shoppingList = ShoppingListModel(message: conversation.selectedMessage) ?? ShoppingListModel(withMussels: true)
            controller = instantiateDetailListViewController(with: shoppingList)
        }

For now, if you compile and run this you should be able to see your app in expanded mode with “DetailViewController”. Let’s deal with the model now.

The app in expanded mode

The app in expanded mode

ShoppingListModel

ShoppingListModel is a simple struct. It will contains some elements (what you want to have in your party) and a name. As this is a struct, if you want to change it you’ll have to mutate it. But more important, ShoppingListModel must have few special things :

  • it must be able to generate an image that will be displayed in iMessage
  • it must have an initializer (a sort of deserializer) with some content provided by the current conversation
  • it must be “serialized” to be transmitted thru a message in the current conversation

The creation of the struct is here (we have an init that add some Mussels and Fries directly to our party) :

import Foundation
import Messages

struct ShoppingListModel {
    
    static let queryNameKey = "name"
    
    var name = "Mussels Party"
    var elements = [ShoppingListElementModel]()
    
    init(withMussels: Bool = true) {
        if withMussels {
            elements.append(ShoppingListElementModel(name: "Mussels"))
            elements.append(ShoppingListElementModel(name: "Fries"))
        }        
    }
    
    mutating func addElement (element: ShoppingListElementModel) {
        elements.append(element)
    }
    
    mutating func removeElementAtIndex(_ index: Int) {
        elements.remove(at: index)
    }
    
    mutating func changeName(name: String) {
        self.name = name
    }
}

struct ShoppingListElementModel {
    var name: String
    static let queryItemKey = "element"
}

The extension to generate/create a list from a conversation :

/**
 Extends `ShoppingListModel` to be able to be represented by and created with an array of
 `NSURLQueryItem`s.
 */
extension ShoppingListModel {
    // MARK: Computed properties
    
    var queryItems: [URLQueryItem] {
        var items = [URLQueryItem]()
        items.append(URLQueryItem(name: ShoppingListModel.queryNameKey, value: name))
        for element in elements {
            items.append(URLQueryItem(name: ShoppingListElementModel.queryItemKey , value: element.name))
        }
        return items
    }
    
    // MARK: Initialization
    
    init?(queryItems: [URLQueryItem]) {
        for queryItem in queryItems {
            guard let value = queryItem.value else { continue }
            switch queryItem.name {
            case ShoppingListModel.queryNameKey:
                name = value
            case ShoppingListElementModel.queryItemKey:
                let newElement = ShoppingListElementModel(name: value)
                elements.append(newElement)
            default:
                break
            }
        }
    }
}



/**
 Extends `ShoppingListModel` to be able to be created with the contents of an `MSMessage`.
 */
extension ShoppingListModel {
    init?(message: MSMessage?) {
        guard let messageURL = message?.url else { return nil }
        guard let urlComponents = NSURLComponents(url: messageURL, resolvingAgainstBaseURL: false), let queryItems = urlComponents.queryItems else { return nil }
        
        self.init(queryItems: queryItems)
    }
}

Why is that ?

When you send a message, you mostly do the same thing as when you do a request to a web service and analyse the response. So everything has to be done thru a query that can contain as many URLQueryItems as you want. What is a URLQueryItem, as said in the declaration of this struct : “A single name-value pair, for use with `URLComponents`.”

So what we do here is :

we transform a list into one URLQueryItem for the name of the list and as many URLQueryItems as needed for every element.

items.append(URLQueryItem(name: ShoppingListModel.queryNameKey, value: name))
for element in elements {
     items.append(URLQueryItem(name: ShoppingListElementModel.queryItemKey , value: element.name))
}

Rem : One good practice would have been to make ShoppingListElementModel and ShoppingListModel conform to CustomStringConvertible as indicated in URLQueryItem declaration by Apple :

public var value: String?

    /// A textual representation of this instance.
    ///
    /// Instead of accessing this property directly, convert an instance of any
    /// type to a string by using the `String(_:)` initializer. For example:
    ///
    ///     struct Point: CustomStringConvertible {
    ///         let x: Int, y: Int
    ///
    ///         var description: String {
    ///             return "(\(x), \(y))"
    ///         }
    ///     }
    ///
    ///     let p = Point(x: 21, y: 30)
    ///     let s = String(p)
    ///     print(s)
    ///     // Prints "(21, 30)"
    ///
    /// The conversion of `p` to a string in the assignment to `s` uses the
    /// `Point` type's `description` property.

This is a sort of “serialization” process.

Now the “deserialization” is here :

init?(message: MSMessage?) {}

and here :

init?(queryItems: [URLQueryItem]) {}

Those are straight forward methods.

Now that we have our models and that we can transmit them thru a message, let’s send the message.

The message

When the user taps on “send” button in DetailViewController, then we trigger the delegate method :

/**
 Extends `MessagesViewController` to conform to the `DetailListViewControllerDelegate`
 protocol.
 */
extension MessagesViewController: DetailListViewControllerDelegate {
    func detailListViewController(_ controller: DetailListViewController) {
        guard let conversation = activeConversation else { fatalError("Expected a conversation") }
        guard let shoppingList = controller.shoppingList else { fatalError("Expected the controller to be displaying a shoppingList") }
        
        // 1 : Create a new message with the same session as any currently selected message.
        let message = composeMessage(with: shoppingList, caption: "List :\(shoppingList.name)", session: conversation.selectedMessage?.session)
        
        // 2 : Add the message to the conversation.
        conversation.insert(message) { error in
            if let error = error {
                print(error)
            }
        }
        // 3 : dismiss
        dismiss()
    }
}

We need two thing to send a message : a conversation and a shoppingList. Of course if we don’t have this our reaction is a little bit too tough (we send a fatal error which will result in a nice crash for the user). Next time we’ll handle errors like this more gently.

1 : The “composeMessage” is a way to generate a message with a layout (the layout is where your UX designer will start being picky and where you’ll have to add the nice image generated by your ShoppingListModel extension – in our case it generates a list of elements).

private func composeMessage(with shoppingList: ShoppingListModel, caption: String, session: MSSession? = nil) -> MSMessage {
        var components = URLComponents()
        components.queryItems = shoppingList.queryItems
        
        let layout = MSMessageTemplateLayout()
        layout.image = shoppingList.renderSticker(opaque: true)
        layout.caption = caption
        
        let message = MSMessage(session: session ?? MSSession())
        message.url = components.url!
        message.layout = layout
        
        return message
    }

2 : we insert the created message in the conversation

3 : After inserting the message in the conversation, then we dismiss.

Kate will then have to enter a small message and validate to send the list to John. When John will receive it he will be able to open the list, edit it and sent it back to kate.

step10

step8 step9

That’s it we have an iMessage application to create Mussels Party (and never forget “Andalouse” sauce – pronounced “Andaloooossse” with a real nice belgian accent).