Chapter    7

Building an Interactive watchOS App

Ahmed Bakir

Introduction

In this chapter, you will learn how to make the CarFinder watchOS app even more powerful by adding interactive features. While an app that can let you view information from your iPhone is great, there is even more value in an app that allows you to create new data from your watch. The interactive features you will add to the CarFinder app in this chapter will demonstrate the following features of watchOS:

  • How to add context menus to an interface controller
  • How to add buttons to an interface controller
  • How to add text to an item using text input
  • How to pass data between interface controllers
  • How to pass data back to your iOS companion app

The examples in this chapter expand upon the CarFinder app from Chapter 6. The updated source code for CarFinder is available in the Source Code/Download area on this book’s web page www.apress.com).

Using Force Touch to Present Menus

Force Touch is the feature built into the Apple Watch’s touchscreen, which allows you to detect how much pressure the user applied to the screen. Users can perform different actions depending on whether they press the screen with a light touch (“shallow press” in Apple’s terminology) or a hard touch (“deep press”). The standard User Experience (UX) for Apple Watch apps is to use shallow presses to select an item and Force Touch to bring up a contextual menu, allowing the user to perform related actions. Figure 7-1 provides an example of a contextual menu.

9781484211953_Fig07-01.jpg

Figure 7-1. watchOS contextual menu

In the CarFinder app, you will implement a contextual menu to allow the user to add a new location and to reset the location list. If the user chooses to add a new location, a modal screen will allow him to confirm or reject his current location. If the user chooses to reset the location list, she will be returned to the initial location list interface controller with an empty data set.

To add a contextual menu to an interface controller, open the storyboard for your watchOS app (Interface.storyboard) and drag a Menu from the Object Library onto your desired view controller, as shown in Figure 7-2. For the CarFinder app, the initial interface controller (the InterfaceController class) will be your destination.

9781484211953_Fig07-02.jpg

Figure 7-2. Adding a menu to the main interface controller

Unfortunately, Interface Builder does not give you any feedback on the storyboard to indicate that an interface controller has a menu attached to it. To verify that you have successfully added a menu to your interface controller, select its scene on the storyboard and check to make sure a menu item appears in the view hierarchy, as shown in Figure 7-3.

9781484211953_Fig07-03.jpg

Figure 7-3. Verifying that a menu is in an interface controller’s view hierarchy

You will notice that by default, one menu item appears in the menu. To change the properties of the menu item, click it in the view hierarchy and navigate to the Attributes inspector in Interface Builder (the fourth tab in the right pane). From here, you can assign a new name to the menu item and change the icon, as shown in Figure 7-4.

9781484211953_Fig07-04.jpg

Figure 7-4. Modifying a menu item

Just as with bar button items and tab bar items, Apple provides a wide set of pre-rendered icons for you to use in your menu items. To use your own custom icon, follow the same rules you would use for a tab bar or bar button item:

  • Create an Image Set entry for your icon in your project’s Assets Library (Assets.xcasssets)
  • Makes sure the icon is a PNG with an alpha layer
  • Make sure the icon is monotone
  • Make sure the icon is anti-aliased (smoothed to remove bitmap “jaggedness”)

You do not need to manage an object in your interface controller class to use a context menu; however, you do need to define handler methods for the menu items, as for UIButtons in iOS.

As shown in Listing 7-1, expand the InterfaceController class by adding IBAction methods for the menu item actions.  The resetLocations() method will be used to clear the saved location list. The requestLocations() method will be used to add a new location. CoreLocation on watchOS is not designed to continuously monitor location, as it would drain the watch’s battery too quickly, so you need to manually request a location based on a user action.

As with button actions, in order to tie a menu item to a handler method, you need to use the Connection Inspector (last tab of right panel in Interface builder). As shown in Figure 7-5, drag a line from the selector radio box to the CarFinder scene. A pop-up will appear allowing you to choose the requestLocation() or resetLocations() method.

9781484211953_Fig07-05.jpg

Figure 7-5. Connecting a menu item to a selector

You can verify that the operation was successful by checking that the Connection Inspector has linked the bubbles in the Sent Actions section, as indicated in Figure 7-6.

