ハイライト
Apple Watch はますます独立しています——Series 3 のセルラー、watchOS 6 のスタンドアロン App、watchOS 7 のファミリー設定まで。このセッションでは iCloud Keychain、Core Data + CloudKit、Watch Connectivity、URL Session、Socket の 5 つのデータ転送方式を体系的に整理し、それぞれの適用シーンと選択の決定木を示します。
主要内容
転送方式を選ぶ前の 5 つの質問
(02:08)
データ転送の方法を決める前に、次の 5 つを自問してください。
- データの種類は?機密情報、構造化データ、大きなファイル?
- データは今どこにあり、どこへ送る必要がある?
- ペアリングされた iOS App に依存するか?
- ファミリー設定をサポートする必要があるか?
- データはどれだけ早く届く必要がある?
答えに応じて、ツールボックスから最適な手段を選びます。
iCloud Keychain: 少量の機密データを全デバイスで同期
(02:53)
Keychain はパスワード、鍵など機密認証情報の安全な保存を提供します。watchOS 6.2 から iCloud Keychain 同期がサポートされ、Keychain 項目はユーザーの全デバイスに同期できます。
最も簡単なのはパスワード自動入力(Password Autofill)です。TextField に textContentType を設定するだけです。
TextField("User:", text: $username)
.textContentType(.username)
SecureField("Password", text: $password)
.textContentType(.password)
(04:20)
Associated Domains 能力と、サーバー上の apple-app-site-association ファイルが必要です。watchOS 8 のテキスト編集体験の改善で、自動入力はさらに使いやすくなりました。
OAuth トークンなどカスタムデータを保存する場合は Keychain API を直接操作します。kSecAttrSynchronizable を true に設定すれば全デバイスに同期されます。
Core Data + CloudKit: 構造化データのクラウド同期
(11:10)
Core Data と CloudKit を統合すると、ローカルデータベースが同じ CloudKit コンテナを共有する全デバイスに同期されます。SwiftUI の @FetchRequest と @Environment(\.managedObjectContext) で Watch App でも Core Data を簡単に使えます。
(11:59)
Watch のストレージとバッテリーは限られています。クロスプラットフォーム App では、本当に時計上に必要なデータを慎重に選んでください。複数の Core Data 設定で、時計向けとスマホ向けのデータを分けられます。
Core Data + CloudKit は iOS コンパニオン App に依存せず、ファミリー設定をサポートします。同期タイミングはネットワークや電池などに基づきシステムが決定するため、即時同期は期待しないでください。
Watch Connectivity: ペアデバイス間のリアルタイム通信
(13:19)
Watch Connectivity は Bluetooth 範囲内または同一 Wi-Fi で Watch と iPhone 間のデータ転送を行います。両端に App があるときの UX 最適化や、片方にしかないデータの共有に最適です。
(14:38)
Watch Connectivity のベストプラクティス:
- セッションは早く有効化する。できれば App 起動時に
- すべての delegate コールバックはメインキュー外で実行される。UI 更新はメインキューへ
- reachability を理解する:
sendMessageは両端が reachable である必要がある。バックグラウンド転送は不要
Watch Connectivity の転送方式:
- Application Context: 単一辞書。更新で古い値を置換。頻繁に更新する設定データ向け
- User Info Transfer: キュー式、順序通り配信、キャンセル可能
- File Transfer: ファイルキュー。受信後は document inbox に置かれ、処理後削除
- transferCurrentComplicationUserInfo: complication 専用データ、優先転送
- sendMessage: リアルタイム双方向。両端が reachable である必要がある
(20:31)
Watch Connectivity はファミリー設定では使えません。ペアリングされた iPhone に依存するためです。
URL Session: サーバーとの直接通信
(21:41)
多くのシーンでは、サーバーとの直接通信に URL Session が最適です。フォアグラウンドとバックグラウンドの 2 モードがあります。
バックグラウンド URL Session を優先してください。App をフォアグラウンドに保つ必要がなく、システムが適切なタイミングで転送を実行します。大きなファイルでは isDiscretionary = true を設定し、最適なタイミングをシステムに任せます。
(23:04)
バックグラウンド転送完了時、システムは WKURLSessionRefreshBackgroundTask で App を起動します。Extension Delegate で処理し、速やかに完了をマークしてください。
フォアグラウンド URL Session は当日のコンテンツ一覧取得など素早い通信向けです。ただし 2.5 分のタイムアウトがあり、消費電力も大きいです。
詳細
Keychain による OAuth トークン保存の完全フロー
(06:25)
トークンを保存(存在すれば更新、なければ追加):
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)
}
}
キーポイント:
kSecClassInternetPasswordでインターネットパスワード型を指定kSecAttrSynchronizableをtrueにして iCloud 同期を有効化SecItemUpdateで既存項目を更新。存在しない場合はerrSecItemNotFound- 存在しない場合は
addTokenDataで新規追加
新規項目の追加:
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)
}
}
(07:59)
キーポイント:
SecItemAddで新しい Keychain 項目を追加- すべての属性は追加時に一度に設定する必要がある
errSecSuccessが成功を示す
トークンの読み取り:
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 {
return nil
}
guard status == errSecSuccess else {
throw OAuthKeychainError.retrievalError(status)
}
guard let existingItem = item as? [String : Any],
let tokenData = existingItem[kSecValueData as String] as? Data else {
throw OAuthKeychainError.invalidKeychainItemFormat
}
return try JSONDecoder().decode(OAuth2Token.self, from: tokenData)
}
(08:25)
キーポイント:
kSecReturnDataをtrueにするとデータ内容を返すSecItemCopyMatchingで一致項目を検索してコピーerrSecItemNotFoundはトークンが保存されていないことを意味するJSONDecoderで Data を OAuth2Token にデコード
トークンの削除:
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)
}
}
(09:39)
キーポイント:
SecItemDeleteで一致する Keychain 項目を削除errSecItemNotFoundも成功とみなす(既に存在しないため)
バックグラウンド URL Session の完全実装
(23:04)
バックグラウンド URL Session の作成:
class BackgroundURLSession: NSObject, ObservableObject, Identifiable {
private let sessionIDPrefix = "com.example.backgroundURLSessionID."
enum Status {
case notStarted
case queued
case inProgress(Double)
case completed
case failed(Error)
}
private var url: URL
var body: Data?
var contentType: String?
private(set) var id = UUID()
@Published var status = Status.notStarted
@Published var downloadedURL: URL?
private var backgroundTasks = [WKURLSessionRefreshBackgroundTask]()
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: sessionID)
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config,
delegate: self, delegateQueue: nil)
}()
private var sessionID: String {
"\(sessionIDPrefix)\(id.uuidString)"
}
init(url: URL) {
self.url = url
super.init()
}
}
キーポイント:
URLSessionConfiguration.backgroundでバックグラウンド設定を作成sessionSendsLaunchEvents = trueでタスク完了時に App を起動isDiscretionary = falseは転送を延期しない(小さなデータ向け)- 各 session には一意の identifier が必要
バックグラウンド転送のキュー投入:
func enqueueTransfer() {
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
}
(24:22)
キーポイント:
downloadTask(with:)でダウンロードタスクを作成earliestBeginDateで最早開始時刻を設定可能task.resume()を呼ばないとタスクは開始されない- 後続のバックグラウンドタスク処理のため session をグローバルリストに保存
バックグラウンドタスクの処理(Extension Delegate):
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
WatchConnectivityModel.shared.activateSession()
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
switch task {
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
if let session = BackgroundURLSessions.sharedInstance()
.sessions[urlSessionTask.sessionIdentifier] {
session.addBackgroundRefreshTask(urlSessionTask)
} else {
urlSessionTask.setTaskCompletedWithSnapshot(false)
}
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
}
(25:45)
キーポイント:
- Watch Connectivity セッションは早く有効化する
- タスクの種類で分岐して処理
- 対応する session を見つけたらバックグラウンドタスクを保存し、ダウンロード完了後に完了マーク
- session が見つからない場合は即座に完了マーク
Extension Delegate を App に接続:
@main
struct MyWatchApp: App {
@WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extensionDelegate
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
}
}
}
(26:43)
ダウンロード完了の処理:
extension BackgroundURLSession : URLSessionDownloadDelegate {
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
// ファイルは一時場所にあるため、速やかに移動または処理する
saveDownloadedData(location)
BackgroundURLSessions.sharedInstance().sessions[sessionID] = nil
DispatchQueue.main.async {
self.status = .completed
}
for task in backgroundTasks {
task.setTaskCompletedWithSnapshot(false)
}
}
}
(27:31)
キーポイント:
- ダウンロード完了ファイルは一時場所にあり、すぐに処理または移動が必要
- 処理後ですべてのバックグラウンドタスクを完了としてマーク
- グローバルリストから session を削除
- 完了マークが遅れると App がシステムにより終了される可能性がある
転送方式選択のまとめ
(30:21)
| シーン | 推奨方式 | ファミリー設定 |
|---|---|---|
| 少量の機密データ、全デバイス同期 | iCloud Keychain | 可 |
| 構造化データベース、全デバイス同期 | Core Data + CloudKit | 可 |
| ペア iPhone + Watch の UX 最適化 | Watch Connectivity | 不可 |
| サーバーとの直接通信 | URL Session / Socket | 可 |
| リアルタイム双方向(両端フォアグラウンド) | Watch Connectivity sendMessage | 不可 |
重要ポイント
-
クロスデバイスパスワードマネージャーの Watch 版を作る: iCloud Keychain 同期で時計上からよく使うパスワードを素早く確認。
SecItemCopyMatchingで Keychain のパスワードを読み取り、textContentTypeで自動入力。入口 API はSecurityフレームワークの Keychain 関数。 -
ファミリー共有の ToDo Watch App を作る: Core Data + CloudKit で家族の Apple Watch 間で ToDo を同期。
@FetchRequestで読み取り、NSPersistentCloudKitContainerが自動同期。入口 API はNSPersistentCloudKitContainer+@FetchRequest。 -
運動データ同期 App を作る: iPhone 側で詳細なルートデータを取得し、Watch Connectivity の
transferFileで Watch に送信。Watch 側は complication に簡略版を表示。入口 API はWCSession.transferFile(_:metadata:)。 -
バックグラウンドデータ報告ツールを作る: バックグラウンド URL Session で、時計が充電中かつ Wi-Fi 接続時にセンサーデータを一括アップロード。
isDiscretionary = trueで最適な転送タイミングをシステムに任せる。入口 API はURLSessionConfiguration.background+WKURLSessionRefreshBackgroundTask。 -
インスタントメッセージ Watch App を作る: Watch Connectivity の
sendMessageで iPhone と Watch 間のリアルタイムメッセージ同期。スマホでメッセージ受信時に即座に時計へプッシュ。入口 API はWCSession.sendMessage(_:replyHandler:errorHandler:)。
関連セッション
- What’s new in watchOS 8 — バックグラウンドデータ配信と Bluetooth を含む watchOS 8 の新機能
- Connect Bluetooth devices to Apple Watch — Bluetooth デバイスと Apple Watch の接続とデータ転送
- Build apps that share data through CloudKit and Core Data — CloudKit と Core Data でデバイス間のデータを同期
コメント
GitHub Issues · utterances