Author: Chars, works at Kingsoft Office, one of the core developers of WPS Office for iOS.

Review:

Guo Peng, editor of veteran Driver Technology Weekly, works in Dingxiang Garden, the development of Dingxiang Mother App

Damonwong, iOS developer, editor of veteran driver technology weekly, works in the technology department of tao department

preface

Comb based on Session 10003.

Since its launch, the Apple Watch has become increasingly independent. The Series 3, for example, is the first of its kind with cellular capabilities.

Watch Apps in watchOS 6 no longer require iOS component Apps and can be purchased from the App Store through the Watch.

With the introduction of Family Setup in watchOS 7, users no longer need a companion iPhone.

With further updates to the Apple Watch, it will give us more ways to communicate with our apps. This Session mainly introduces iCloud, Keychain, Watch Connectivity, Core Data and other technologies, as well as their advantages and disadvantages.

Transmission strategy

We can roughly divide transport policies into the following categories:

  1. Keychain with iCloud Synchronization
  2. CoreData with CloudKit
  3. Watch Connectivity
  4. URL Sessions
  5. Sockets

In order to better choose the technical solution suitable for our business scenario, we also need to pay attention to the following information:

  1. Type of Data
  2. Data source and destination
  3. Reliance on companion iOS app
  4. Support Family Setup
  5. Timing

Let’s take a look at each of these transport strategies in detail.

Keychain with iCloud

Keychain stores passwords, keys, and other sensitive data.

The iCloud Keychain Synchronization transmission policy has been introduced in watchOS 6.2. We can use it to synchronize Keychain items to all devices with the same account.

The capabilities provided by iCloud Synchronization can be roughly divided into the following two types:

  • Automatic password filling
  • Shared Keychain items

Automatic password filling

The password auto-fill function is derived from the text auto-fill function, but passwords are private data, so we need to consider data security.

Here’s what it looks like using autofill text:

And need to implement the above text automatic filling, you just need to set UITextField at. TextContentType to fullStreetAddress can.

Many data autopopulations are already supported and will be added in the future.

These are all auto-populated apps on iOS.

Now you can add automatic password filling to the Watch App with very little code. The specific steps are as follows:

1, Add the Associated Domains Capability to the WatchKit Extension Target. Add a Webcredentials entry with our domain name.

2. Add apple-app-site-association files to our Web server. The file must be accessible over HTTPS without redirection. The file is in JSON format and has no file name extension. You need to save the file in the./well-known directory on the server.

The apple-app-site-association file format is as follows:

For more information, see “Supporting Associated Domains”.

Add the text content type to the security field and text field. For example: The autofill options in the example include username and password.

struct LoginView: View {
   
    @State private var username = ""
    @State private var password = ""
    
    var body: some View {
        Form {
            TextField("User:", text: $username)
                .textContentType(.username)
            
            SecureField("Password", text: $password) 
                .textContentType(.password)
            
            Button {
                processLogin()
            } label: {
                Text("Login")}Button(role: .cancel) {
                cancelLogin()
            } label: {
                Label("Cancel", systemImage: "xmark.circle")}}}private func cancelLogin(a) {
        // Implement your cancel logic here
    }
    