9781484211953_Fig07-06.jpg

Figure 7-6. Verifying connections have been set

Follow this same process to connect the reset button to the resetLocations() method.

Resetting the Location List

After selecting an item in a context menu, the menu disappears and takes the user back to the presenting interface controller. For the reset menu item, you want to take the user back to the location list, with an empty set of contents. Since the data source for the location list is an array, you can reset the contents simply by clearing out the array.

However, clearing out the array is not enough to refresh the user interface (UI). You may remember from Chapter 6 that watchOS tables do not have a reloadData() method like UITableViews on iOS. To refresh a table in watchOS, you need to reset the number of rows and rebuild the cells. Luckily, the configureRows() method does both of these operations for us. As shown in Listing 7-2, using your resetLocations() method, after clearing the location array, you can rebuild the table using the configureRows() method.

Presenting a Detail View Controller

Although it would be extremely convenient to present interface controllers from menu items via a segue in Interface Builder, as of this writing, that has not yet been implemented in watchOS. To present an interface controller from a menu item, you need to use the method presentControllerWithName(_:context:), specifying a name (storyboard identifier) for the interface controller you want to present and information you want to pass along to this interface controller via a context object.

To begin, you need to add an interface controller to your storyboard that represents the confirm screen. As shown in Figure 7-7, drag a new Interface Controller object to your storyboard.

9781484211953_Fig07-07.jpg

Figure 7-7. Adding a Details interface controller

To represent this interface controller in code, create a subclass of WKInterfaceController, named ConfirmInterfaceController. Listing 7-3 provides the definition for the ConfirmInterfaceController class, including the properties for the UI.

To connect the code and storyboard, remember that you need to set a parent class and storyboard identifier. To set the parent class, click the scene for the Interface Controller and click the Identity Inspector in Interface Builder. As shown in Figure 7-8, set the parent class to ConfirmInterfaceController.

9781484211953_Fig07-08.jpg

Figure 7-8. Setting parent class for an Interface Controller

To set the storyboard as the storyboard identifier and (optionally) the title for the Interface Builder, click the Attributes Inspector. As shown in Figure 7-9, use ConfirmInterfaceController identifier.

9781484211953_Fig07-09.jpg

Figure 7-9. Setting the storyboard identifier for an Interface Controller

Having established the Details interface controller, you can now rest assured that calling the presentControllerWithName() method will function correctly, given the storyboard identifier ConfirmInterfaceController. Listing 7-4 provides the definition for the requestPermission() method, which at this point presents the confirm interface controller from the main interface controller. Place this code in your InterfaceController.swift file.

Simulating Force Touch

While it is possible to debug Force Touch primarily by running it on your watch, it could be a potentially time-consuming effort, due to the time required to install a watchOS app and establish a debugging session. By default, all touches in the watchOS simulator are treated as shallow presses. You can simulate deep press events by changing the Force Touch Pressure in the simulator. To modify this setting, go to the Hardware Menu in the watchOS simulator and select Force Touch Pressure, as shown in Figure 7-10.

9781484211953_Fig07-10.jpg

Figure 7-10. Enabling deep press events

Unfortunately, once you toggle the setting, all touches will retain it. This could have some negative side effects, as a deep press on a menu dismisses it. During your debugging session, remember to keep switching the mode between deep press and shallow press to simulate realistic user interaction.

Adding Buttons to an Interface Controller

Now that you are able to present the interface controller for the confirm screen, you need a way of exiting it, either by confirming that the location is correct or by dismissing the view. For the CarFinder application, you will perform these actions using the WKInterfaceButton class, which is intended to provide similar functionality to a UIButton in iOS. As watchOS is a subset of iOS, you cannot catch touch events to the same level of granularity as an iOS app, such as “touch down repeat.” However, you can trigger a method via a selector or perform a segue when the button is pressed.

Apple suggests that when you plan to use buttons in your watchOS apps, you opt for a purely vertical layout. You can also place buttons into a horizontal layout, but I suggest two at most, since the touch area on the Apple Watch is so small. For the interface of the ConfirmInterfaceController, you will use a purely vertical layout to contain all of the UI items. Start by adding a Group object to your Interface Controller. As shown in Figure 7-11, set the layout to Vertical.

