There and back again: Data transfer on Apple Watch
Advances in Apple Watch give you more ways to communicate to and from your app, and new audiences to consider. Learn what strategies are available for data communication and how to choose the right tool for the job. Compare and contrast the benefits of using technologies such as iCloud Keychain, Watch Connectivity, Core Data, and more.
- Hi! Welcome to WWDC. My name is Anne Hitchcock, and I'm an engineer on the Watch Frameworks team. I'm excited to be here today to talk about data transfer strategies on Apple Watch.
Since its introduction, Apple Watch has become increasingly independent. Series 3 was the first Apple Watch available with cellular capability.
Independent Watch Apps in watchOS 6 gave you the ability to write apps that didn't require an iOS companion and could be purchased from the App Store on your customers' Watches. With the introduction of Family Setup in watchOS 7, your customers have more independence than ever, without having a companion iPhone. But these new capabilities present us, as developers, new challenges in the way we think about communicating with our Watch apps. Fortunately, we have lots of great options. Today we're going to talk about those options and how we pick the right one for the job.
I'm going to give you an overview of tools we have for data communication in Watch apps and discuss how to evaluate which one will be the right choice for your task. We can broadly group the tools into a few categories.
iCloud allows us to share with all our devices and gives us server storage. We can use this in our apps using Keychain with iCloud Synchronization and CoreData with CloudKit.
If we need to transfer data between paired devices, we can use Watch Connectivity.
To communicate directly with servers, we can use URL sessions or sockets. But first, we'll talk about the questions you can ask to pick the right one.
When I think about how I'm going to communicate from my Watch app, I think about a few things.
What kind of data is it? And where is the data now, and where do I need it to be? Is the interaction reliant on a companion iOS app? Do I want to support Family Setup? And when does the data need to be at its destination? Can it wait to let the system optimize performance and battery usage for my customer? How frequently is the data going to change? Based on my answers to these questions, I can start going through my toolbox to evaluate how to craft the right solution for my data transfer task.
Let's look at what capabilities we get from the Keychain with iCloud Synchronization.
Keychain provides secure storage for passwords, keys, and other sensitive credentials. And with iCloud Keychain Synchronization, introduced in watchOS 6.2, these keychain items can be synchronized to all of a person's devices.
There are two ways you can benefit from iCloud Synchronization in your app: by using Password autofill with Associated Domains and Shared Keychain items.
Password autofill allows you to use Keychain synchronization with very little code. First, add the Associated Domains capability to your target. For your Watch app, add the capability to the WatchKit Extension Target. Add a webcredentials entry with your domain name.
Add the apple-app-site-association file to your web server. The file must be accessible via HTTPS without redirects. The file is JSON format with no file extension, and it should be placed in the ./well-known directory on your server. Check out the documentation online for "Supporting Associated Domains" for complete details. Add text content types to your text fields and secure fields. Your options for autofill are username, email address, password, and new password. For new passwords, the system will prompt the person to save, and a record will be added or updated in the Keychain for your site.
AutoFill suggestions have been available since watchOS 6.2 and are even better with the new text-editing experience in watchOS 8.
For more information about using password autofill, check out the "Autofill everywhere" session in the Developer app or online.
Another way you can share data using Keychain synchronization is by sharing Keychain items among your apps.
As we discussed, the Keychain is secure storage for sensitive data, like passwords, keys, and credentials. You can also store other small bits of shared data in the Keychain, such as a person's preference for a startup screen, as long as the information isn't changing frequently. The data stored in the Keychain will be synchronized to all of the person's devices.
Let's look at how we can store and retrieve an OAuth 2 token in the Keychain, and share that with a group of our apps.
First, we need to add the Keychain Sharing or App Groups capability, all the apps we want to share these keychain items. This is required to share the items, and helps ensure the security and privacy of your customers' information by preventing access by other apps. For your Watch app, add the capability to your Watch Extension target. In this example, I'm going to add the Keychain Sharing capability and add my app to a Keychain Group. All of my apps that are going to share the Keychain items need to also share this group.
Now, let's look at the code to store an OAuth 2 token in the Keychain. To store a token, we'll update the item if it exists, and add it if it doesn't. I've created an OAuth 2 token struct to contain the token data, like the token string, expiration, and refresh token. I made the token struct conform to codable to make it easy to store and retrieve. We create a query dictionary. This is the set of attributes that match an existing item if we already have one saved for this server and account.
Note the synchronizable attribute here that is set to "true." It's important that we include this attribute in our query to indicate that we want our item to synchronize to all of our customer's devices. We'll encode the token as data, and set that Data as the value for our Keychain item in the attributes dictionary. Then, update the item in the Keychain with the query and attributes.
We always need to check the result code returned from the Keychain API. We'll first check to see if the Keychain said that the item was not found. If so, we'll call another function we wrote to add it to the Keychain. We'll look at that in a moment. Otherwise, we'll make sure there was no error. To do that, we check for the success result. If the update function returned success, then our token was updated in the Keychain.
Now, let's look at the add function. To add the token to the Keychain, we'll set up a dictionary with all the attributes. This includes the attributes we used to find an existing item, plus the token data. Then, we'll call the Keychain API's add function with the attributes. And check the return code to make sure it succeeded.
To retrieve the token information from the Keychain, we'll set up a query dictionary to find the item we want. We'll include the same set of keys and values we included to find the item before in the update function. Plus, we include some attributes to tell the Keychain API whether we want the item attributes returned (we don't) and whether we want the item data returned (we do). The Keychain "copy matching" function searches using our query and populates the reference we provided as "item." Before we try to access the retrieved item, we'll check the return code to ensure it was found.
Then, as always, we check the return code for success. Get the dictionary that was copied for the item, get the token data we requested from the dictionary, and decode the data as our OAuth 2 token type. And now, we've successfully saved, updated, and retrieved an OAuth 2 token to the Keychain, and it's shared with all the apps in our Keychain Sharing Group.
There's one more Keychain storage function I want to share with you. Just like anywhere that you store something on your customers' devices, you should remove it when you're done with it. We'll set up our query with our now-familiar attributes to search. Call the Keychain API's delete function with our query. And, as always, check for success. In the case of delete, not found is successful. Now, we finish cleaning up after we're done with the data.
Keychain services with iCloud Keychain synchronization are a great way for your app to share small pieces of data that don't change frequently, and that data will be synchronized to all of a person's devices. Use Associated Domains to easily add password autofill functionality to your app.
You can also store and retrieve values directly to the Keychain and share them with your other apps using Keychain Sharing or App Groups. iCloud Keychain Synchronization doesn't rely on having an iOS companion app, and it supports Family Setup. The items are synchronized when possible based on network availability, battery, and other system conditions.
Be aware that customers can disable iCloud Keychain synchronization and that it isn't available in all regions.
CoreData with CloudKit synchronizes your local database to all of your customer's other devices that share your app's CloudKit container. CoreData integration with SwiftUI simplifies accessing and displaying data from your database in your Watch application.
You can quickly get too much data on a Watch this way if you're developing a multi-platform application. Think carefully about what information your customer really needs on their Watch.
Consider using multiple configurations in your Core Data model to segment data that makes sense to have in your Watch app from data that is appropriate for your app running on a device with more storage and battery capacity.
CloudKit and Core Data are powerful tools. The integration of Core Data with SwiftUI makes it easier to use Core Data features in your apps. You can provide the managed object context" to your Views with the environment value, and use the fetch request property wrapper to get results from your database. Those results can be used in SwiftUI Lists and other views. Core Data with CloudKit provides us with a way to share structured data that can be synchronized to all of a person's devices and backed up on iCloud. It doesn't rely on having a companion iPhone app and does support Family Setup. Synchronization of Core Data changes happens based on network availability and system conditions. Don't expect it to be instantaneous, but CloudKit will handle optimizing performance of this synchronization for your app.
To learn more about using Core Data with CloudKit in your app, check out "Build apps that share data through CloudKit and Core Data" and "Bring Core Data concurrency to Swift and SwiftUI" in the Developer app or online.
You might already be familiar with Watch Connectivity, and you might have used it before. But I'd like to give you more details and some best practices to help you be successful.
Watch Connectivity allows you to send data between your Watch app and its companion iPhone app when both devices are within Bluetooth range or on the same Wi-Fi network. It's best used for optimizing your customer's experience when they have both your phone and Watch apps installed, and for sharing data that is only available on one device.
For example, if someone has launched your iPhone app and has downloaded the latest data, you can share that data with your Watch app to keep your complications current and allow your Watch app to start with that same data the next time it launches. This will feel more responsive for your customers and minimizes the duplicate data downloads your apps need to do. Watch Connectivity has a diverse set of features, so it's helpful to know what's available and when to use each one. But first, I want to share a few tips to help you succeed if you decide that Watch Connectivity is the right tool for your task. Since Watch Connectivity is a tool to communicate between two devices, it's going to require us to know about a few preconditions and handle some errors. These are some things you can do to ensure your Watch Connectivity communication works smoothly. Activate your watch connectivity session as early in your app life cycle as possible, preferably when your app finishes launching in your app or extension delegate. This makes your app available to receive information from its counterpart app as soon as possible.
Understand reachability. None of the background communication requires your counterpart app to be reachable when you send data. But interactive messaging does have reachability requirements, and we'll discuss them then. It will save you time to understand them.
All of the watch connectivity session delegate functions are called on a non-main serial queue. If you need to do any work from these functions to update your user interface, make sure you do that on the main queue.
Now, let's talk about the different Watch Connectivity features and when to use each one. The application context is a single property list dictionary that is sent to the counterpart app in the background, with the goal of being available when the app wakes up. If you update the application context before the previous dictionary is sent, it is replaced by the new value.
Application context is useful for keeping content up to date on the counterpart app when you have new data, and for data that may update frequently. User info transfer also sends a property list dictionary to the counterpart app in the background, but it's a little different than application context. Instead of being a single dictionary that is replaced each time you update it, each user info dictionary transfer is queued and delivered in the order that you enqueued it. You can also access the queue to cancel a transfer.
File transfer is similar to user info transfer, and once you've done one, the other will feel familiar. Files are queued to be sent to the counterpart app, and sent when power and other conditions permit. You can access the queue to cancel a transfer.
The files are placed in the document inbox for the receiving app when they are transferred. Each file will be deleted from the inbox when you return from the did receive file callback in your session delegate. Make sure you move the file or otherwise quickly process it before you return from this method.
One helpful thing to remember about this: since this callback is called on a non-main serial queue, if you call an async method to process the file from the inbox, you will most likely run into a problem because the file will be gone. The timing of file transfers is based on system conditions, and, of course, larger files may take longer to transfer.
transferCurrentComplicationUserInfo(_:) is a special case of the user info transfer functionality to send complication-related data to the Watch. It's transferred as soon as possible, ahead of other user info transfers, as long as you have complication transfers remaining in your budget. This immediate transfer allows you to keep your active complications current for your customers when you have updated data from your phone. You can check your remaining budget, and if you transfer current complication info with no remaining budget, it will still be sent. It will just use the normal user info transfer queue.
You can use sendMessage to send data to your counterpart app and get a reply. This is for interactive messaging when your counterpart app is reachable. Whether you're sending a dictionary or data, keep your messages small. We also recommend that you opt to include a reply handler for your sendMessage calls. A short reply allows you to verify that the counterpart app did receive the message and that the data was correct. When you include that reply handler on your sendMessage, also make sure you implement the version of the did receive message or did receive data delegate callback function in the counterpart app that includes a reply handler. Otherwise, you'll get an error when you send the message.
Now that we've heard about sendMessage, let's revisit the concept of reachability. Both of your apps need to be reachable to send messages. You can check the isReachable property on the Watch Connectivity session to determine if your counterpart app is reachable for live, non-background messaging. But what does it mean to be reachable? Both devices need to be within range of each other via Bluetooth or on the same Wi-Fi network. For the WatchKit Extension to be reachable, it must be either running in the foreground or running in the background with a high priority, as it does when performing long-running background sessions. The iOS app doesn't have this foreground requirement. If you send a message from your Watch app to your iOS app, and your iOS app is not in the foreground, your iOS app will be activated in the background to receive the message.
This means that your iOS app is reachable from your Watch Extension far more of the time than vice versa.
Watch Connectivity is a good way to provide your customers with an experience that feels timely, responsive, and intuitive when they have installed both your iPhone and Watch apps. Since Watch Connectivity is specifically communicating between a phone and a paired Watch, don't use it to support apps for Family Setup.
Data transfer is dependent on the availability of the companion device via Bluetooth or Wi-Fi. Real-time communication using sendMessage requires the counterpart to be reachable. Remember that your counterpart app won't be reachable a lot of the time, especially when you're trying to communicate to your Watch app. Background transfers aren't delivered immediately. Think of them like posting a letter: you drop it in the box, but you're not sure exactly when it's going to be there.
For more information about Watch Connectivity, check out "Introducing Watch Connectivity" in the Developer app or online. Now, we're going to talk about a couple of ways to communicate directly with servers. For most use cases, the best option is URL sessions. Depending on the interaction and type of data, you might be able to defer communication or might need to do it right away. So, we have different configurations for URL sessions to allow them to be run in the background or foreground. Let's look at when you should use each of these options.
You should use background sessions wherever possible. This might not be our first instinct as developers, where we might want to just go ahead and get or send data right away.
But really think about it. Foreground sessions need to complete while your app is in the foreground or front-most, and for all but the shortest tasks, this isn't enough time. Think about your customers' experience if their communication task fails.
So, please be considerate of your customers and carefully evaluate each communication task and ask, "Can I do this in the background?" Background URL sessions are the right choice for any time communication can be delayed and for large data transfers. You can also send a push notification to your app to indicate that new data is available and initiate a background update. The exact timing of your background transfer will depend on system conditions. Let's walk through an example of sending some data to a server in the background. For example, if I have some settings for my application that I want to store through my web server, when my customer saves those, I can save them on the Watch and then send them to the server in the background.
To do that, I've made a background URL session class to handle the work of the server communication.
Our URL session will have a background configuration with a unique identifier we can use to find it later. Set the sends launch events property to true to indicate that the session should launch your app in the background when tasks on your session need to be handled.
Note that if you're transferring a large amount of data, you should set the isDiscretionary property of the URL session configuration to true to let the system schedule the transfer at an optimal time for the device for best performance.
In this case, you should also let your customers know that their downloads might not happen until they're connected to Wi-Fi and power.
When we're ready to send the data, we need to enqueue the transfer to schedule the background session. We'll create and configure a URL request with the contents for our Settings update to our server.
Then, we'll create a task for our request on the session. In this simplified example, I'm only adding one task to my session, but you can add multiple requests to your session for efficiency. Set the earliestBeginDate to start the download later. Note that the system will determine the actual time our task starts based on background budget, network, and system conditions. Your app can receive up to four background refresh tasks per hour, if you have a complication on the active watch face, so schedule your tasks at least 15 minutes apart to prevent them from being delayed by the system.
I'm holding on to this session in a list of in-progress sessions. This will become important later, when the system lets me know that my URL request has completed.
Calling "resume" on the task actually starts it, so it's important that you call this.
Finally, I set my status to queued in case there are observers for the session. The system will notify our app when our background request has been processed using a background task sent to our Extension Delegate. In order for us to handle that task, we need to create a class that conforms to WK extension delegate and implement the handle(_ backgroundTasks:) function. For Background URL Session Refresh tasks, we'll try to find our session in our list of in-progress requests. If we have it, we'll call a function on the session to add the background refresh task to the list for the session, so we can let the system know we've completed it as soon as we've finished processing the data. I'll show you this in a moment.
If we don't find the session in our list, we need to mark the task as completed. It is very important that you always set your background refresh task completed as soon as you're done. There's one more thing we need to do to get our background task calls: we need to connect our extension delegate to our App. To do this we're going to use the WK extension delegate adaptor property wrapper with our Extension Delegate class, and add a property to our app. Now, the system will call our extension delegate to handle our background tasks. In our extension delegate, we called this function to add our background task to our existing session. Add this task to our list of background tasks so we can mark it completed as soon as we're done processing the URL data. Now, we've wired up the whole round trip, and all we have left to do is get our data and let the system know we're done. Our URL session download delegate will get called when our request completes. Process the data received in the file from the download task. It's important that you either move this item into a directory accessible to your app or quickly process the data from the file. When this task completes, the downloaded file will be deleted. We're removing this session from the list of in-process sessions, since we won't get any more background tasks for it from the extension delegate, and we're setting the status to completed in case there are any observers.
Finally, we're setting our background task completed. This lets the system know we've completed our background processing. Making sure you do this isn't just being a good Watch app citizen. It prevents the system from terminating your app for exceeding its background limits. And that's it! We're all done sending our settings in the background and getting any updates. Note that in a full implementation, you'll want to handle errors and authentication challenges, but this gives you the basic steps. Use foreground URL sessions for quick server communication while a person is interacting with your app. A good example of this is getting the latest workout list or the meditation of the day. Foreground URL sessions are a less-power-efficient way to get and send data, and a two-and-a-half minute timeout is enforced. But in practice, you should try to target foreground sessions to interactions that are much quicker than that limit. URL sessions are the best method for general purpose communication directly with servers. They don't rely on a companion iPhone app, and you can use them with apps supporting Family Setup. Use background sessions for anywhere you can possibly delay the data transfer, and always for transferring larger amounts of data.
To learn more about URL sessions, check out "Keep your complications up to date" and "Background execution demystified" in the Developer app or online.
In addition to URL sessions, if you're building a streaming audio app, sockets are another option to communicate directly with servers. You can use HTTP Live Streaming or Web Sockets in Watch apps in the context of your active streaming audio session.
For more information on using Sockets, check out the "Streaming Audio on watchOS 6" session in the Developer app or online.
We've covered a lot, so let's summarize how we can choose from among all the options we've seen.
For small pieces of sensitive data that can be synced to all a person's devices, choose Keychain with iCloud Synchronization. To store databases in iCloud and share with all a person's devices, choose Core Data with CloudKit. To optimize the experience for companion iPhone and Watch apps, or to share data that is available only on one device in companion apps, choose Watch Connectivity. To communicate directly with servers, choose URL sessions. For streaming audio apps, you can also use sockets. To support customers who are using Family Setup or to use cellular data transfer, make sure you choose Keychain with iCloud Synchronization, Core Data with CloudKit, URL sessions, or sockets.
Think about the type of data, its source and destination, and your customer audience before choosing a solution to help you pick the right tool for the job. And always test your app on devices, not connected to the debugger, before you deploy it to verify its behavior in real-world conditions. Thank you for coming to learn about all the great tools we have for data transfer in your Watch apps. We can't wait to see what you build next. [music]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.