Integrating Core Data with WatchOS

I've been somewhat underwhelmed by the Apple Watch's load times and responsiveness since I bought one at launch last year. But now that it's late 2016, things have improved. WatchOS 3 offers a somewhat snappier experience than before. While it's not as fast as the Series 2 demoed earlier this month, I'm happy with the progress.

Inspired by some new iOS 10 APIs, particularly those in Core Data, I decided to try out the WatchKit framework. In the past, developers needed to write their own Core Data stack to make sense of managed object contexts. The new NSPersistentContainer simplifies this process by encapsulating the managed object model within the class. Even creating in-memory persistent stores for unit testing is easier with NSPersistentContainer.

With both WatchOS and Core Data in mind, I wanted to explore the ease of creating a Core Data stack and utilizing existing WatchOS APIs for watch connectivity. I'll build a simple app with a table view controller and an alert prompt for adding favorite snacks. The first goal will be to sync the table view between the app and the watch, propagating additions and deletions to both.

Watch and App Communication Design

When creating a WatchKit extension, you get two targets: the WatchKit app (containing UI storyboards) and the WatchKit extension (housing the app's domain logic).

To pass data between the app and the watch, both need to conform to the Watch Connectivity delegate and begin a WatchKit session before attempting to send data. Rather than putting all connectivity logic in the app and extension delegates, I'll create separate classes: PhoneToWatchService for the app and WatchToPhoneService for the watch. We'll also introduce a 'Recipe' object model for our Core Data data.

Midway Learnings and Tips

  • The watch cannot open its parent application if it's suspended. The iOS app needs to be at least running in the background for the watch to communicate with it.
  • Unlike an iOS app's AppDelegate, the rootInterfaceController in a watch app is not yet initialized in the extension's applicationDidFinishLaunching. This means you can't inject dependencies into a root view controller in the same way.
  • The root interface controller is located in the app’s main storyboard and has the Main Entry Point object associated with it. WatchKit displays the root interface controller at launch time, although the app can present a different interface controller before the launch sequence finishes.

I'll dive deeper into the watch extension's lifecycle. In the meantime, to work around the root interface controller issue, I'll check for its existence every time I receive data from the iOS app, setting the WatchToPhoneService delegate once it's available.

Passing Managed Object Data to Watch

There are several ways to pass data between the watch and the app. The key is that watch connectivity APIs use dictionaries of property list types (strings, integers, floats, and data). This is a limitation if we want to fetch Recipe managed objects from the app. However, I'll create a WatchRecipe type alias that's a dictionary representation of our Recipe object model. Since the Recipe has only a 'name' attribute, the mapping is straightforward.

let watchRecipes = recipes.map({ (recipe) -> WatchRecipe in
  return ["name": recipe.name as AnyObject]
})

After sending a watch recipe over, updating the watch table view is simple. Note that the WatchKit approach to table views is different than standard iOS. You don't conform to UITableViewDelegate or UITableViewDataSource. Instead, you set the number of rows and the "RowType" identifier in one method. Also, table rows are called 'rowControllers'.

recipeTableView.setNumberOfRows(recipes.count, withRowType: "RecipeRowType")

for (index, recipe) in recipes.enumerated() {
  let controller = recipeTableView.rowController(at: index) as! RecipeRowController

  if let recipeName = recipe["name"] as? String {
    controller.titleLabel.setText(recipeName)
  }
}

Deleting Snacks

In the next blog post, I'll cover deleting rows from both the iOS app and watch while keeping both in sync.

Next Steps

I want to be able to update my managed objects from the WatchKit extension as well. I won't try to fully synchronize separate data models, but rather send fetched objects from the app to the watch and make read, update, and delete requests from the watch.

Let me know if you have feedback!