A watchOS 2 WatchConnectivity Messaging Tutorial

Revision as of 13:44, 22 June 2015 by Neil (Talk | contribs) (New page: <br><br><br> The previous chapter explored the use of the different forms of communication provided by the WatchConnectivity framework to enable a WatchKit app to communicate with the cor...)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Revision as of 13:44, 22 June 2015 by Neil (Talk | contribs) (New page: <br><br><br> The previous chapter explored the use of the different forms of communication provided by the WatchConnectivity framework to enable a WatchKit app to communicate with the cor...)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)




The previous chapter explored the use of the different forms of communication provided by the WatchConnectivity framework to enable a WatchKit app to communicate with the corresponding iOS app. The tutorial outlined in this chapter will make use of the interactive messaging Watch Connectivity feature to control the playback of audio on the iPhone device from a WatchKit app running on a paired Apple Watch.

About the Project

The project created in this chapter will consist of two parts. The first is an iOS application that allows the user to playback music and to adjust the volume level from an iPhone device. The second part of the project involves a WatchKit app designed to allow the user the same level of control over the audio playback on the iPhone from the paired Apple Watch device. The communication back and forth between the WatchKit and iOS apps will be implemented entirely using the interactive messaging feature of the WatchConnectivity framework.

Creating the Project

Start Xcode and create a new iOS project. On the template screen choose the Application option located under watchOS in the left hand panel and select iOS App with WatchKit App from the main panel. Click Next, set the product name to WatchConnectApp, enter your organization identifier and make sure that the Devices menu is set to Universal. Before clicking Next, change the Language menu to Swift and turn off each of the Include options. On the final screen, choose a location in which to store the project files and click on Create to proceed to the main Xcode project window.


Enabling Audio Background Mode

When the user begins audio playback it should not stop until either the user taps the stop button or the end of the audio track is reached. To ensure that the iOS app is not suspended by the operating system the Audio background mode needs to be enabled. Within Xcode, select the WatchConnectApp target at the top of the Project Navigator panel, select the Capabilities tab and switch the Background Modes option from Off to On. Once background modes are enabled, enable the checkbox next to Audio and Airplay as outlined in Figure 14-1:


Enabling iOS background audio mode

Figure 14-1


With this mode enabled, the background iOS app will not be suspended as long as it continues to play audio. It will also allow the iOS app to be launched in the background and begin audio playback at the request of the WatchKit app.

Designing the iOS App User Interface

The user interface for the iOS app will consist of a Play button, a Stop button and a slider with which to control the volume level. Locate and select the Main.storyboard file in the Xcode Project Navigator panel and drag and drop two Buttons and one Slider view onto the scene canvas so that they are centered horizontally within the scene. Double click on each button, changing the text to “Play” and “Stop” respectively, and stretch the slider so that it is slightly wider than the default width. On completion of these steps the layout should resemble that of Figure 14-2:


The layout of the iOS app for the WatchConnectivity tutorial

Figure 14-2


Using the Resolve Auto Layout Issues menu (indicated in Figure 14-3) select the Reset to Suggested Constraints option to configure appropriate layout behavior for the three views in the scene:


Setting layout constraints

Figure 14-3


Establishing Outlets and Actions

With the Main.storyboard file still loaded into Interface Builder, display the Assistant Editor panel and verify that it is displaying the content of the ViewController.swift file. Ctrl-click on the Slider object in the storyboard scene and drag the resulting line to a position immediately beneath the class declaration line in the Assistant Editor panel. On releasing the line, establish an outlet named volumeControl in the connection panel.

Repeat these steps on both of the Button views, this time establishing action connections to methods named playAudio and stopAudio respectively.

Finally, establish an action connection for the Slider view to a method named sliderMoved based on the Value Changed event. On completion of these steps the ViewController.swift file should read as follows:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var volumeControl: UISlider!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    @IBAction func playAudio(sender: AnyObject) {
    }

    @IBAction func stopAudio(sender: AnyObject) {
    }

    @IBAction func sliderMoved(sender: AnyObject) {
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Initializing Audio Playback

Before sounds can be played within the iOS app a number of steps need to be taken. First, an audio file needs to be added to the project. The music to be played in this tutorial is contained in a file named vivaldi.mp3 located in the audio_files folder of the sample code download available from the following URL:


http://www.ebookfrenzy.com/watchkit/audio_files.zip


Locate the file in a Finder window and drag and drop it beneath the WatchConnectApp folder in the Project Navigator panel as shown in Figure 14 4, clicking on the Finish button in the options panel:


Adding the Audio file to the iOS app folder

Figure 14-4


With the audio file added to the project, code now needs to be added to the viewDidLoad method of the ViewController.swift file to initialize an AVAudioPlayer instance so that playback is ready to start when the user taps the Play button. Select the ViewController.swift file and modify it to import the AVFoundation framework, declare AVAudioSession and AVAudioPlayer instance variables and to initialize the player:

import UIKit
import AVFoundation
import MediaPlayer

class ViewController: UIViewController {

    var audioSession: AVAudioSession = AVAudioSession.sharedInstance()
    var audioPlayer: AVAudioPlayer?

    @IBOutlet weak var volumeControl: UISlider!

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            try audioSession.setCategory(AVAudioSessionCategoryPlayback)
            url = NSURL.fileURLWithPath(
                NSBundle.mainBundle().pathForResource("vivaldi",
                    ofType: "mp3")!)
        } catch {
            print("AudioSession error")
        }

        do {
            try audioPlayer = AVAudioPlayer(contentsOfURL: url!, 
				fileTypeHint: nil)
            audioPlayer?.prepareToPlay()
            audioPlayer?.volume = 0.1
        } let error as NSError {
            print("Error: \(error.description)")
        }
    }
