An iOS 10 Interactive iMessage App Tutorial
Previous | Table of Contents | Next |
An Introduction to Building iOS 10 iMessage Apps | Using iOS 10 Event Kit to Create Date and Location Based Reminders |
Learn SwiftUI and take your iOS Development to the Next Level |
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.
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:
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:
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:
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):
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.
Learn SwiftUI and take your iOS Development to the Next Level |
Designing the MessageApp User Interface
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:
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:
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:
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:
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:
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.
Learn SwiftUI and take your iOS Development to the Next Level |
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.
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:
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).
Learn SwiftUI and take your iOS Development to the Next Level |
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.
Learn SwiftUI and take your iOS Development to the Next Level |
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=-
Learn SwiftUI and take your iOS Development to the Next Level |
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) } } } }
Learn SwiftUI and take your iOS Development to the Next Level |
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() }
Learn SwiftUI and take your iOS Development to the Next Level |
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.
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:
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.
Learn SwiftUI and take your iOS Development to the Next Level |
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.
Learn SwiftUI and take your iOS Development to the Next Level |
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.
Learn SwiftUI and take your iOS Development to the Next Level |
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:
Learn SwiftUI and take your iOS Development to the Next Level |
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:
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.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
An Introduction to Building iOS 10 iMessage Apps | Using iOS 10 Event Kit to Create Date and Location Based Reminders |