9781484211953_Fig07-11.jpg

Figure 7-11. Setting vertical layout on a group

Drag three buttons and two labels onto the Group, as shown in Figure 7-12. You do not need to connect the buttons to properties in your class, but make sure you connect the Note and Coordinates label to the noteLabel and coordinatesLabel properties via the Connection Inspector.

9781484211953_Fig07-12.jpg

Figure 7-12. Final storyboard layout for ConfirmInterfaceController

As with action sheets on iOS, the prevailing design standard for buttons in watchOS is to differentiate the cancel or dismiss button by changing its background color. As shown in Figure 7-13, you can change the background color of a button in watchOS by selecting the button in your scene and navigating over to the Attributes Inspector.

9781484211953_Fig07-13.jpg

Figure 7-13. Changing the background color for a button

As with UIButtons in iOS, you need IBAction-enabled handler methods to perform actions from a WKInterfaceButton in watchOS. Listing 7-5 shows the handler methods for the buttons. At this time, both buttons dismiss the confirm interface controller via the dismissController() method of the WKInterfaceController class. To pass data back to the location table, you will eventually add a call to a delegate method as part of the confirm action.

Finally, connect the handler methods and button objects using Connections Inspector in Interface Builder. As shown in Figure 7-14, make your connections from the Sent Action selector.

9781484211953_Fig07-14.jpg

Figure 7-14. Connecting button actions

Passing Information Between Interface Controllers

Having established a way to present the confirm interface controller, you now need a way to initialize it with real data. For the CarFinder application, you will want to show users their current location, so they can save the entry or cancel the prompt. I will cover the specifics of how to use CoreLocation natively on the watchOS application in the section “How to Add Notes Using Text Input”; however, for now, you can assume that you will need to pass an object that contains latitude and longitude data.

Remember that earlier in this chapter, you used the method presentControllerWithName(_:context:), to present the confirm interface controller with a storyboard identifier. The other parameter you left blank was context. All subclasses of WKInterfaceController implement an awakeWithContext() method, which responds to “waking” the view controller with a context, or a data object that you can safely pass between interface controllers. By generating a valid context object from the location list and overriding the awakeWithContext() method in the ConfirmInterfaceController class, you can initialize the confirm interface controller with location data.

To begin, you need an object that contains the user’s location. I have chosen to implement this by adding a CLLocation object to the InterfaceController class and initializing it with a known location. This adds an extra layer of safety if the user denies the app location permission, or if there is another issue resolving the user’s location. Listing 7-6 provides the modified definition for the InterfaceController class, adding a CLLocation object to the InterfaceController class.

Although the type for the context parameter specifies AnyObject, in practice, you are limited in the types you are allowed to use, primarily primitive data types like strings or numbers. You can send multiple pieces of data over by combining them into an array or dictionary; however, as of this writing, it is not possible to pass a CLLocation object. To resolve this issue, you need to create a dictionary containing the user’s latitude and longitude as double values. You can extract these from the coordinates property of a CLLocation object. Once you have built the dictionary, you can pass it over using the presentControllerWithName() method, as shown in Listing 7-7.

In the ConfirmInterfaceController class, you complete the process by overriding the awakeFromContext() method. For now, all you need to do when you receive the context data is initialize the coordinatesLabel by extracting the appropriate values out of the dictionary, as shown in Listing 7-8.

Using a Delegate to Pass Information on Dismissal

As you have just learned, it is rather straightforward to pass data to an interface controller while presenting it. Unfortunately, Apple does not provide a method this convenient for passing back data when dismissing an interface controller in watchOS. However, by taking advantage of delegation you can create your bridge between the classes.

The driving concepts behind delegation are that you specify a class to “delegate” a piece work to and define the messages that will be passed back through a protocol. The class that requests the work declares itself as implementing the protocol and sets itself as the delegating object, allowing it to respond to messages from the outsourced class. Delegates are perfect for establishing a message-passing scheme between two classes, without specifying all the details of implementation.