    private func processLogin(a) {
        // Implement your login logic here}}Copy the code

Auto-fill recommendations have been available since watchOS 6.2. For now, watchOS 8’s new text editing experience will be even better.

For more information about using password auto-fill, see “Autofill Everywhere.”

Shared Keychain items

We typically store sensitive data, such as passwords, keys, and credentials, in the Keychain. But we can also use Keychain to store some shared data, such as user preferences for the launch screen.

However, we need to be careful not to store frequently changing information in the Keychain. In addition, data stored in the Keychain is synchronized to all devices of the account.

Next, we use the OAuth2 token as an example to describe the use of shared Keychain items.

1. Add “Keychain Sharing or App Groups” Capability to the Watch Extension target. That is, we want to share all the apps with these Keychain items.

These shared items can be configured to prevent access by other apps to ensure the security and privacy of user information. All of our apps that share Keychain items also need to share this group.

2. Let’s look at the code that stores the OAuth2 token using Keychain.

func storeToken(_ token: OAuth2Token.for server: String.account: String) throws {
    let query: [String: Any] = [
      kSecClass as String: kSecClassInternetPassword,
      kSecAttrServer as String: server,
      kSecAttrAccount as String: account,
      kSecAttrSynchronizable as String: true,]let tokenData = try encodeToken(token)
    let attributes: [String: Any] = [kSecValueData as String: tokenData]
    
    let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
    
    guard status ! = errSecItemNotFound else {
        try addTokenData(tokenData, for: server, account: account)
        return
    }
    
    guard status = = errSecSuccess else {
        throw OAuthKeychainError.updateError(status)
    }
}
Copy the code

The above code stores the token via SecItemUpdate or addTokenData.

For example, we have created an OAuth2 token data model with token strings, expired and refresh tokens, and other elements. We need to make the token structure NSCoding compliant to make it easy to store and retrieve. To do this, we create a Query dictionary.

Note: Attributes can be synchronized if kSecAttrSynchronizable is set to “true”. We must include this property in the query to specify whether the item is synchronized to all users’ devices.

The addTokenData method mentioned above has the following code:

func addTokenData(_ tokenData: Data.for server: String.account: String) throws {
    let attributes: [String: Any] = [
      kSecClass as String: kSecClassInternetPassword,
      kSecAttrServer as String: server,
      kSecAttrAccount as String: account,
      kSecAttrSynchronizable as String: true,
      kSecValueData as String: tokenData,
    ]
    
    let status = SecItemAdd(attributes as CFDictionary.nil)
    
    guard status = = errSecSuccess else {
        throw OAuthKeychainError.addError(status)
    }
}
Copy the code

To add tokens to the Keychain, we need to set up a dictionary with all the attributes. The SecItemAdd method is then called with attributes as an argument.

3. After storing the token data, how do we get it? Here is how to retrieve the token data:

func retrieveToken(for server: String.account: String) throws -> OAuth2Token? {
    let query: [String: Any] = [
      kSecClass as String: kSecClassInternetPassword,
      kSecAttrServer as String: server,
      kSecAttrAccount as String: account,
      kSecAttrSynchronizable as String: true,
      kSecReturnAttributes as String: false,
      kSecReturnData as String: true,]var item: CFTypeRef?
    let status = SecItemCopyMatching(query as CFDictionary.&item)
        
    guard status ! = errSecItemNotFound else {
        // No token stored for this server account combination.
        return nil
    }
    
    guard status = = errSecSuccess else {
        throw OAuthKeychainError.retrievalError(status)
    }
    
    guard let existingItem = item as? [String : Any] else {
        throw OAuthKeychainError.invalidKeychainItemFormat
    }
    
    guard let tokenData = existingItem[kSecValueData as String] as? Data else {
        throw OAuthKeychainError.missingTokenDataFromKeychainItem
    }
    
    do {
        return try JSONDecoder().decode(OAuth2Token.self, from: tokenData)
    } catch {
        throw OAuthKeychainError.tokenDecodingError(error.localizedDescription)
    }
}
Copy the code

First, we need to create a Query dictionary and set the items needed for the lookup. Then call the SecItemCopyMatching method. The retrieved results are populated with the “item” parameter.

We get the requested token data from item and decode the data to the OAuth2 token type.

At this point, we have completed the example operation of using Keychain to store and retrieve OAuth2 token data.

Delete data that is no longer needed when using a Keychain to store data. For example:

func removeToken(for server: String.account: String) throws {
    let query: [String: Any] = [
      kSecClass as String: kSecClassInternetPassword,
      kSecAttrServer as String: server,
      kSecAttrAccount as String: account,
      kSecAttrSynchronizable as String: true,]let status = SecItemDelete(query as CFDictionary)
    
    guard status = = errSecSuccess || status = = errSecItemNotFound else {
        throw OAuthKeychainError.deleteError(status)
    }
}
Copy the code

summary

ICloud Keychain synchronization is the best way to share infrequently changing data in an App.

ICloud Keychain synchronization does not depend on having an iOS companion application and also supports Family Setup.

Note: Users can disable iCloud Keychain synchronization and it is not available in all areas.

CoreData with CloudKit

Next, let’s look at another data transfer strategy, CoreData with CloudKit.

CoreData of CloudKit is used to synchronize the local database with all other devices that share the user application CloudKit container. Not only that, CoreData’s integration with SwiftUI makes it easier to access and display data from a database within Watch applications.

If we need to develop a multi-platform App and use this data communication method, we just need to design the data model.

We can also segment the meaningful parts of the Watch App that are suitable for running Data with more storage and battery capacity through various configurations in Core Data.

import CoreData
import SwiftUI

struct CoreDataView: View {
    
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Setting.itemKey, ascending: true)],
        animation: .easeIn)
    private var settings: FetchedResults<Setting>
    
    var body: some View {
        List {
            ForEach(settings) { setting in
                SettingRow(setting)
            }
        }
    }
}
Copy the code