.
.
}

The code configures the audio session to allow audio playback to be initiated from the background even when the device is locked and the ring switch on the side of the device is set to silent mode. The audio player instance is then configured with the mp3 file containing the audio to be played and an initial volume level set.

Implementing the Audio Control Methods

With the audio player configured and initialized, the next step is to add some methods to control the playback of the music. Remaining within the ViewController.swift file, implement these three methods as follows:

func stopPlay() {
    audioPlayer?.stop()
}

func startPlay() {
    audioPlayer?.play()
}

func adjustVolume(level: Float)
{
    audioPlayer?.volume = level
}

Each of these methods will need to be called by the corresponding action methods:

@IBAction func playAudio(sender: AnyObject) {
    startPlay()
}

@IBAction func stopAudio(sender: AnyObject) {
    stopPlay()
}

@IBAction func sliderMoved(sender: AnyObject) {
    adjustVolume(volumeControl.value)
}

Compile and run the iOS app and test that the user interface controls allow playback to be started and stopped via the two buttons and that the slider provides control over the volume level. With the iOS app now functioning, it is time to initialize the WatchConnectivity session for the iOS app.

Initializing the iOS App Watch Connectivity Session

The Watch Connectivity session needs to be initialized as early as possible in the app lifecycle. This process involves checking that the Apple Watch is paired and that the corresponding WatchKit app is installed on the watch device. For the purposes of this example, the code to perform these tasks will be added to the didFinishLaunchingWithOptions method located in the AppDelegate.swift file as follows, making sure to import the WatchConnectivity framework and to declare the class as implementing the WCSessionDelegate protocol:

import UIKit
import WatchConnectivity

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

    var window: UIWindow?

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        if (WCSession.isSupported()) {
            let session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()

            if session.paired != true {
                print("Apple Watch is not paired")
            }

            if session.watchAppInstalled != true {
                print("WatchKit app is not installed")
            }
        } else {
            print("WatchConnectivity is not supported on this device")
        }

        return true
    }

Designing the WatchKit App Scene

Select the Interface.storyboard file located under WatchConnectApp WatchKit App so that the storyboard loads into Interface Builder. Drag and drop a Label, two Buttons and a Slider from the Object Library onto the scene canvas so that the layout matches that shown in Figure 14-5:


Designing the scene layout for the Watch Connectivity example WatchKit app

Figure 14-5


Select the Label object, display the Attributes Inspector panel and set the Alignment property to center the displayed text. Within the Position section of the panel, change the Horizontal menu to Center.

Double click on the uppermost of the two buttons and change the text to “Play”. Repeat this step for the second button, this time changing the text so that it reads “Stop”.

Select the Slider object and, in the Attributes Inspector panel, change both the Maximum and Steps properties to 10.

On completion of the above steps, the scene layout should resemble Figure 14-6:


The scene layout for the Watch Connectivity example WatchKit app

Figure 14-6


Display the Assistant Editor and verify that it is showing the contents of the InterfaceController.swift file. Using the Assistant Editor, establish an outlet connection from the Label object in the user interface named statusLabel. Next, create action connections from the two buttons named startPlay and stopPlay respectively and an action connection from the slider named volumeChange. With these connections established, the top section of the InterfaceController.swift file should read as follows:

import WatchKit
import Foundation

class InterfaceController: WKInterfaceController {

    @IBOutlet weak var statusLabel: WKInterfaceLabel!

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.
    }

    @IBAction func startPlay() {
    }

    @IBAction func stopPlay() {
    }

    @IBAction func volumeChange(value: Float) {
    }
.
.
}

Initializing the WatchKit App Connectivity Session

Having initialized the Watch Connectivity session from the iOS app side, it is now time to initialize the session for the WatchKit app. The ideal location in which to place this initialization code is within the applicationDidFinishLaunching method of the extension delegate class. Within the Project Navigator panel, select the ExtensionDelegate.swift file located under the WatchConnectApp WatchKit Extension entry and modify the code to import the WatchConnectivity framework, declare the class as implementing the WCSessionDelegate protocol and to initialize the session:

import WatchKit
import WatchConnectivity

class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {

    func applicationDidFinishLaunching() {
        if (WCSession.isSupported()) {
            let session = WCSession.defaultSession()
            session.delegate = self
            session.activateSession()
        }
    }

Sending the Message to the iOS app

Now that the WatchKit app user interface is wired up to methods in the interface controller class, the next step is to implement the calls to the sendMessage method in those action methods.

Each sendMessage method call will include a dictionary consisting of a key named “command” and a value of either “play”, “stop” or “volume”. In the case of the volume command, an additional key-value pair will be provided within the dictionary with the value set to the current value of the slider. The sendMessage method calls will also declare and pass through replyHandler and errorHandler closures. This is essentially a block of code that will be called and passed data by the iOS application once the message request has been handled.

Within the InterfaceController.swift file, implement this code within the action methods so that they read as follows, noting that in each case a check is made to ensure that the iOS app is still reachable:

import WatchConnectivity
.
.
.
@IBAction func startPlay() {
    if WCSession.defaultSession().reachable == true {

        let requestValues = ["command" : "start"]
        let session = WCSession.defaultSession()
            
        session.sendMessage(requestValues, replyHandler: { reply in
            self.statusLabel.setText(reply["status"] as? String)
            }, errorHandler: { error in
                print("error: \(error)")
        })
    }
}

@IBAction func stopPlay() {
    if WCSession.defaultSession().reachable == true {

        let requestValues = ["command" : "stop"]
        let session = WCSession.defaultSession()
            
        session.sendMessage(requestValues, replyHandler: { reply in
            self.statusLabel.setText(reply["status"] as? String)
            }, errorHandler: { error in
                print("error: \(error)")
        })
    }
}

@IBAction func volumeChange(value: Float) {
    let requestValues = ["command" : "volume", "level" : value/10]
    let session = WCSession.defaultSession()

    session.sendMessage(requestValues as! [String : AnyObject], 
	replyHandler: { reply in
        self.statusLabel.setText(reply["status"] as? String)
        }, errorHandler: { error in
            print("error: \(error)")
    })
}

Note that the slider will contain a value between 0 and 10. Since the AVAudioPlayer class has a range of 0.0 to 1.0 for the volume level, the slider value is divided by 10 before being passed to the parent application.

The reply handler closure expects as a parameter a dictionary object. In the case of this example, the closure code simply extracts the string value for the “status” key from the reply dictionary and displays it on the status Label object in the main WatchKit app scene.

Handling the Message in the iOS app

When the sendMessage method is called, the parent iOS application will be notified via a call to the session:didReceiveMessage:replyHandler method within the application delegate class. The next step in this tutorial is to implement this method.

Locate and select the AppDelegate.swift file in the Project Navigator panel so that it loads into the editor. Once loaded, add an implementation of the session:didReceiveMessage:replyHandler method as follows:

func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {

    var replyValues = Dictionary<String, AnyObject>()

    let viewController = self.window!.rootViewController
        as! ViewController

    switch message["command"] as! String {
    case "start" :
        viewController.startPlay()
        replyValues["status"] = "Playing"
    case "stop" :
        viewController.stopPlay()
        replyValues["status"] = "Stopped"
    case "volume" :
        let level = message["level"] as! Float
        viewController.adjustVolume(level)
        replyValues["status"] = "Vol = \(level)"
    default:
        break
    }
    replyHandler(replyValues)
}

The code begins by creating and initializing a Dictionary instance in which to store the data to be returned to the WatchKit Extension via the reply handler. Next, a reference to the root view controller instance of the iOS app is obtained so that the playback methods in that class can be called later in the method.

One of the arguments passed through to the session:didReceiveMessage:replyHandler method is a dictionary named userInfo. This is the dictionary that was passed through when the sendMessage method was called from the WatchKit app extension (in other words the requestValues dictionary declared in the extension action methods). The method uses a switch statement to identify which command has been passed through within this dictionary. Based on the command detected, the corresponding method within the view controller is called. For example, if the “command” key in the userInfo dictionary has the string value “play” then the startPlay method of the view controller is called to begin audio playback. The value for the “status” key in the replyValues dictionary is then configured with the text to be displayed via the status label in the WatchKit app scene.

Also passed through as an argument to the session:didReceiveMessage:replyHandler method is a reference to the reply closure declared as part of the sendMessage method call. The last task performed by the session:didReceiveMessage:replyHandler method is to call this closure, passing through the replyValues dictionary. As previously described, the reply closure code will then display the status text to the user via the previously declared statusLabel outlet.

Testing the Application

In the Xcode toolbar, make sure that the run target menu is set to WatchConnectApp WatchKit App before clicking on the run button. Once the WatchKit app appears, click on the Play button in the main interface controller scene. The status label should update to display “Playing” and the music should begin to play within the iOS app. Test that the slider changes the volume and that the Stop button stops the playback. In each case, the response from the parent iOS app should be displayed by the status label.

Summary

This chapter has created an example project intended to demonstrate the use of the WatchConnectivity messaging capabilities to launch and communicate with the iOS companion application of a WatchKit app. The example has shown how to initialize a Watch Connectivity session, call the sendMessage method and implement the session:didReceiveMessage:replyHandler method within the app delegate of the parent iOS app.