For the CarFinder app, the data we are interested in passing back from the confirm interface controller is whether the user decided to save the location. The location list delegates the work of making the determination to the confirm interface controller, so you need to define your protocol there.

Protocols are defined by specifying a protocol block with the protocol name and a list of methods that its delegate needs to implement. Listing 7-9 provides the protocol definition for the ConfirmInterfaceController class. Protocol blocks are defined before classes, as the corresponding class includes a property placed on the protocol.

To call methods via a protocol, you need to implement a delegate property on your class. You can then call methods from the protocol on this property, which will be sent to the class that has declared itself as your delegate. In Listing 7-10, I have added the delegate property to the ConfirmInterfaceController class; it is an optional variable with the protocol name as its type.

For the final major piece of the implementation in the ConfirmInterfaceController, call the saveLocation() delegate method. As shown in Listing 7-11, make this call part of the confirm() handler method, before dismissing the interface controller.

The implementation is much easier in the class that is delegating work, InterfaceController. In this class, you need to do the following:

  • Declare that you are implementing the ConfirmDelegate protocol
  • Provide an implementation for the methods that the protocol exposes (saveLocation())
  • Let the ConfirmInterfaceController know that you are the delegate.

To declare that the InterfaceController is implementing the ConfirmDelegate protocol, add the protocol name to the class signature, with a comma separating it from the parent class name, as shown in Listing 7-12.

The compiler will immediately throw an error stating that the InterfaceController does not implement all of the methods of the ConfirmDelegate protocol. To resolve this issue, implement the saveLocation() method. When the user has confirmed that he wants to save his current location, append the current location to the locations array and refresh the table. Listing 7-13 provides the implementation for the saveLocation() method.

Finally, to receive messages, you need a way of specifying that the InterfaceController object associates with the delegate property of the ConfirmInterfaceController class. In iOS, you would use the instantiateViewController() method on a UIStoryboard object to make this connection; however, this method is not available on watchOS. But, you can pass the pointer along by adding it to your context dictionary. In Listing 7-14, I have modified the requestLocation() method to include a delegate key.

To extract this value from the context dictionary, go back to the ConfirmInterfaceController. In the awakeForContext method, verify that the value exists and that it has the type ConfirmDelegate. As shown in Listing 7-15, once this check has passed, you can set the property.

You are now able to pass messages upon dismissing the confirm interface controller!

On a closing note, did you like my pun? Driving concepts? CarFinder? Ha, ha, ha!

How to Add Notes Using Text Input

To illustrate another way of accepting user input in your application, you will learn how to add text input. This feature brings up a modal, shown in Figure 7-15, which allows the user to add text in the form of an emoji, a pre-string string, or through Siri text-to-speech recognition.

9781484211953_Fig07-15.jpg

Figure 7-15. Text input modal

For the CarFinder application, you will want to trigger this modal when the user clicks the “Add Note” button. To help the user, you should pre-populate the picker with strings that relate to location notes, such as “next to light pole” or “across from house.”

Note  You need to use an Apple Watch to test text input, as the simulator does not support Siri.

To present the text input modal, use the method, presentTextInputControllerWithSuggestions(_:allowedInputMode:), which allows you to specify an array of suggestion strings, limits on the types of input accepted (e.g., no emoji), and a completion handler. Listing 7-16 provides the implementation for the CarFinder app. As a reminder, this goes in the ConfirmInterfaceController class.

The String type in Swift allows a wider character set than NSString in Objective-C, so you do not need to set any limits on the input type. Emoji will display in-line when provided as input to a string.

As another reminder, remember that you need to set text for the noteLabel on the main thread, as UI updates only execute on the main thread.

To pass the note back to the InterfaceController, expand the ConfirmDelegate protocol’s saveLocation() method to include a “Note” parameter. Listing 7-17 provides the modified protocol declaration. Place this code in ConfirmInterfaceController.swift.

Listing 7-18 includes the modified confirm() method for the ConfirmInterfaceController class, which pulls in the note that was saved earlier.