The CoreData integration with SwiftUI mentioned above makes it easy to use CoreData functionality in your applications.

In the code above, we use @environment to provide the managed Context to the “View” and get the result from the database. These results can be used in the SwiftUI list and other views. CoreData using CloudKit gives us a way to share structured data that can be synced to all of a person’s devices and backed up to iCloud. Instead of relying on having a companion iPhone application, it supports Family Setup. Synchronization timing is not instantaneous, depending on network availability and system conditions.

To learn more about using Core Data with CloudKit in your applications, See “Build apps that share data through CloudKit and Core Data” and “Bring Core Data concurrency to Swift and SwiftUI”.

Watch Connectivity

Before we talk about what The Watch Connectivity is, let’s take a look at how data is stored in earlier apps.

It is not difficult to see from the above figure that the data in the App on iPhone and Watch are independent and cannot interact with each other. In order to solve this problem, Apple designs the Watch Connectivity framework. The following figure simply points out the connection and function between them.

Watch Connectivity was first released on watchOS 2 and iOS 9. I won’t go into too much detail here. Here are the features of this framework:

  • Send data between the Watch app and its companion iPhone app when within Bluetooth range or on the same WiFi network
  • Share data that is only available on one device (Watch/iPhone)

In the practical application process, we know that there are some points to pay attention to when using Connectivity:

1. We need to Activate Watch’s WCSession as early as possible in the application lifecycle;

2. Reachability. We need to check WCSession reachability before we send data.

All WCSession Delegate methods are called on non-primary serial queues. Therefore, if you need to manipulate interfaces in these methods, you need to switch to the main thread.

Wcsession.issupported (); wcsession.issupported ()

For the limitation of transmission content, Connectivity transmission content can be divided into the following types:

  1. Application Context
  2. File transfer
  3. Transfer user info
  4. Send Message

Let’s look at each of them.

Application Context

Application Context is a single property list dictionary that is sent to the corresponding Application in the background and is available when the App switches to the foreground. If the Application Context is updated before the last dictionary is sent, it will be replaced with the new value.

When there is new data, the Application Context is useful for keeping the current content on the corresponding App and data that may be updated frequently. Transfer User Info also sends a property list dictionary to the corresponding App in the background, but it is a little different from Application Context. Each update of the user dictionary is not a dictionary to be replaced, but queued and passed in the order in which each user dictionary is transferred. Developers can access this queue to cancel the transfer.

It is important to note that updateApplicationContext: method in data transmission, the receiver is invoked.

File transfer

File transfers are similar to Transfer User info in that files are queued to the appropriate application when power and other conditions permit. Developers can access this queue to cancel the transfer.

During file transfer, operations need to be performed in the background. Since files are queued, we can cancel unsent files using the outstandingFileTransfers method.

When a file is transferred, it is placed in the Inbox directory that receives the application documents. When the Session :didReceiveFile: callback is received from the Delegate, each file is deleted from the Inbox. Therefore, files can be moved or otherwise processed quickly before this method returns.

Note: Since this callback is invoked on a non-primary serial queue, if you call an asynchronous method to process files from the INBOX, you will most likely experience file disappearance problems. In addition, the time of file transfer is based on system conditions. The speed of file transfer is determined by the file size.

Transfer user info

This way of data transmission is a kind of real-time transmission mode, we can through remainingComplicationUserInfoTransfers check available resources. When we need to transmit data, but there are no available resources, this transmission mode takes the form of queue, waiting for the time to send.

This way is the main API transferCurrentComplicationUserInfo (_), we can understand as it is a special case of the user information transmission function, it can send the complex data to Watch.

Send Message

