Difference between revisions of "An iOS 10 Interactive Message App Tutorial"

From Techotopia
Jump to: navigation, search
(Designing the MessageApp User Interface)
Line 88: Line 88:
 
   
 
   
  
[[Image:xcode_8_ios_10_message_app_stacks_selected]]
+
[[Image:xcode_8_ios_10_message_app_stacks_selected.png]]
  
 
Figure 89-9
 
Figure 89-9
Line 101: Line 101:
  
 
Figure 89-10
 
Figure 89-10
 
  
 
== Creating the Outlet Collection ==
 
== Creating the Outlet Collection ==

Revision as of 20:07, 29 August 2016

The previous chapter introduced iMessage app extensions and described the way in which the Messages framework allows custom, interactive messages to be send and received from within the standard iOS iMessages app. In this chapter, many of the concepts described in the previous chapter will be put to practical use while working through the creation of an example interactive iMessage app.


Contents


About the Example iMessage App Project

This tutorial will create an interactive iMessage app extension project that implements a tic-tac-toe game designed to be played by two players via the iMessage app. The first player begins the game by loading the app from the iMessage app drawer and making a selection from within the tic-tac-toe game grid. The current game status is then sent to a second player where it appears in the standard iMessage transcript area. The second player selects the message to open the message app extension where the next move can be made and sent back to the first player. This process repeats until the game is completed.

Creating the MessageApp Project

Launch Xcode and select the option to create a new Xcode project. On the template selection screen, select the iMessage Application option as illustrated in Figure 89 1 below and click on the Next button:


Creating a new Xcode iMessage App Template project

Figure 89-1


On the following screen, name the product MessageApp and select Swift as the language. Click the Next button, select a location for the project files and click the Create button.

A review of the project structure in the project navigator panel will reveal that both a main iOS app (named MessageApp) and an extension (MessageExtension) have been created:


Xcode 8 ios 10 imessage app files.png

Figure 89-2


Within the MessagesExtension folder, the MessagesViewController.swift file contains the code for the MSMessagesAppViewController subclass while the MainInterface.storyboard file contains the user interface for the message app extension. By default, the layout currently consists of a Label object configured to display “Hello World”.

Before making any changes to the project, run the app on an iOS simulator and note that the iMessage app launches and opens the app drawer containing the new message app extension:


Xcode 10 ios 8 message app drawer.png

Figure 89-3


To load the extension, tap the MessageApp icon in the drawer and note that the default “Hello World” user interface appears (Figure 89-4):


Xcode 8 ios 10 message app hello.png

Figure 89-4


Swiping left or right over the extension app within the drawer will move between the different message apps currently installed on the device. Tapping the up arrow in the bottom right hand side of the extension panel will switch the app from compact to expanded presentation style. To return to compact presentation style, tap the down arrow in the top right of the full screen view.


Designing the MessageApp User Interface

The user interface for the iMessage app extension is going to consist of 9 Button objects arranged into a 3x3 grid using UIStackView layouts. Later in the tutorial, screenshots of the current game status will be taken and displayed in the interactive message bubbles. Due to problems with screenshots and the UIStackView class, the button collection will be contained within an additional UIView instance.

Begin the user interface design process by selecting the MainInterface.storyboard file and deleting the “Hello World” Label object. Drag and drop a View instance from the object library panel and position it so that it is centered horizontally and located along the bottom margin of the storyboard scene as illustrated in Figure 89-5:


Xcode 8 ios 10 message app view added.png

Figure 89-5


Drag a Button from the palette and place it within the newly added view. With the new button selected, display the Attributes Inspector panel and change the Background color to a light shade of gray. Double-click on the button and delete the current text so that the parent view and button match the layout shown below:


Xcode 8 ios 10 message App button one.png

Figure 89-6


Display the Assistant Editor panel and establish an action connection from the button to a method named buttonPressed.

Now that the first button has been added and configured it needs to be duplicated eight times. Select the button in the layout, use the Command-C keyboard shortcut to copy and Command-V to paste a duplicate. Position the new button to the right of the first button and continue pasting and moving button instances until there are three rows of three buttons:


Xcode 8 ios 10 message app buttons added.png

Figure 89-7


Select the first button in the top row and then hold down the shift key while selecting the remaing two buttons in that row. With all three buttons selected, click on the Stack View button (highlighted in Figure 89 8) to add the buttons to a horizontal UIStackView instance:


Xcode 8 ios 10 message app stackview row one.png

Figure 89-8


With the UIStackView instance selected, use the Attributes Inspector panel to change the Spacing attribute to 2. Repeat these steps on the two remaining rows of button so that each row is contained within a horizontal stackview. Display the Document Outline panel, hold down the Command key and select each of the three Stack View instances:


Xcode 8 ios 10 message app stacks selected.png

Figure 89-9


With the three Stack View entries selected, click on the stack view button in the canvas toolbar once again to add the three horizontal stacks into a single vertical stack. Using the Attributes Inspector panel, increase the spacing property on the vertical stack view to 2.

With the vertical stack still selected, display the Auto Layout align menu and enable both the horizontal and vertical center in container options before clicking the Add 2 Constraints button. Reduce the width of the parent View object so that it more closely matches the size of the button grid, use the Auto Layout Pin menu to set a spacing to nearest neighbor constraint on the bottom edge of the object using with the Constrain to margins option enabled. Before adding the constraint, also enable the Height and Width constraint check boxes using the current values. Next, display the Auto Layout Align menu, add a constraint to center the view horizontally within the container and change the Update Frames menu option to All Frames in Container before clicking on the Add Constraints button. At this point the layout of the view and grid should match Figure 89-11. Before proceeding, display the Assistant Editor and establish an outlet connection for the View object named gridView:


Xcode 8 ios 10 message App ui.png

Figure 89-10

Creating the Outlet Collection

As is invariably the case, the code for the app will need to be able to access the buttons in the user interface via outlets. In this project, all of the buttons are going to be connected to the same outlet by making use of an outlet collection. Where a normal outlet contains a reference to a single user interface object, an outlet collection is an array of references to multiple user interface objects. Display the Assistant Editor and select the first button on the top row of the grid (note that initial selection attempts may select the parent StackView objects so continue clicking until only the button is selected). Create the outlet collection by Ctrl-clicking on the selected button and dragging to a position beneath the class declaration line in the Assistant Editor panel. When the connection dialog appears, change the Connection menu setting from Outlet to Outlet Connection (Figure 89-11), name the connection Buttons and click on the Connect button.


Creating an Xcode Outlet Connection

Figure 89-11


To add the second button in the first row, click on the outlet marker in the margin of the Assistant Editor panel and drag to the button as outlined in Figure 89-12:


Creating an Xcode Outlet Connection

Figure 89-12


Connect the outlet collection using this technique for the remaining buttons in the grid, taking care to work from left to right and row by row (the order in which the buttons are added to the collection is important if the game is to function correctly).

Run the extension on the simulator and verify that the layout appears as designed in both compact and expanded presentation styles.

Creating the Game Model

The model for tracking the status of the game is very simple and consists of an array containing 10 string elements. The first element in the array is used to store the current player (‘X’ or ‘O’) while the remaining 9 elements contain the current settings of the corresponding buttons in the array (also ‘X’ or ‘O’). The elements in this array are initialized with ‘-‘ characters to indicate unselected grid locations.

Open the MessagesViewController.swift file and add some variable declarations as follows:

class MessagesViewController: MSMessagesAppViewController {

    @IBOutlet weak var gridView: UIView!
    @IBOutlet var Buttons: [UIButton]!