You will implement the saveLocation() delegate method in the InterfaceController class, which will allow you to extract the note as an input parameter.

Sending Data Back to the Parent iOS App

For the final piece of the interactive version of CarFinder, you will send locations that the watch app created back to the parent iOS app. Once again, you can rely on your old friend WatchConnectivity to help communicate between your watchOS app and its parent iOS app.

While the primary method for sending data to the Apple Watch from an iOS app is through the updateApplicationContext() method on a WKSession, there are a few more options available for sending information from the Apple Watch to its parent iOS application. Table 7-1 provides an overview of these methods and their intended uses.

Table 7-1. Methods for Transferring Data from the Apple Watch to an iOS Application

Method

Purpose

sendMessage:replyHandler:errorHandler:

Send a context containing data to the parent app immediately. Queue any old versions of the dictionary that have not yet been processed.

transferFile:metadata:

Transfer a file to the parent app.

transferUserInfo:

Transfer a dictionary to the parent app. Queue any old versions of the dictionary that have not yet been processed.

updateApplicationContext:error:

Transfer a dictionary to the parent app. Discard any old versions of the dictionary that have not yet been processed.

For the CarFinder application, you want to post location updates as the user creates them. The messages will be infrequent, but they need to be queued and delivered in order. For this reason, you will use the sendMessage() method to post location updates back to the companion iOS app.

In the flow of the CarFinder app, the save action happens in the saveLocation() method of the InterfaceController class. At this point, you have a context dictionary containing the latitude and longitude information for a new location. The sendMessage() method takes a dictionary as input; it would make sense to pass the input dictionary directly to the iOS app, which has its own logic for adding a location based on latitude and longitude. Listing 7-19 provides the modified saveLocation() method, which includes the sendMessage() call.

You will notice the completion handlers for the error and reply states are very light in my example. In general, you should not show an error alert unless the failing action will hinder the user’s experience with the app. In this example, I did not implement any custom logic for the reply handler because the watchOS UI does not update based on a successful post to the iOS app.

To receive a message from a watchOS app in an iOS app, you need to expand your AppDelegate class, which handles external events and launching your app, to handle watchKit extension messages. To handle the watchKit extension messages, you need to implement the delegate method func application(application: UIApplication, handleWatchKitExtensionRequest userInfo:reply:), which receives a dictionary containing information from the watchOS app and a reply method signature, which you can use to send a reply back to the watchOS app. This allows you to send a confirmation back to the app or update the UI on the watch.

Receiving the message in the iOS parent app is one thing, but in order to do something with it, you need to send a message to the location list, represented by the FirstViewController class. Best practices in Apple development suggest against creating a singleton or maintaining pointers to view controllers in your app delegate. However, you can use a much more generic message-passing method to get the update to the FirstViewController class: notifications.

With notifications, you specify a name for your message (the notification name) and post a message using that name. Generally, the data is transferred in a dictionary, which is extremely convenient, because your input is also a dictionary. A notification is sent to anyone who wants to listen. A class will declare itself as an “observer” of a notification and specify a selector or completion handler that should execute when the notification is received.

Listing 7-20 provides the handleWatchKitExtensionRequest() method for the AppDelegate, including the call to post a notification. Place this code in AppDelegate.swift.

To observe the notification, in the FirstViewController’s viewDidLoad() method, implement the addObserver() method. As shown in Listing 7-21, I have chosen to use a completion handler to process the notification.

Now that you have added interactive features to CarFinder, your app should look like Figure 7-16. The new app retains the location list from the original CarFinder app, while adding an expanded details page and menu options to add and delete locations.

9781484211953_Fig07-16.jpg

Figure 7-16. Expanded user interface for interactive CarFinder app

Summary

In this chapter, you learned how to make a watchOS app interactive by adding the ability to create a new location from the watch, add notes using text input, and post the new item back to the parent app. Along the way, you learned that much of the battle lies in properly passing data between interface controllers and to the parent app. You saw that you could use an application context to post data to an interface controller while presenting it, delegates to post data after dismissing an interface controller, and the WatchConnectivity class to post information back to your parent iOS app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.142.250