When we send data to the corresponding App using sendMessage, we can get a reply. It is mainly used for interactive messaging with the corresponding App. But whether we send dictionaries or data, we need to keep the amount of information small.

When using such a message sending method, you should specify the corresponding replyHandler operation. You also need to make sure that the didReceiveMessage: or didReceiveData: methods in the Delegate are implemented in replyHandler.

For more information about “Watch Connectivity”, see “Introducing Watch Connectivity”.

URL Sessions

URL sessions are a common way to communicate with the server. URL sessions can be divided into the following types according to their usage:

  • Background URL Sessions
  • Foreground URL Sessions

Background URL Sessions

In most cases, Background URL Sessions should be preferred. If we need to conduct data interaction in the foreground, we are likely to encounter the problem of insufficient time (for example, due to the large amount of data, slow network speed, etc.). Think about the user experience when the data interaction in the foreground fails.

Let’s take a simple example to illustrate:

For example, we have some personalized configuration data of apps, which need to be stored through the network server. When users update these configurations, we need to save them on the Watch and then send them to the server in the background.

class BackgroundURLSession: NSObject.ObservableObject.Identifiable {
    
    /// The current status of the session
    @Published var status = Status.notStarted
    
    /// The downloaded data (populated when status == .completed)
    @Published var downloadedURL: URL?
    
    private var backgroundTasks = [WKURLSessionRefreshBackgroundTask] ()private lazy var urlSession: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: sessionID)
            // Set isDiscretionary = true if you are sending or receiving large 
            // amounts of data. Let Watch users know that their transfers might 
            // not start until they are connected to Wi-Fi and power.
            config.isDiscretionary = false
            config.sessionSendsLaunchEvents = true
            return URLSession(configuration: config,
                              delegate: self, delegateQueue: nil()})}Copy the code

To do this, we need to create a Background URL Sessions class to handle server communication.

The unique identifier we configured for the URL Session so that we can use it to find a particular Session. Setting sessionSendsLaunchEvents to true indicates that the Session should start the application in the background when tasks on the Session need to be processed.

Note: If you are transferring a large amount of data, you should set isDiscretionary to true in the URL Session configuration so that the system chooses the best time to transfer the data.

In this case, we should also let users know that they may not download until their Watch is connected to WiFi and power.

// This is a member of the BackgroundURLSession class in the example. 
// Enqueue the URLRequest to send in the background. 
func enqueueTransfer(a) {
    var request = URLRequest(url: url)
    request.httpBody = body
    if body ! = nil {
        request.httpMethod = "POST"
    }
    if let contentType = contentType {
        request.setValue(contentType, forHTTPHeaderField: "Content-type")}let task = urlSession.downloadTask(with: request)
    task.earliestBeginDate = nextTaskStartDate
  
    BackgroundURLSessions.sharedInstance().sessions[sessionID] = self
  
    task.resume()
    status = .queued
}
Copy the code

When we’re ready to send the data, we need to queue it up for transmission.

Then, create a task for the request on the Session. In this simplified example, we only add one task to the Session, but we can actually add multiple requests to the Session to improve efficiency. Set a start date so that the download can begin later. Note that the system determines the actual task start time based on background resources, network, and system conditions. If we enabled the concurrent delegate, our application could receive up to four background refresh tasks per hour, so it’s best to keep tasks at least 15 minutes apart to prevent them from being delayed by the system.

class ExtensionDelegate: NSObject.WKExtensionDelegate {
    
    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
        for task in backgroundTasks {
            // Use a switch statement to check the task type
            switch task {
            case let backgroundTask as WKApplicationRefreshBackgroundTask:
                // Be sure to complete the background task once you’re done.
                backgroundTask.setTaskCompletedWithSnapshot(false)
            case let snapshotTask as WKSnapshotRefreshBackgroundTask:
                // Snapshot tasks have a unique completion call, make sure to set your expiration date
                snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
            case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
                // Be sure to complete the connectivity task once you’re done.
                connectivityTask.setTaskCompletedWithSnapshot(false)
            case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
                if let session = BackgroundURLSessions.sharedInstance()
                        .sessions[urlSessionTask.sessionIdentifier] {
                    session.addBackgroundRefreshTask(urlSessionTask)
                } else {
                    // There is no model for this session, just set it complete
                    urlSessionTask.setTaskCompletedWithSnapshot(false)}case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
                // Be sure to complete the relevant-shortcut task once you're done.
                relevantShortcutTask.setTaskCompletedWithSnapshot(false)
            case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
                // Be sure to complete the intent-did-run task once you're done.
                intentDidRunTask.setTaskCompletedWithSnapshot(false)
            default:
                // make sure to complete unhandled task types
                task.setTaskCompletedWithSnapshot(false)}}}}Copy the code

