ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
再往復: Apple Watch上のデータ転送
Apple Watchの進化により、Appとの通信方法が増え、検討すべき対象者も増えました。データ通信にはどのような戦略があるのか、ジョブに適したツールの選び方を紹介します。iCloud Keychain、Watch Connectivity、Core Dataなどのテクノロジーを使用した場合のメリットを比較検討します。
リソース
- Downloading files from websites
- Keeping your complications up to date
- Keeping your watchOS content up to date
- Sharing access to keychain items among a collection of apps
- Supporting Associated Domains
- WCSession
関連ビデオ
WWDC21
WWDC20
WWDC19
-
ダウンロード
- 世界開発者会議 (WWDC)へようこそ 私はアン・ヒッチコックです Watchフレームワーク チームのエンジニアです 今日ここに来られて 嬉しいです Apple Watchのデータ転送 戦略についてお話します
Apple Watchの登場以来 その独立性は 高まってきました Series 3がセルラー機能を 搭載した 初のApple Watchでした
watchOS 6の Independent Watch App― では iOSコンパニオンを 必要とせず Appを作成することが できます そのためお客様の Watchで App Storeから Appを購入できます watchOS 7に導入された ファミリー共有設定により お客様はコンパニオンとなる iPhoneを持つことなく これまで以上に独立した 体験を得ることができます しかしこれらの新しい機能は 開発者である私達にとって Watch Appとの通信を 考える上で 新たな課題となります 幸い私達にはたくさんの 優れたオプションがあります 今日はそのような オプションと ジョブに適したものを選択する 方法についてお話します ツールの概要を 説明していきます Watch Appの データ通信において タスクごとに何が 適切な選択であるか 判断する方法を 議論していきます 大まかにはいくつかの カテゴリに分類できます iCloudはすべての デバイスとの共有を可能とし サーバーストレージを 提供してくれます iCloudキーチェーンの 同期や CoreDataとCloudKitを 組み合わせて Appで使用することが できます
ペアリングされたデバイス間 でのデータ転送の場合は Watch Connectivityを 使用できます
サーバーと直接通信するには URL Sessionや ソケットを使用します その前に 適切な 選択をするための 質問についてお答えします Watch Appからどのように 通信したらいいのかについて いくつか考えられます どのような データなのか? そのデータは今どこにあり どこにあるべきなのか? インタラクションはコンパニオンの iOS Appに依存してるか? ファミリー共有設定の サポートが必要か? データはいつまでに 転送される必要があるのか? お客様に向けた性能と バッテリー使用量の システムの最適化を 待つことができるのか? データはどのくらいの頻度で 変更されるのか? 私の答えはこれらの 質問に基づいています 自分のデータ転送タスクに 適した ソリューションを 判断するため ツールボックスを 確認してみます
iCloudキーチェーンの 同期からどのような 機能が得られるのか 見てみましょう
キーチェーンはパスワード 鍵その他の重要な認証情報を 安全に保管することが できます さらにwatchOS 6.2から 導入された iCloudキーチェーンの 同期では これらのアイテムを その人が持つ全ての― デバイスに同期させる ことができます
Appでこの同期機能を 利用するには 2つの方法があります 連携ドメインと 共有キーチェーンアイテムで パスワード自動入力を 使用することです パスワード自動入力により わずかなコードで キーチェーンの同期が 可能になります まず連携ドメイン機能を ターゲットに追加します Watch Appの場合 機能をWatchKit Extensionターゲットに 追加する必要があります ドメイン名を指定して "web credentials" を追加し
webサーバーに apple-app-site-association ファイルを 追加 します ファイルをリダイレクトなしで HTTPSでアクセスできるようにします ファイルはJSON形式で ファイル拡張子はなく サーバー上のwell-known ディレクトリに配置する 必要があります 詳細は"Supporing Associated Domains"の オンラインドキュメントを ご覧ください TextFieldと SecureFieldに textContentTypeを 追加します 自動入力の対象は ユーザー名 メールアドレス パスワードと 新しいパスワードです 新しいパスワードの場合は 保存を促すメッセージが 表示され サイトに紐づいたキーチェーンに 記録が追加 または更新されます
自動入力のサジェスト機能は watchOS 6.2から 搭載されていて watchOS の新しいテキスト 編集エクスペリエンスで さらに良くなりました パスワードの自動入力に ついての詳細は Developer Appまたは オンラインにて ”どこでもAutoFill” の セッションをご覧ください キーチェーンの同期を使って データを共有する もう一つの方法は キーチェーンのアイテムを App間で共有することです
説明したように キーチェーンは パスワードや鍵や 認証情報などの 重要なデータを安全に 保管するためのものです キーチェーンでは他の小さな 共有データを 保存することもできます 例えば起動画面に関する 設定など 頻繁に変更されない 情報です キーチェーンに保存された データは その人のすべてのデバイスに 同期されます ではOAuthトークンを キーチェーンに 保存 取得する方法を 見てみましょう そしてそれをAppの グループで共有します まずキーチェーンを 共有したい すべてのAppに キーチェーン共有機能または Appグループ機能を 追加します これはアイテムを 共有するために必要であり セキュリティとプライバシー の確保に役立ちます 他のAppからのアクセスを 防ぎ お客様の情報を 守ります Watch Appの場合は Watch Extension― ターゲットに 機能を追加します この例では キーチェーン共有機能 を追加し 自分のAppをキーチェーン グループに追加してみます キーチェーンアイテムを 共有するすべてのAppは このグループを共有する 必要があります ではOAuthトークンを キーチェーンに 保存するコードを 見てみましょう トークンを格納するために アイテムがあれば更新し なければ追加します OAuth2トークン構造体を 作成し トークン文字列 有効期限 リフレッシュトークンなどの トークンデータを 格納しています トークン構造体を Codableにconformし 保存や取得が しやすくします クエリーディクショナリを 作成します サーバーとアカウント用に 保存された 既存のアイテムに マッチする属性のセットです ここではSynchronizableの 属性が "true"に設定されている ことに注意してください この属性をクエリーに 含めることで お客様のすべてのデバイスに アイテムを同期させたい 事を示すのが重要です トークンをデータとして エンコードし そのデータを 属性ディクショナリの キーチェーンアイテムの 値として設定します その後キーチェーンの アイテムを クエリーと属性を使って 更新します
キーチェーンAPIから 戻ってくる 結果のコードを 常にチェックする 必要があります まずキーチェーンに アイテムが― 見つかりません という状態かを確認します もし見つからない場合は キーチェーンに追加する― ために書いた別の関数を 呼び出します 少し確認してみましょう そうでない場合はエラーが なかったことを確認します そのために成功したか 結果を チェックします 更新機能が成功した場合 キーチェーンのトークンが 更新されたことになります
ではAdd関数について 見てみましょう トークンをキーチェーンに 追加するために すべての属性の ディクショナリを設定します これには既存のアイテムを 見つけるために使用した 属性とトークンデータが 含まれます そしてその属性で キーチェーンAPIの Add関数を 呼び出します リターンコードを チェックして 成功したことを 確認します
キーチェーンからトークンの 情報を取得するために クエリーディクショナリを 設定しアイテムを探します 以前アイテムを見つけるため 入れたのと同じキーと値の― セットを更新関数同様に 組み込みます さらにキーチェーンAPIを 識別するために いくつかの属性を 組み込んでいます アイテムのリターン属性が 必要か (必要ありません) アイテムのリターンデータが 必要かどうかを識別します (必要です) キーチェーンの "コピーマッチング" 機能は クエリーを使って検索し 私たちが提供した itemリフェレンスを 生成します 取得したアイテムにアクセス しようとする前に リターンコードをチェックし 見つかったことを確認します
そして今回も リターンコードを確認して 成功したか確認します アイテム用にコピーされた ディクショナリを取得し ディクショナリから要求した トークンデータを取得します OAuth2トークンタイプで データをデコードします これでOAuth2トークンを キーチェーンに保存 更新し取得することが できました そしてキーチェーン 共有グループの すべてのAppと 共有されました
もうひとつ キーチェーンストレージの機能を ご紹介します お客様のデバイスに 何かを保存する場合と 同じように 使い終わったら 削除してください 慣れ親しんだ 属性を持つ クエリーを設定して 検索します キーチェーンAPIのdelete 関数をクエリーで呼び出し いつものように 成功したか確認します deleteの場合not foundは 成功したことになります データを使い 終わったら 後始末をします iCloudキーチェーンの 同期サービスは 頻繁に変更されない 小さなデータを Appで共有するのに 最適な方法で そのデータは その人のすべてのデバイスに 同期されます 連携ドメイン機能を 使用することで簡単に― パスワードの自動入力機能を Appに追加できます またキーチェーンに直接 値を保存 取得し 他のApp と 共有することもできます キーチェーン共有機能や Appグループ機能を使用します iCloudキーチェーンの同期は iOSのコンパニオンAppが なくても可能で ファミリー共有設定にも 対応しています アイテムはネットワークの 可用性 バッテリー その他のシステムの 条件に基づいて 可能な限り同期されます お客様はiCloud キーチェーンの同期を 無効にすることができるので ご注意ください またすべての地域でご利用 できるわけではありません CoreDataとCloudKitの 連携は ローカルの データベースを AppのCloudKitコンテナを 共有するお客様の 他のすべてのデバイスに 同期させます SwiftUIでの CoreDataの統合は Watch Appにおける データベースのデータへの アクセスと表示を 簡素化します マルチプラットフォームの Appを開発している場合 この方法では Watch上に かなり多くのデータを 取得することになります お客様が 本当に必要としている 情報はなにか よく考えてみてください
CoreDataモデルで 複数の構成を使用し Watch Appで 使用するのに適したデータと ストレージやバッテリーの 容量が大きいデバイスで 実行するAppに適したデータを 分けることを 検討しましょう
CloudKitとCoreDataは パワフルなツールです CoreDataとSwiftUIの 統合により Appで CoreDataの機能を 簡単に 使用できるようになりました Environment valueを使って ビューに managedObjectContextを 提供することができ FetchRequest プロパティーラッパーを使い データベースから結果を 得ることができます これらの結果はSwift UIの リストや その他のビューで 使用できます CoreDataとCloudKitの 連携は 構造化されたデータを 共有する方法を提供し このデータは顧客の すべてのデバイスに同期され iCoudにバックアップ されます コンパニオンiPhone Appとの 連携に頼らず ファミリー共有設定にも 対応しています CoreDataの変更の同期は ネットワークの可用性や システムの状態に応じて 行われます 即効性を期待すべきではありませんが Appへの同期の パフォーマンスの最適化は CloudKitが行います
CloudKitでCoreDataを 使用したAppの 詳細については Developer Appかオンラインで "CloudKitとCore Dataで データを共有するAppの構築"と "SwiftとSwiftUIへの Core Dataの並行処理の導入" をご覧ください Watch Connectivity については すでにお使いの方も 多いのではないでしょうか しかしもっと詳細な 成功するための ベストプラクティスを お伝えしたいと思います
Watch Connectivityは Watch Appと iPhone Appの間で 両方のデバイスが Bluetoothの範囲内または 同じWi-Fiネットワーク上に ある場合 データを送信 することができます iPhone Appと Watch Appの両方を インストールしている お客様の体験を最適化したり 一方にしか存在しない データの共有に最適です
例えば誰かがあなたの iPhone Appを起動して 最新のデータを ダウンロードした場合 そのデータをWatch Appと 共有することで コンプリケーションを 最新の状態に保ち 次回起動時には 同じデータで Watchをスタートさせる ことができます これによりお客様が 反応が良いと感じるようになり Appが必要とする 重複したデータの ダウンロードを最小限に 抑えることができます Watch Connectivityには 様々な機能があるので 何が利用できるのか どのような場合に 使えるのかを知ると 便利です しかしまず Watch Connectivityが あなたのタスクに適した ツールであると判断した場合 成功するためのヒントを いくつか紹介していきます Watch Connectivityは 2つのデバイス間で 通信するための ツールなので いくつかの前提条件を知り いくつかのエラーを 処理することが 必要となります これがWatch Connectivityの 通信をスムーズに行うための ポイントとして ご紹介したい点です WatchConnectivity セッションは Appの ライフサイクルのできるだけ 早い段階で できれば Appやエクステンションの デリゲートで Appの起動完了時に アクティベートします これにより自分のAppが もう一方のAppからの 情報をいち早く受け取る ことができるようになります 到達性を把握します バックグラウンド通信では データを送信する際に 相手のAppが 到達可能である 必要はありません しかしインタラクティブな メッセージ通信には 到達性の 要件がありますので それについては後で説明します それらの理解のための時間を 節約することができます
すべてのWatchConnectivity セッションデリゲート関数は メインではないシリアル キューで呼び出されます もしこれらの機能を使って ユーザーインターフェースを 更新するために これらの関数から何か作業を 行う必要がある場合は必ず メインキューで行います さてここからは Watch Connectivityの 各機能と それぞれの機能を使用する タイミングについて 説明します Application contextは Appが起動したときに 利用できるようにすることを 目的として バックグラウンドで 相手のAppに送信される 単一のプロパティリスト ディクショナリです 前のディクショナリが 送信される前に Application contextを 更新した場合 新しい値で 置き換えられます
Application contextは 新しいデータがあるときに 相手のAppで コンテンツを 最新の状態にしたり 頻繁に更新される データがあるときに 便利です User Info transferも バックグラウンドで相手の Appにプロパティリストの ディクショナリを送信します しかしapplication context とは少し異なります ディクショナリを 更新するたび置き換えられる 単一のディクショナリ ではなく 各User Infoの ディクショナリの転送は キューイングされ キューイングされた順に 配信されます またキューにアクセスして 転送をキャンセルすることも できます File transferは User Info transferと似ていて どちらかやったことがあれば 別の方も使いやすいでしょう ファイルは相手のAppに 送られるためキューに入り 電源などの条件が整ったとき 送信されます キューにアクセスして 転送をキャンセルできます そのファイルは 転送されたときに受信側の Appのドキュメント 受信箱に置かれます 各ファイルはセッション デリゲートのdidReceiveFile コールバックから戻ったとき 受信箱から削除されます このメソッドから戻る前に ファイルを移動するなどの 迅速な処理を 行ってください 一つの参考になることを 覚えておくといいでしょう このコールバックは メインではない シリアルキューで 呼び出されているため 非同期メソッドを呼び出して 受信箱からファイルを 処理すると ファイルがなくなってしまうため ほとんどの場合 問題が発生します ファイル転送のタイミングは システムの状況に応じて 行われます もちろん サイズの大きいファイルは 転送に時間がかかります
transferCurrentComplicationUserInfo(_:)は User Info trasfer機能の 特殊なケースで コンプリケーション関連の データをWatchに送信します 他のUser Infoの 転送に先駆けて コンプリケーション用のbudgetが 残っている限り 可能な限り早く転送されます この即時転送により あなたのiPhoneに 更新されたデータがあれば 顧客に対し アクティブな コンプリケーションを最新の 状態に保つことができます 残りのbudgetを 確認することができます また残りのbudgetがない 状態でも現在の コンプリケーション情報を 転送できます これは通常のUser Info transferの キューを使用します。
sendMessageを 使うと 相手のAppにデータを送り その返事をもらうことが できます これは相手のAppが 到達可能な場合に インタラクティブなメッセージング通信を 行うことができます 送っているのが ディクショナリやデータでも メッセージは 小さくしましょう またsendMessage の呼び出しには replyハンドラを含めることを お勧めします 短い返信で 相手のAppが メッセージを受信し データが正しいかどうかを 確認することができます replyハンドラを sendMessageに 組み込む際には replyハンドラを組み込んだ 相手のAppで didReceiveMessageや didReceiveDataの デリゲートコールバック― 関数といったバージョンを実装する ことも忘れないでください そうしないとメッセージを 送る際にエラーになります
sendMessageの話が 出たところで 到達性の概念を 再確認してみましょう メッセージを送信するには 両方のAppが受信可能で ある必要があります Watch Connectivity セッションの isReachableプロパティを チェックして相手のAppが バックグラウンドではない ライブメッセージングに 対応しているかどうかを 確認できます でも到達可能であるとは どういうことでしょう? 両方のデバイスが Bluetoothか同じ Wi-Fiネットワークに接続 されている必要があります WatchKit Extension が到達可能であるためには 長時間のバックグラウンド セッションを実行している ときのように フォアグラウンドで 実行されているか 高い優先度の状態で バックグラウンドで実行 されている必要があります iOS Appはフォアグラウンドの 必要性はありません Watch Appから iOS Appにメッセージを 送信したとき iOS Appが フォアグラウンドにない場合 iOS Appが バックグラウンドで起動して メッセージを受信します
つまりWatch Extensionから iOS Appに アクセスできる時間は その逆の場合よりも はるかに多いということです Watch Connectivityは iPhone Appと Watch Appの両方を インストールしたお客様に タイムリーで反応が良く 直感的に感じられる 体験を提供する 良い方法です Watch Connectivityは iPhoneとペアリングされた Watchとの間の通信に 特化しているため ファミリー共有設定配下のAppの サポートには使用できません
データ転送は Bluetoothまたは Wi-Fi経由で コンパニオンデバイスの 利用状況に依存します リアルタイムの コミュニケーション sendMessageの使用には 相手が到達可能であることが必要です 特にWatch Appに 連絡を取ろうと している時に 相手のAppが 到達可能でないことが 多いことを 覚えておいてください バックグラウンド転送は すぐには配信されません 手紙を投函するような ものだと思ってください 手紙を投函しても いつ頃届くのかわかりません
Watch Connectivity については Developer App またはオンラインで "Introducing Watch Connectivity"をご覧下さい さてこれから サーバと直接通信する方法を いくつかご紹介します ほとんどの使用ケースでは 最良の選択肢は URL Sessionです インタラクションや データの種類によっては コミュニケーションを 延期できる場合もあれば すぐに実行する必要が ある場合もあります そのためにURL Sessionは バックグラウンドでも フォアグラウンドでも使えるよう それぞれ設定が可能です いつそれぞれのオプションを 使うべきか考えてみましょう バックグラウンドセッションを 可能な限り使用して下さい これは開発者としての最初の 衝動ではないかもしれません つまりすぐにデータを 送受信したいと 思うかもしれません でもよく考えてみて下さい フォアグラウンド セッションは Appがフォアグラウンドか Frontmostである間に 完了する必要があり 最も短時間のタスク以外に とっては 十分な時間ではありません コミュニケーションタスクが 失敗した場合の お客様のことを 考えてみてください
だからこそお客様に配慮し コミュニケーションタスクを 慎重に判断して バックグラウンドでできるか 問う必要があります
バックグラウンド URL Sessionは 通信の遅延を 許容できる場合や 大きいデータ転送を 行う場合に適しています Appにプッシュ通知を送り 新しいデータが利用可能で あることを示し バックグラウンドでの更新を 開始することもできます バックグラウンド転送の 正確なタイミングは システムの状況によって 異なります バックグラウンドで サーバーにデータを 送信する例を 見てみましょう 例えば 私のAppの設定を ウェブサーバを通じて 共有したい場合 お客様がそれらを 保存する際に Watchに保存した後 バックグラウンドで サーバーに 送信することができます
そしてサーバー通信を 処理するために バックグラウンド URL Session クラスを作成しました
当社のURL Sessionには 後で検索する際に使用できる 固有の識別子を持つ background設定があります SendsLaunchEvents プロパティをtrueに 設定するとセッション上の タスクを処理する 必要があるとき セッションがバックグラウンドで Appを 起動することを示します なお大容量のデータを 転送する場合は URLSessionConfigurationの "isDiscretionary" プロパティをtrueに設定し システムがデバイスに 最適な時間に転送を スケジュールし 最高のパフォーマンスが 得られるようにします この場合 お客様には Wi-Fiと 電源がつながるまで ダウンロードができない 可能性があることも お知らせください
データを送信する 準備ができたら 転送をキューに加えて バックグラウンドセッションを スケジュールする 必要があります 設定変更用のサーバへの コンテンツを伴う URLリクエストを 作成して設定します
セッション上でリクエストの タスクを作成します この単純化した例では 1つのタスクしか 追加していませんが 効率化のために 複数のリクエストを セッションに追加することが できます earliestBeginDateにして ダウンロードを後ほど開始します なおシステムは バックグラウンドbudget ネットワーク システムの 条件に基づいて タスクの実際の開始時間を 決定します アクティブな文字盤に コンプリケーションを 表示している場合 Appは1時間に最大4つの バックグラウンド更新タスクを 受け取ることができます そのためシステムによって タスクが遅延しないように 最低15分間隔でタスクを スケジュールしてください
このセッションは進行中の セッションのリストに入れて 持っています これは後で システムが URLリクエストの完了を 知らせるときに 重要になります タスクに"resume"を 呼び出すと実際に 開始されるので これを 呼び出すことが重要です 最後にセッションに オブザーバーがいる場合に 備えて ステータスを キューに設定します Extension Delegateに 送信された バックグラウンドタスクを使用して バックグラウンドリクエストが 処理されると システムから Appに通知されます このタスクを処理する ためには WKExtensionDelegateに 準拠したクラスを作成し handle(_ backgroundTask:) 関数を 実装する必要があります Background URL Session Refreshタスクでは 進行中のリクエストの リストの中から 自分のセッションを 見つけようとします それがあれば私たちは 関数を呼び出して バックグラウンド更新タスクを セッションのリストに追加し データの処理が終わったら すぐに完了したことを システムに知らせることが できます これはすぐにお見せします
リストにセッションが 見つからない場合は タスクを完了したと マークする必要があります Background Refresh タスクが終了したら すぐに完了するように 設定することが 非常に重要です バックグラウンドタスクを 呼び出すためには もうひとつ 必要なことがあります デリゲートをAppに 接続する必要があるのです これを実現するためには WKExtensionDelegateAdaptor プロパティラッパーとして Extension Delegateクラスを 指定しAppに プロパティを追加します そしてシステムは バックグラウンドタスクを処理するために Extensionデリゲートを 呼び出します Extensionデリゲートでは 既存のセッションに バックグラウンドタスクを 追加するために この関数を呼び出しました バックグラウンドタスクのリストに このタスクを追加して URLデータの処理が 終わったら すぐに完了したことに できるようにします さて 往復のやり取りを 結びつけたので あとはデータを取得して システムに完了を 知らせるだけです URL Sessionの ダウンロードデリゲートは リクエストが完了したときに 呼び出されます ダウンロードタスクから 受信したデータを処理します このアイテムをAppが アクセスできる ディレクトリに移動するか ファイルのデータを 素早く処理することが 重要です この作業が完了すると ダウンロードしたファイルが 削除されます Extensionデリゲートから これ以上 バックグラウンドタスクを 得ることができないので このセッションは処理中の セッションリストから削除し またオブザーバーがいる 場合に備えて ステータスを完了に 設定しています ついにバックグラウンド タスクが完了しました これにより バックグラウンドの処理が 完了したことが システムに伝わります これを確実に行うことは Watch Appの 良い市民という だけではありません システムがバックグラウンド の制限を超えて Appを終了させることを 防ぎます これで終わりです 私たちはすべてを終えて バックグラウンドで設定を送信し あらゆる更新情報を 得ることができます なお 完全な実装では エラーや認証といったチャレンジを 処理する必要がありますが これは基本的な手順を 示しています フォアグラウンドURL Sessionを 使用すると ユーザーが Appを操作している間に 素早くサーバー通信できます 良い例は 最新のワークアウトリストや その日の瞑想の取得などです フォアグラウンド URL Sessionは データを送受信するための 電力効率の良くない方法であり 2分半のタイムアウトが 強制されます しかし実際にはフォアグラウンドの セッションはこの制限値より はるかに速いインタラクションに ターゲットを絞るべきです URL Sessionは 様々な目的で サーバーと直接 通信する場合に 最適な方法です ファミリー共有設定に対応している Appと一緒に使え iPhoneのコンパニオン Appに依存しません バックグラウンドセッションは データ転送を遅らせることが できる場所で使用し 大容量のデータを転送する ときは常に使用します URL Sessionについて 詳しく知りたい方は Developer App またはオンラインの “常に最新の コンプリケーションを”と “バックグラウンド進行の 謎を解く”をご覧下さい URL Sessionの他にも オーディオのストリーミング Appを作っている場合 サーバーと直接通信するには ソケットも選択肢の一つです Watch Appでは HTTP Live Streamingや Web Socketsを アクティブなストリーミング オーディオセッションの コンテキストで使用できます ソケット使用の詳細に ついては Developer App またはオンラインの “watchOS 6でオーディオを ストリーミングする”をご覧ください 多くのことを取り上げて きたので 今まで見てきたオプションを どのように 選択すればいいのかを まとめてみました すべての人のデバイスに 同期できる 機密性の高いデータの場合は iCloud同期機能を使った キーチェーンを選択します iCloudにデータベースを 保存して その人のすべてのデバイスで 共有する には CoreData with Cloudkitを 選択します コンパニオンのiPhoneと Watch Appの体験の最適化や コンパニオンAppの1つの デバイスにのみ存在する データを共有したりするには Watch Connectivityを選択 サーバーと直接通信するには URL Sessionを 選択します ストリーミングオーディオ Appはソケットも使えます ファミリー共有設定を使用している お客様をサポート またはiPhoneのデータ転送 を使用するために 必ず iCloud同期キーチェーン CloudKitでの CoreData URL Sessionまたは ソケットを選択して下さい ソリューションを 選択する前に データの種類 送信元と送信先 顧客層などを考えておくと その目的に適したツールを 選ぶことができます そして常に 展開する前に デバッガに接続されていない デバイスでAppをテストし 実環境での動作を 確認してください この度はWatch Appでの データ転送のための 素晴らしいツールを 学びに来ていただき ありがとうございました 次は何を作ってくれるのか 楽しみにしています [音楽]
-
-
4:20 - Password Autofill
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() { // Implement your cancel logic here } private func processLogin() { // Implement your login logic here } }
-
6:25 - Store Item in 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) } }
-
7:59 - Add Item to Keychain
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) } }
-
8:25 - Retrieve Item from Keychain
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) } }
-
9:39 - Remove Item from Keychain
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) } }
-
11:59 - Core Data SwiftUI View
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) } } } }
-
23:04 - Background URL Session Configuration
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 /// Data to send with the URL request. /// /// If this is set, the HTTP method for the request will be POST var body: Data? /// Optional content type for the URL request var contentType: String? private(set) var id = UUID() /// 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) }() private var sessionID: String { "\(sessionIDPrefix)\(id.uuidString)" } /// Initialize the session /// - Parameter url: The URL for the Background URL Request init(url: URL) { self.url = url super.init() } }
-
24:22 - Enqueue the background data transfer
// This is a member of the BackgroundURLSession class in the example. // Enqueue the URLRequest to send in the background. 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 }
-
25:45 - WatchKit Extension Delegate
class ExtensionDelegate: NSObject, WKExtensionDelegate { func applicationDidFinishLaunching() { // For Watch Connectivity, activate your WCSession as early as possible WatchConnectivityModel.shared.activateSession() } func applicationDidBecomeActive() { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillResignActive() { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, etc. } 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) } } } }
-
26:43 - Connect the WatchKit Extension Delegate to the App
@main struct MyWatchApp: App { @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extensionDelegate @SceneBuilder var body: some Scene { WindowGroup { NavigationView { ContentView() } } } }
-
27:07 - Store the Background Refresh Task it can be completed
// This is a member of the BackgroundURLSession class in the example. // 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) }
-
27:31 - Process Downloaded 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) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。