Sharing Data in Apple Watch Applications

Ron 10/24/2016 0
In my previous articles on Apple Watch programming, you learned about the basics of Apple Watch programming and how your watch apps rely on the extension running on your iPhone. As the Apple Watch application is an extension of your iOS app, your watch apps and iOS need to share data constantly. For example, suppose you are writing a weather application. Your iOS app allows users to select a country to find out about its weather. In addition, you want to let users check the weather by using their Apple Watch. In this case, it is important for both apps to be able to access the same set of data.In this article, you will learn how to share data between the iOS and watch apps, and how to architect your apps to facilitate code reuse.

Creating a Shared App Group

In order to share data between the watch app and the containing iOS app, you need to create a shared container known as the shared app group. Follow these steps:
  1. Using Xcode, create a Single View Application and name it SharingData.
  2. Add a WatchKit App target to the project.
  3. Select the project name in Xcode and then the SharingData target, and select the Capabilities tab (see Figure 1). Turn on the App Groups section.
    Figure 1Figure 1 Turning on the App Groups feature of your project.
  4. When prompted, enter your Apple ID account that is enrolled in the iOS Developer Program. Once your account is authenticated, select its Apple ID and click Choose (see Figure 2).
    Figure 2Figure 2 Selecting an account for enabling App Groups.
  5. Click on the plus ( ) button under the App Groups section and enter a name to use for the shared container. I entered the container name shown in Figure 3:
    group.net.learn2develop.articles.sharingdata
    Enter the unique name for your group and click OK.
    Figure 3Figure 3 Entering a name for the shared app group.
  6. Your group name should now appear in the App Groups list (see Figure 4). If you encounter a problem in registering the name, make sure that you have a valid provisioning profile on your Mac. Then click the Refresh button to try again.
    Figure 4Figure 4 The shared app group added to the project.
  7. Repeat the previous steps for the SharingData WatchKit Extension target (see Figure 5). This time, you just need to select the shared group name that you entered earlier.
    Figure 5Figure 5 Adding the shared app group as a WatchKit extension target.
  8. Now let's write the code for the iOS application:
    1. First we'll populate the View window in the Main.Storyboard file with the following views (see Figure 6):
      • Label
      • Picker
      Figure 6Figure 6 Populating the View window in the containing iOS app. (The list of city names is just a placeholder.)
    2. Create an outlet for the Picker view by using the drag-and-drop method (via the Show Editor window). This action will create the statement shown in bold below in theViewController.swift file:
      import UIKit
      
      class ViewController: UIViewController {
      
          @IBOutlet weak var pickerCountries: UIPickerView!
      
          override func viewDidLoad() {
              super.viewDidLoad()
              // Do any additional setup after loading the view, typically
              // from a nib.
          }
      
          override func didReceiveMemoryWarning() {
              super.didReceiveMemoryWarning()
              // Dispose of any resources that can be recreated.
          }
      
      
      }
    3. Add the following statements in bold to the ViewController.swift file to populate thePicker view with a list of countries. When a country is selected, the value of the country will be saved using the NSUserDefaults class:
      import UIKit
      
      class ViewController: UIViewController,
                            UIPickerViewDataSource,
                            UIPickerViewDelegate {
      
          @IBOutlet weak var pickerCountries: UIPickerView!
      
          //---replace "group.net.learn2develop.articles.sharingdata"
          // with the string that you have created earlier---
          var defaults = NSUserDefaults(
              suiteName: "group.net.learn2develop.articles.sharingdata")
      
          var countries: [String]!
      
          override func viewDidLoad() {
              super.viewDidLoad()
              countries = ["Singapore", "Norway", "Japan", "Thailand",
                           "Hong Kong"]
              self.pickerCountries.delegate = self
              self.pickerCountries.dataSource = self
          }
          func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
              return 1
          }
      
          func pickerView(pickerView: UIPickerView,
              numberOfRowsInComponent component: Int) -> Int {
              return countries.count
          }
      
          func pickerView(pickerView: UIPickerView, titleForRow row: Int,
              forComponent component: Int) -> String! {
              return countries[row]
          }
      
          //---read a value from the setting, given a key---
          func readSettingValue(key:String) -> String? {
              return defaults?.objectForKey(key) as? String
          }
      
          //---save a setting value, given the key---
          func saveSettingValue(key:String, value:String) {
              defaults?.setObject(value, forKey: key)
              defaults?.synchronize()
          }
      
          //---when the user selects a country---
          func pickerView(pickerView: UIPickerView, didSelectRow row: Int,
              inComponent component: Int) {
              saveSettingValue("country", value: countries[row])
          }
      
          //---when the view appears---
          override func viewDidAppear(animated: Bool) {
              if let countrysSelected = self.readSettingValue("country") {
                  //---set the picker to display the previously
                  // selected country---
                  self.pickerCountries.selectRow(
                      find(countries, countrysSelected)!,
                      inComponent: 0, animated: true)
              }
          }
      
          override func didReceiveMemoryWarning() {
             super.didReceiveMemoryWarning()
             // Dispose of any resources that can be recreated.
          }
      }
      NOTENotice that you passed the shared app group name as the argument to the initializer for the NSUserDefaults.
    4. Select the SharingData scheme at the top of Xcode and run the application on the iPhone Simulator. Select a country as shown in Figure 7.
      Figure 7Figure 7 Selecting a country from the Picker view.
    5. 

      Coding the Apple Watch App

      Now that the iOS app is done, you can focus on the Apple Watch app. To keep things simple, we will simply display the country that the user has selected in the iOS app. This will prove that the country has been saved correctly in the iOS app and is accessible on the watch app.
      1. Select the Interface.storyboard file and add a Label control to the Interface Controller (see Figure 8). Set the Lines attributes of the Label control to 0.
        Figure 8Figure 8 Populating the Interface Controller with a Label view.
      2. Create an outlet for the Label control. This will create the following statements in bold in the InterfaceController.swift file:
        import WatchKit
        import Foundation
        
        class InterfaceController: WKInterfaceController {
        
            @IBOutlet weak var label: WKInterfaceLabel!
            override func awakeWithContext(context: AnyObject?) {
                super.awakeWithContext(context)
        
                // Configure interface objects here.
           }
        Add the following statements in bold to the InterfaceController.swift file:
        import WatchKit
        import Foundation
        
        class InterfaceController: WKInterfaceController {
        
            @IBOutlet weak var label: WKInterfaceLabel!
        
            //---replace "group.net.learn2develop.articles.sharingdata" with
            // the string that you have created earlier---
            var defaults = NSUserDefaults(
                suiteName: "group.net.learn2develop.articles.sharingdata")
        
            //---read a value from the setting, given a key---
            func readSettingValue(key:String) -> String? {
                return defaults?.objectForKey(key) as? String
            }
        
            override func awakeWithContext(context: AnyObject?) {
                super.awakeWithContext(context)
        
                // Configure interface objects here.
                if let countrySelected = readSettingValue("country") {
        
                    self.label.setText(countrySelected)
                }
            }
      3. Select the SharingData WatchKit App scheme at the top of Xcode and run the application on the iPhone Simulator. You should now see the country name displayed on the Apple Watch Simulator. The country name should be the same as that previously selected on the iPhone Simulator (see Figure 9).
        Figure 9Figure 9 Testing the application on the iPhone and Apple Watch Simulators.
      4. 

        Using a Framework to Contain Repetitive Code

        The ViewController.swift file contains the following block of code:
        var defaults = NSUserDefaults(
            suiteName: "group.net.learn2develop.articles.sharingdata")
        ...
        
        //---read a value from the setting, given a key---
        func readSettingValue(key:String) -> String? {
            return defaults?.objectForKey(key) as? String
        }
        
        //---save a setting value, given the key---
        func saveSettingValue(key:String, value:String) {
            defaults?.setObject(value, forKey: key)
            defaults?.synchronize()
        }
        The InterfaceController.swift file has something similar:
        var defaults = NSUserDefaults(
            suiteName: "group.net.learn2develop.articles.sharingdata")
        ...
        
        //---read a value from the setting, given a key---
        func readSettingValue(key:String) -> String? {
            return defaults?.objectForKey(key) as? String
        }
        In both cases, you need the code to read and write to the settings (although for this exercise the Watch App extension only reads from the settings). Since the code is repetitive, a better idea would be to wrap the method within a framework. Then you can write the code once and call the framework in both targets. This technique would promote code reuse and make your code safer. Follow these steps to wrap the method in a framework:
        1. To add a framework to the project, select File > New > Target. Under the iOS category, select Framework & Library and then select Cocoa Touch Framework (see Figure 10). Click Next.
          Figure 10Figure 10 Adding a framework to the project.
        2. Name the target MySettings (see Figure 11) and then click Finish.
          Figure 11Figure 11 Naming the framework.
        3. Notice that the MySettings and MySettingsTests targets have been added to the project (see Figure 12).
          Figure 12Figure 12 The framework has been added to the project.
        4. The framework that you have just created is automatically added to the SharingDataproject. Verify this by selecting the project name, selecting the SharingData target (see Figure 13), and observing that MySettings.Framework has been added to the project in the Build Phases tab (under the Linked Frameworks and Libraries section).
          Figure 13Figure 13 The Framework is automatically referenced in the iOS app.
        5. You need to add the framework to the WatchKit Extension target if you intend to use it. In the Build Phases tab, click on the plus ( ) button located under the Linked Frameworks and Libraries section (see Figure 14).
          Figure 14Figure 14 Adding a reference to the framework in the WatchKit extension target.
        6. Select MySettings.framework (see Figure 15).
          Figure 15Figure 15 Selecting the framework to add it to the project.
        7. Add a new Swift file to the MySettings target and name it MySettings.swift (see Figure 16).
          Figure 16Figure 16 Adding a new Swift file to the project.
        8. Populate the MySettings.swift file with the following statements in bold:
          import Foundation
          
          public class MySettings: NSObject {
              var defaults = NSUserDefaults(
                  suiteName: "group.net.learn2develop.articles.sharingdata")
              var countries: [String]!
          
              //---read a value from the setting, given a key---
              public func readSettingValue(key:String) -> String? {
                  return defaults?.objectForKey(key) as? String
              }
          
              //---save a setting value, given the key---
              public func saveSettingValue(key:String, value:String) {
                 defaults?.setObject(value, forKey: key)
                 defaults?.synchronize()
              }
          }
          Notice that you need to use the public keyword for the class and function names so that they can be called from both the iOS and WatchKit Extension targets.
        9. The ViewController.swift file can now be simplified, using the MySettings framework:
          import UIKit
          
          import MySettings
          
          class ViewController: UIViewController,
                                UIPickerViewDataSource,
                                UIPickerViewDelegate {
          
              @IBOutlet weak var pickerCountries: UIPickerView!
          
              var countries: [String]!
          
              var mySettings = MySettings()
          
              override func viewDidLoad() {
                  super.viewDidLoad()
          
                  countries = ["Singapore", "Norway", "Japan", "Thailand",
                               "Hong Kong"]
                  self.pickerCountries.delegate = self
                  self.pickerCountries.dataSource = self
              }
          
              func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
                  return 1
              }
              func pickerView(pickerView: UIPickerView,
                  numberOfRowsInComponent component: Int) -> Int {
                  return countries.count
              }
              func pickerView(pickerView: UIPickerView, titleForRow row: Int,
                  forComponent component: Int) -> String! {
                  return countries[row]
              }
          
              //---when the user selects a country---
              func pickerView(pickerView: UIPickerView, didSelectRow row: Int,
                  inComponent component: Int) {
          
                  mySettings.saveSettingValue("country", value: countries[row])
          
              }
          
              //---when the view appears---
              override func viewDidAppear(animated: Bool) {
                  if let countrysSelected = mySettings.readSettingValue("country") {
                      //---set the picker to select the previously selected country---
                      self.pickerCountries.selectRow(
                          find(countries, countrysSelected)!,
                          inComponent: 0, animated: true)
                  }
              }
          
              override func didReceiveMemoryWarning() {
                  super.didReceiveMemoryWarning()
                  // Dispose of any resources that can be recreated.
          
              }
          }
        10. Likewise, the InterfaceController.swift file can be simplified by using theMySettings framework:
          import WatchKit
          import Foundation
          
          import MySettings
          
          class InterfaceController: WKInterfaceController {
          
              @IBOutlet weak var label: WKInterfaceLabel!
          
              var mySettings = MySettings()
          
              override func awakeWithContext(context: AnyObject?) {
                  super.awakeWithContext(context)
          
                  // Configure interface objects here.
                  if let countrySelected = mySettings.readSettingValue("country") {
                      self.label.setText(countrySelected)
          
                  }
              }
          
              override func willActivate() {
                  // This method is called when watch view controller is about to be
                  // visible to user
                  super.willActivate()
              }
          
              override func didDeactivate() {
                  // This method is called when watch view controller is no longer
                  // visible
                  super.didDeactivate()
              }
          }
    In this article, you learned that sharing data between the containing iOS and watch apps requires you to create a shared app group. You can then use the NSUserDefaults class to save key/value pairs in this shared app group. To better promote code reuse, you can use a framework to encapsulate code that is common to both the iOS and watch apps.