Finally, we set the state to queue in case there are Session observers. The system notifies our application when we use background task processing to send a request. To handle this task, we need to create a class that conforms to the WK extension delegate and implement the handle(_ backgroundTasks:) method.

For the background URL Session refresh task, we will try to find the Session in the list of ongoing requests. If we had it, we would call a method on the Session to add the background refresh task to the Session list so that we could let the system know that we were done with the data once we were done with it.

If we don’t find Session in the list, we need to mark the task as completed. Once this is done, the background refresh task must be completed immediately.

// Connect the Extension Delegate to the App

@main
struct MyWatchApp: App {
    
    @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extensionDelegate
    
    @SceneBuilder var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()}}}}Copy the code

Use the WK of the Extension Delegate adapter to extend the proxy property Package and add properties to our application. You can connect the extension delegate to our application.

class BackgroundURLSession: NSObject.ObservableObject.Identifiable {
    
    // Add the Background Refresh Task to the list so it can be set to completed when the URL task is done.
	func addBackgroundRefreshTask(_ task: WKURLSessionRefreshBackgroundTask) {
    	backgroundTasks.append(task)
	}

}
Copy the code

The system will call our Extension Delegate to handle our background tasks. In the Extension Delegate, we call this method to add a background task to an existing Session. Add this task to the background task list so that we can mark it completed as soon as we process the URL data.

extension BackgroundURLSession : URLSessionDownloadDelegate {
    
    private func saveDownloadedData(_ downloadedURL: URL) {
        // Move or quickly process this file before you return from this function.
        // The file is in a temporary location and will be deleted.
    }
    
    func urlSession(_ session: URLSession.downloadTask: URLSessionDownloadTask.didFinishDownloadingTo location: URL) {
        saveDownloadedData(location)
      
        // We don't need more updates on this session, so let it go.
        BackgroundURLSessions.sharedInstance().sessions[sessionID] = nil
      
        DispatchQueue.main.async {
            self.status = .completed
        }
        
        for task in backgroundTasks {
            task.setTaskCompletedWithSnapshot(false)}}}Copy the code

Finally, setting up our background task is complete. This lets the system know that we are done with background processing. It prevents the system from terminating our application for exceeding its background limits.

Foreground URL Sessions

In contrast to background, Foreground URL Sessions are mainly used in Foreground and are not commonly used in actual development. Mainly because of the following specific restrictions:

  1. 2.5 minutes of timeout
  2. Fast response from the server is required
  3. Timely data interaction is required in App interaction

To learn more about URL Sessions, see “Keep your complications up to Date “and “Background execution demystified”.

Sockets

Sockets are another option for communicating directly with the server. Socket is not a specific network protocol, it is only a network technology interface encapsulation specification.

So on iOS, we can do this through the NSURLSession API. Now, on our Watch platform, we also use NSURLSession.

In practical applications, the following two technical capabilities are generally used:

  • HLS
  • Web Sockets

Web Sockets, which I’m sure you’re all familiar with, are primarily used to build long links. The iPhone Push is just a long link.

However, you may be unfamiliar with HLS, which is actually an audio streaming format supported by The Watch.

This is mainly to solve the problem of media playback on the Watch.

The diagram below illustrates where we might technically be using HLS or custom media streaming formats.

HLS is mainly encapsulated in AVFoundation, and some specific apis will not be elaborated here.

Audio Stream can use the Stream API in NSURLSession to realize data transmission. For more APIS, please refer to the related documentation in SDK.

For more on Audio Streaming on the Watch, see “Streaming Audio on watchOS 6”.

summary

So far, we’ve covered several data interactions supported in Watch. The usage scenarios and capabilities of various technologies are also different. We have summarized the following table in the hope that we can choose the appropriate technical solution according to the actual scenario.

Pay attention to our

For more WWDC2021 articles, please follow and subscribe to WWDC21 Inside Reference