    var gameStatus = [String](repeating: "-", count: 9)
    var currentPlayer: String = "X"
    var caption = "Want to play Tic-Tac-Toe?"
    var session: MSSession?
.
.
.

In addition to the array declaration, the above changes include a variable in which to temporarily store the current player setting (and which will be placed into the array when the user makes a selection), an initial setting of the message caption and a variable to store the current MSSession instance.

Responding to Button Selections

Each of the game buttons was previously configured to call a method named buttonPressed when tapped. This method needs to identify which button was pressed, store the current player value into the matching element of the game status array and then change the title of the button to indicate that it has been selected by the current player. Within the MessagesViewController.swift file, locate the template buttonPressed method and implement the code as follows:

@IBAction func buttonPressed(_ sender: AnyObject) {
    for (index, button) in Buttons.enumerated() {
        if button.isEqual(sender) {

            if gameStatus[index].isEqual("-") {
                gameStatus[index] = currentPlayer
                sender.setTitle(currentPlayer, for: .normal)
            }
        }
    }
}

When called, this method is passed a reference to the user interface object that triggered the event. The added code iterates through the Buttons outlet collection until it finds the matching button. Using the index value associated with this button, the code then makes sure that the corresponding element in the gameStatus array contains a ‘-‘ character. This indicates that the button grid location has not already been selected. If the button is available, the string value representing the current player is stored at the corresponding location in the gameStatus array and also set as the button title.

Compile and run the message app on a simulator session and verify that clicking on the buttons in the game grid causes an ‘X’ to appear on the clicked button.

Preparing the Message URL

Once the user has selected a game move, the message needs to be prepared and inserted into the iMessage transcript ready to be reviewed and sent by the user. Part of this message takes the form of a URL which will be used to encode the current game state so that it can be reconstructed when the second player receives the message.

For this example, the URLComponents class will be used to build a URL that contains a query item for the current player together with 9 other query items representing the status of each of the button positions in the game grid. Below is an example of how the URL might appear partway through an ongoing game:

https://www.ebookfrenzy.com?currentPlayer=X&position0=X&position1=O&position2=-&position3=-&position4=-&position5=-&position6=X&position7=-&position8=-

The first part of the URL contains the standard HTTP scheme and domain declaration while the rest of the URL is comprised of query items. Each query item is represented by a URLQueryItem instance and contains a key-value pair. As can be seen in the example URL, the first query item contains the key “currentPlayer” which is currently assigned a value of “X”. The remaining query items have keys ranging from position0 through to position8, with the value of each set to an ‘X’, ‘O’ or ‘-‘ to indicate the current status of the corresponding position in the button grid.

The code to create this URL is going to reside within a method named prepareURL which can now be added to the MessagesViewController.swift file so that it reads as follows:

func prepareURL() -> URL {
    var urlComponents = URLComponents()
    urlComponents.scheme = "https";
    urlComponents.host = "www.ebookfrenzy.com";
    let playerQuery = URLQueryItem(name: "currentPlayer", 
				value: currentPlayer)

    urlComponents.queryItems = [playerQuery]

    for (index, setting) in gameStatus.enumerated() {
        let queryItem = URLQueryItem(name: "position\(index)", 
						value: setting)
        urlComponents.queryItems?.append(queryItem)
    }
    return urlComponents.url!
}

The method begins by creating a URLComponents instance and configuring the scheme and host values. A new query item is then created comprising a key-value pair representing the current player information. The code then performs a looping operation through the elements of the gameStatus array. For each element, a new query item is created containing a key-value pair indicating the status of the corresponding grid position which is then appended to the urlComponent object. Finally, the encoded array is returned.

This new method now needs to be called from within the buttonPressed method when a valid button has been selected by the user. Now is also a good opportunity to add a call to a method named prepareMessage which will be created in the next section:

@IBAction func buttonPressed(_ sender: AnyObject) {
    for (index, button) in Buttons.enumerated() {
        if button.isEqual(sender) {

            if gameStatus[index].isEqual("-") {
                gameStatus[index] = currentPlayer
                sender.setTitle(currentPlayer, for: .normal)
                let url = prepareURL()
                prepareMessage(url)
            }
        }
    }
}

Preparing and Inserting the Message

The steps to create the message to be sent will now be implemented within a method named prepareMessage. Add this method as follows to the MessagesViewController.swift file:

func prepareMessage(_ url: URL) {

    let message = MSMessage()

    let layout = MSMessageTemplateLayout()
    layout.caption = caption

    message.layout = layout
    message.url = url

    let conversation = self.activeConversation

    conversation?.insert(message, completionHandler: {(error) in
        if let error = error {
            print(error)
        }
    })

    self.dismiss()
}

The method creates a new MSMessage object and creates a template layout object with the caption set to the current value of the caption variable. The encoded url containing the current game status is then assigned to the url property of the message. Next, a reference to the currently active conversation is obtained before the message is inserted into the iMessage input field ready to be sent by the player. Finally, the MessageViewController is dismissed from view.

Run the message app extension once again and click a button in the grid. Note that the entry now appears in the iMessage input field ready to be sent to the other player. Click on the send button (highlighted in Figure 89-13) to send the message.


iOS iMessage app message ready to send

Figure 89-13


When testing messages in the simulator, the iMessage app simulates a conversation between two users named Kate Bell and John Appleseed. After the message has been sent, click the back arrow in the top left corner of the iMessage screen to move back to the conversation selection screen, select John Appleseed’s conversation entry and note that the message has arrived:


iOS iMessage App first message received

Figure 89-14


Tap the message to load the message extension where the button grid will appear. A few areas of functionality, however, have yet to be implemented. First, the current state of play is not reflected on the buttons, all of which remain blank. Also, clicking on a button causes an ‘X’ character to appear. Since the first player is represented by ‘X’, a current selection should display an ‘O’ on the button. Clearly some code needs to be added to handle the receipt of a message and update the game model within the message app extension.

Message Receipt Handling

The first step in handling the incoming message is to write a method to decode the incoming url and update the gameStatus array with the current status. Within the MessagesViewController.swift file, implement a method for this purpose named decodeURL:

func decodeURL(_ url: URL) {

    let components = URLComponents(url: url, 
			resolvingAgainstBaseURL: false)

    for (index, queryItem) in (components?.queryItems?.enumerated())! {

        if queryItem.name == "currentPlayer" {
            currentPlayer = queryItem.value == "X" ? "O" : "X"
        } else if queryItem.value != "-" {
            gameStatus[index-1] = queryItem.value!
            Buttons[index-1].setTitle(queryItem.value!, for: .normal)
        }
    }
}

This method essentially performs the reverse of the prepareURL method in that it initiates a URLComponents object from a url and then extracts the value for the current player key followed by the current setting for each of the buttons. If the status of a button is not a ‘-‘ character, then the current value (an X or O) is displayed on the corresponding button.

Next, the code to handle the incoming message needs to be implemented in the willBecomeActive method, a template for which has been placed within the MessagesViewController.swift file ready to be completed. When called by the Message framework, this method is passed an MSConversation object representing the currently active conversation. This object contains a property named selectedMessage referencing the MSMessage object selected by the user to launch the extension. From this object, the url containing the encoded game status data can be extracted, decoded and used to update the game status within this instance of the message app.

Locate the willBecomeActive method template in the MessagesViewController.swift file and modify it as follows:

override func willBecomeActive(with conversation: MSConversation) {

    if let messageURL = conversation.selectedMessage?.url {
        decodeURL(messageURL)
        caption = "It's your move!"
    }

    for (index, item) in gameStatus.enumerated() {
        if item != "-" {
            Buttons[index].setTitle(item, for: .normal)
        }
    }
}

The method gets the url that was embedded into the message and passes it to the decodeURL method for decoding and to update the internal game status model. The caption variable is then changed to indicate that this is now an ongoing game before a for loop updates the titles displayed on the game buttons.

Test the game again and note that the current game status is preserved between players and that Os are now displayed when the second player clicks on the grid buttons.

Setting the Message Image

At present, the message bubbles in the iMessage transcript area contain the default app icon and the text assigned to the message caption property. A better user experience would be provided if the image property of the message bubble displayed the current status of the game.

A quick way of achieving this result is to take a screenshot of the gameView View object in which the grid layout resides. This can be achieved by adding some code to the prepareMessage method as follows:

func prepareMessage(_ url: URL) {

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

    UIGraphicsBeginImageContextWithOptions(gridView.bounds.size, 
		gridView.isOpaque, 0);
    self.gridView.drawHierarchy(in: gridView.bounds, 
		afterScreenUpdates: true)

    layout.image = UIGraphicsGetImageFromCurrentImageContext()!;
    UIGraphicsEndImageContext();

    message.layout = layout
    message.url = url

    let conversation = self.activeConversation
    conversation?.insert(message, completionHandler: {(error) in
        if let error = error {
            print(error)
        }
    })
    self.dismiss()
}

This new code designates a graphic context covering the area of the screen containing the gridView object. A graphics rendering of the view hierarchy of which gridView is the parent is then drawn into the context. Finally an image is generated and displayed as the image property of the message layout object.

When the app is tested, the message bubbles should now contain an image showing the current status of the tic-tac-to grid:


iOS interactive iOS message app completed

Figure 89-15

Implementing a Session

After a few messages have been sent back and forth between the players it will become clear that each entry inserted into the game appears in full within the iMessage transcript area. Since only the current state of play of the game matters at any particular time, it would be better if only the current step in the game is displayed fully in the transcript. To implement this behavior, the messages all need to be assigned to the same MSSession object. To implement this, begin by modifying the prepareMessage method. The method first needs to check if a session already exists, create one if it does not, and then reference the session when the MSMessage object is created:

func prepareMessage(_ url: URL) {

    if session == nil {
        session = MSSession()
    }

    let message = MSMessage(session: session!)

    let layout = MSMessageTemplateLayout()
    layout.caption = caption
.
.
.
}

Code now also needs to be added to the willBecomeActive method to extract the current session from the selectedMessage object within the current conversation:

override func willBecomeActive(with conversation: MSConversation) {

    if let messageURL = conversation.selectedMessage?.url {
        decodeURL(messageURL)
        caption = "It's your move!"
        session = conversation.selectedMessage?.session
    }
.
.
}

Run the app one last time, play a few turns of the game with the two test user accounts and note that previous messages are represented by the app icon while only the current message is displayed with the full message bubble and image:


iOS iMessage App Extension using a session

Figure 89-16

Summary

The Message framework allows iMessage app extensions to be integrated into the standard iOS iMessage app. These extensions allow interactive messages to be sent between users. This process involves obtaining a reference to the currently active conversation and the creation and configuration of an MSMessage which is then inserted into the conversation. Data to be transferred with the message may be encoded into a URL using the URLComponents class and then assigned to the URL property of the MSMessage object. This data is then decoded when it is received by another instance of the app extension and used to restore the state of the app. To avoid cluttering the iMessage transcript area, messages may be declared as belonging to the same session.

This chapter has worked through the creation of an example application designed to demonstrate the key steps in developing an interactive iMessage app.