ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
CloudKitの新機能
CloudKitは、安全で便利で信頼性の高いクラウドデータベースをAppに提供し、さらに進化しています。async/awaitのサポートと便利なAPIの追加により、どのようにスレッドを解きほぐすことができるかをご確認ください。また、データのレコードゾーン全体を共有することで、Appユーザ間のコラボレーションを促進する方法も紹介します。暗号化された値などのCloudKitの機能を利用して、App内の機密データを保護する方法についても検証します。 このセッションを最大限活かしていただくためには、CloudKitおよびコンテナー上のCloudKit操作に関する知識があり、レコードとデータのタイプの基礎を理解していることが推奨されます。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
Tech Talks
-
ダウンロード
ようこそ 「CloudKitの新機能」へ CloudKitチームのエンジニア Nihar Sharmaです 後ほどQianが加わります Swiftの並行処理を活用する CloudKit APIの変更について まずお話しします その後 Quianがレコードの 暗号化フィールドについて お話しします
そして最後にレコードゾーンを 簡単に共有する機能に触れます
まずはCloudKitとSwiftについてです CloudKitはAppがiCloudの データベースにアクセス できるフレームワークです APIのCKContainerで いくつものCKDatabasesに アクセスできます
それぞれのコンテナに全ユーザーが 読み書きできる パブリックデータベースが 1つずつあります デバイスがiCloudに ログインしていれば Appはユーザーデータがある プライベートデータベースに アクセスすることもできます もしAppが共有機能をサポートしていれば 現在のiCloudユーザーが共有するデータに 共有のCKDatabaseで アクセスすることができます CloudKitにコードを書くとき 2つのAPIのエリアがあります まずは関数です これはCKContainerと CKDatabaseにあります このAPIはCloudKitの 導入初心者向けで コードを書きやすくします すべての構成を 提供する代わりに ユーザーが操作するUI Appに 最も相応しいデフォルトの動きを フレームワークが選びます 次はNSOperation サブクラスである Operation APIです このAPIはCKContainerや CKDatabase関数にない機能を 提供します
それらはサーバに一往復で 多数のアイテムを送受したり サーバから徐々にデータを 取り寄せ 莫大な結果セットを 検索したり サーバからの過去のデータベースと レコードゾーンチェンジのリクエスト そして異なるオペレーション のグループ化などです これにより ユニットとしてログされ オペレーションの作業量を システムに 知らせることができます いずれは多くのデベロッパが このAPIを使うでしょう Swift並行処理の新機能を使い CloudKitに改良点が加えられました まずCloudKit APIsと共に Swift async/await機能が 使えることです 次にper-itemと per-operationの コールバックの違いを 明確にする新しいAPIと これらのコールバックの パラメータの役割を 明確にするSwift.Resultタイプを どう利用しているか 最後に 今までオペレーションAPI にしかなかった機能と 環境設定を実現する コンテナとデータベースの 関数にどのような改善を 追加したかお話しします
CloudKit APIは コンテナとデータベースで 関数に非同期バリアントを追加します 並行処理に関わるコードの改善に 非同期関数を使用できます エラーの処理が自然になり コードの視覚的な制御フローが シンプルになります 非同期関数の詳細は 「Swiftのasync/awaitについて」 を参照してください それではその例です
これはPrivateDatabase コードのサンプルです これは最近AppleがGitHubに掲示した CloudKit特有のサンプルで いつでもご覧になれます この関数は データをサーバから削除し 完了時に通知するというものです
多数のオプションや 条件のラップ解除が 各所に見られます この関数を理解する上で 制御フローは明確ではありません
それと比べ これは CloudKitの非同期関数 を使ったコードです オプションやラップ解除が なくなり 制御フローが直線的で 簡単に確認できます
GitHubのコードサンプルでは Swift並行処理を使って コードをどうのように リファクターできるのかがわかります
次にper-itemのコールバックです 例としてここに CKFetchRecordsOperationが サーバーに4つの CKRecord.IDsを送り 4つのCKRecord payloadsを 呼び戻しています このオペレーションには 3つ可能性があります
まず オペレーションが成功すること エラーもなくサーバからレコードが 取得できました
2つ目は オペレーション全体のエラーです これはオペレーション全体が 失敗になるエラーです デバイスがネットワークに 接続していないのが一例です networkUnavailable エラーコードが出て オペレーションは失敗です
3つ目は オペレーションが無事 サーバと往復したものの サーバからは 3つのCKRecordsとともに 4つ目のレコードはないという エラーメッセージが 戻ってきました このエラーでは per-itemのエラーは unknownItemで partialFailureという per-operationのエラーに 含まれます コードではどう処理されるのでしょう? CKFetchRecordsOperationが per-recordの完了ブロックと per-operationの完了ブロックを宣言し それぞれの実装例が 下部に表示されます
2つのコールバックが 重複しています 先程の例のように コードは 2つのper-itemのエラーを予想します per-itemコールバックの .unknownItemエラー および per-operationコールバックの .partialFailureエラーに含まれる形です 同様にper-itemの成功も レコード取得の成功として 2箇所に表示されます まずper-itemのコールバックの 上位パラメータに そしてper-operation コールバックの結果の成功として recordsByRecordIDの辞書内に 表示されます
Swift.Resultタイプを使い APIを明確にするため CloudKitはこれのコールバックを 置き換えました
新しい結果ベースのコールバックで ブロックパラメータが 上位で分離しており perRecordResultBlockに CloudKitが呼び戻している アイテムのIDがあります そしてper-itemの結果もあります 結果がクリアに表示され CKRecord payloadを 無事 呼び出せたか per-itemエラーが発生したかが わかります
同様に オペレーション単位の 完了ブロックも オペレーション単位の 結果ブロックにアップデートされ per-itemの結果ブロックの 成否の結果報告と 重複しません
CloudKitは正式に 懸念を分離しました 1つのブロックはアイテム毎の報告用 もう1つはオペレーション単位の 報告用です
先程の例に戻ると CKRecord payloadsの 取り寄せに成功した 3つのper-item結果ブロックの 報告と .unknownItemエラーの per-item結果ブロックの 報告1つ そして per-operation結果ブロックの ノーエラー報告です オペレーション全体は おおよそ成功したからです CloudKitの 向上された機能の一つは per-itemとper-operationの コールバックを 全体的に分離表示している点です per-itemエラーを表示する per-itemコールバックは これまで ハイライトされている オペレーションに限られていました
しかしこれからは 全てのCKOperationsにおいて 該当する場合 per-itemコールバックが per-itemエラーをパスバックします 次はコンテナと データベースAPIにおける 向上と拡張を見てみましょう CKContainerと CKDatabaseに 新機能が追加されます
これらの新機能は CKContainerとCKDatabaseの 関数としてCKOperation APIsの 大部分を構成します これはオペレーションAPIの 1対1マッピングでは ありません その代わりに デフォルトパラメータと Swift.Resultタイプを使い 親しみやすく強力で async/awaitを使える APIを作成しました 新しい関数はそれぞれ2度 完了ハンドラと 非同期関数として使われます このAPIを使い コンテナと データベースの関数は 複数アイテムのバッチ処理や 大量データの検索 変更の取り寄せなどの オペレーションAPIの機能を サポートします またロギングで関数の呼び出しを グループ化したり ワークロードの合計サイズを システムに伝達できます
関数の呼び出しでは タイムアウトなどの 設定も可能です 例を見てみましょう
それでは再び レコードを削除する 非同期関数を使った GitHub PrivateDatabaseの サンプルコードです
アイテムバッチ処理で このコードをアップデート してみましょう 向上したデータベースの 関数APIを使い 小さなレコード2つを 削除するため 関数に変更を加えます
問題が分類化されました ハイライトされた部分は 関数スコープです 関数を起動し 関数上でのエラーを発見します
関数が無事終了すれば この部分でper-itemの 成功・失敗がチェックされます GitHubのコードサンプルに 各機能をカバーした例があり このセッションのノートに それらのリンクがあります ぜひご利用ください それでは これからQuianが 暗号化フィールドについて お話しします ありがとう Nihar Quianです ではこれから ユーザーデータの プライバシーを簡単に守る CloudKitの新機能を ご紹介します まず CloudKitがどのように ユーザーデータを守るか 概要を説明し データ暗号化の新機能を紹介 そしてユーザーアカウントにおける 必要条件についてお話しします Appleでは全製品の製作において プライバシーはコアバリューです AppleのAppとサービスの フレームワークとして CloudKitは常にプライバシー テクノロジーを革新し CloudKitに保存された データの保護を提供します まずCloudKitがどうデータを 保護するかです CloudKitには2つの データ保護方法があります アカウントベースの保護と 暗号法による保護です
CloudKitに保存されるデータは デフォルトでアカウントベース の認証で保護されています これには皆さんの CloudKit-backed Appと AppleのCloudKit-backedの 全Appが含まれます 保存と取得段階でCloudKitは セキュアトークンを使い 認証されたユーザーのみが データにアクセスできます Appleも第三者も アクセスできません
アカウントベースの保護は プライベートと共有データ ベースのデータのみです それらのデータベースでは データは特定の iCloudアカウントに属するか 共有されているかで 共有データは認証が必要です しかし公共データベースは 全ユーザーが データにアクセスできるので アカウントベースのデータ保護は デフォルトで適用されません
それではもう一つの データ保護技術である 暗号保護です CloudKitはApple所有のAppと サービスの 機密性の高いデータおよび CKAssetとして保存される デベロッパAppの全ユーザー データに対し 暗号保護を提供しています これらのデータはCloudKitに 送られ保存される前に デバイスで処理・暗号化され 呼び込まれたデータはデバイスで ローカルに解読されます
この暗号化機能はデバイスで サインインされた iCloudアカウントに属する iCloud Keychainの 重要材料を使います またCloudKitの 共有機能と互換性があり CKShareのユーザーのみが 関連する暗号フィールドを 解読できます
暗号保護は アカウントベースの保護に さらなるレイヤーを加えます 権限のない者が 認証の壁を仮にすり抜けても 呼び出したデータは 解読できません
暗号保護はユーザーの プライベートまたは 機密性の高いデータに 使用されるべきです AppleのCloudKit-backed Appの多くが この機能を取り入れています 写真とメモがその例です
これまでCloudKitの ノンアセットデータ保護は デフォルトで アカウントベースの保護でした このたびCloudKitでは 鍵導出や管理・暗号化・解読に加え 暗号保護を提供し これまで以上に強い プライバシー保護で CloudKit-backed Appの 構築を応援します
では新しいAPIを 見てみましょう CKRecordsの新プロパティ encryptedValuesに どのようなキー・値ペアも追加でき 解読されたオリジナル値 を呼び戻すのに 同じプロパティが使えます
encryptedValues APIが CloudKitサーバー上で どのように暗号化データを 同期させるか見てみましょう ここに2つのデバイスと CloudKitサーバーがあります encryptedValuesの キー・値ペアを設定すると CloudKitは自動的に CKModifyRecordsOperationで レコード値をローカルに暗号化し サーバに送ります 別のデバイスで データをサーバから 引き出した後 同じAPIを使って CloudKitは自動的に key value pairを解読します
このプロセスは 最小限のコードで完了します 最初のデバイスで encryptedValues APIを使い キー・値ペアを設定します この場合キーは encryptedStringFieldで 値は文字列オブジェクトです CKModifyRecordsOperationを その後呼び出し 新しいデータを サーバに保存します
CKFetchRecordsOperationを 2つ目のデバイスで呼び出し 暗号化データを呼び出します 同じencryptedValues プロパティを使い この文字列が戻ってきます 以上です 1つのシンプルなプロパティが 暗号化と解読処理を行います CKReferenceを除いて ほぼ全てのCKRecord 値タイプを 暗号化できます CKReferenceはサーバに 見えねばなりません CKAssetsフィールドは 前にも話した通り すでにデフォルトで 暗号化していますので encryptedValueとして 設定はできません
CloudKitデータベーススキーマで 暗号化フィールドを視覚化できます 他のフィールドと同様です 「CloudKitコンソールについて」 のセッションで その他の変更について 知ることができます コンソールの レコード値データタイプの ドロップダウンリストで 全フィールドを一覧できます
Encrypted Double Encrypted Timestampの様に “Encrypted”から始まっており 暗号化されていないものと 区別できます またコードの変更なく コンソール自体で フィールドを管理できます 例えばご自分の開発 データベーススキーマで 新しい暗号化フィールドを 加えられます
次に暗号化に伴う アカウントのオペレーション に関する必要条件です 他のプライベート及び 共有データベース同様 ログインされた 有効なアカウントが必要です 初期化のロジックで CKContainer accountStatus (completionHandler:)を使い アカウントのステータスを 確認する必要があります
プライベートと共有データベースでの オペレーションでは ステータスが.availableで なければなりません
それ以外のステータスは CKErrorNotAuthenticatedの エラーメッセージが出て 今年から含まれる .temporarilyUnavailableは ログインされているものの 準備ができていず ユーザーに設定Appでログイン情報を 再確認してもらう必要があります
ユーザーアカウントが .availableでない場合 CKAccountChanged通知で ステータスが変更され 準備ができているという通知を 受信できるようにします
CloudKitでの データの暗号化に関し 知るべきことは以上です 独自の解決方法を 実装する時間と努力を節約し ユーザーデータを 保護することができます 続いて 再びNiharが ゾーンの共有について お話しします ありがとう CloudKit共有 について話します
CloudKitは ユーザーの すべてのデバイスを通し ユーザーデータの 保存・同期に役立つ 保護意識の強い iCloudデータベースです iOS 10とmacOS Sierraで 他のiCloudユーザーと安全に データを共有できる CloudKit共有が登場 新機能について話す前に CloudKit共有の仕組みを 見てみましょう
CloudKit共有は CKShareオブジェクトの作成で 開始されます これは誰とデータを共有し どのような許可を 持っているかなど 共有されるデータを 共有関連情報から 分別するものです 水面下で CloudKitはリクエストに対し アカウントベースの 認証だけでなく 参加者の共有データへの 暗号化アクセスを 設立します
主に2つの方法で Appに共有機能のサポートを 加えることができます iOSの UICloudSharingControllerか macOSの NSSharingServiceで 共有管理のシステム提供UIで 手早く始めるか これらのフレームワーク オペレーションを使い ユーザーに共有設定をさせる カスタムUIを作るかです
先ほど話したように CKSharesは 共有する物と 共有する相手を分別します 今日はその前半部分に注目し 幾つかの違ったデータ設計と CloudKit sharing APIsの使用で それがどう影響するかについて お話ししたいと思います
まず既存のCloudKit共有機能を 利用する例を見てみましょう iCloudドライブのフォルダ共有は CloudKitで構築されています どうやって似たようなものを Appに組み込めるでしょうか このデータモデルは ファイルシステムの階層で まず「ファイル」と「フォルダ」 タイプのレコードから始めます それらの中に含まれる ファイルやフォルダとその中身を 含む全てのレコードを ユーザーが簡単に 共有できるようにします
このCloudKit内の 階層関係を表し 共有に利用するには 子から親レコードにおいて CKRecord.parentを 使用することです
これによりCloudKitは 結果となる階層を 一つの共有ユニットとします ですのでここに リファレンスを足します これは非常に重要で 親リファレンスがCloudKitで 特別な理由です 共有をサポートしないなら 親リファレンスを使う必要はなく シンプルなCKReference フィールドでも十分です
これでフォルダの共有は CKShareの初期設定で サポートされ フォルダレコードがCKShareの ルートレコードとなります
フォルダをルートレコード として使えば CloudKitは自動的に 親リファレンスベースの 階層の一部である すべてのレコードを 共有することになります また 後ほどこの階層に レコードを追加したり削除すると 自動的にそれが共有または 共有解除されます ではコードでどう 設定すればいいでしょうか?
ここにプライベート データベースの カスタムゾーンで共有する 2つのファイルレコードと 1つのフォルダレコードがあります
まず両ファイルに 親リファレンスが設定され フォルダレコードを指しています ファイルレコードが 保存されました フォルダが共有された時 変更が必要なレコード数を 最小限にするため 親リファレンスを すぐに保存する習慣を つけましょう
フォルダをルートレコードとして CKShareを初期化し プライベートデータベースで フォルダレコードと共に CKShareを保存し 3つのレコードが全て 共有されます 親リファレンスがこの前に サーバに保存されているため 変更が必要なのは ルートフォルダレコードと 共有時の共有だけです
これであなたのAppは フォルダレコードと その傘下全てのレコードを 共有できました CloudKitはレコード階層が 重複しない限り同ゾーン内で 複数のCKSharesを サポートします
では階層フォルダ共有 モデルの代わりに ゾーンの中に 階層関係のない 幾つかのレコードが あるとします
つまり ゾーンは レコードの入ったバケツで それらをすぐに 共有したいとします
どのレコードも変更すること なく ゾーン全体を 共有できるのが理想的です
ゾーンの共有でそれが可能です コードで設定しましょう
CKShareの 新しいイニシャライザを使い プライベートデータベースの 既存ゾーンの レコードゾーンIDを 得るだけです ゾーン全体の共有レコードが 保存されれば サーバ上でこのゾーンにある レコードは すべて自動的に共有され このゾーンから レコードを追加・削除するだけで 共有と共有解除が 可能になります ゾーン全体の共有レコードを 削除することで レコードゾーン全体の共有を いつでも解除できます ゾーン全体の共有レコードを さらに見てみましょう
ゾーン全体の共有レコードは 常にわかりやすい名前です CKRecordNameZoneWideShareは ゾーンIDとともに フルの共有レコードIDを作成します
ゾーンの共有を使用している場合 親リファレンスの設定は不要です
ゾーンの共有は ゾーン毎に共有レコード1つです このタイプの共有では 同じゾーン内で 階層共有は共存できません ですので ゾーン内で いくつかの階層共有をするか ゾーン全体の共有レコード1つ のみかのどちらかです
ゾーン全体のシェアは いずれの非デフォルトの レコードゾーンも保存でき 新しいゾーン機能 CKRecordZoneCapability ZoneWideSharingで 記されています
CKShareレコード作成以降の 既存のCloudKit 共有方法の すべてはこれまで通りで ゾーン全体のシェアを サポートしていますが 1つだけ例外があります ゾーンの共有には ルートレコードがないため hierarchicalRootRecordIDや rootRecordなどCKShareMetadata の関連プロパティはゾーン共有の
受け入れ時にnilになります カスタム共有 の受入れフローのブートストラップに CKFetchShareMetadataOperation を使用すると shouldFetchRootRecordと rootRecordDesiredKeysは ゾーン全体のシェアの 共通メタデータを読み込み時に システムに無視されます
皆さんのデータモデルにより 2タイプのCloudKit共有があります Appのスキーマが階層を形成し 階層ツリーが理に叶うのであれば CKRecordの親リファレンスで ルートレコードを共有しましょう Appleではメモやリマインダー iCloud Driveフォルダ共有などを 今日紹介したような形で これを実装しています
それ以外なら ゾーン全体の共有レコードで 全レコードゾーンを共有し CloudKit共有をフルに利用できます Appleでは既にゾーンの共有を HomeKitセキュアビデオや HomePodマルチユーザーに 活用しています
今日はper-item progressの向上や エラーレポートAPIsなど Swiftのasync/awaitを使い 新しい方法でCloudKitの コードを書く方法を ご紹介しました
独自に暗号化に取り組むことなく Appleのプライバシー保護を利用し 暗号化フィールドを 機密性の高いユーザーデータに 使用する方法をご紹介しました
そしてデータモデルが 階層タイプでない場合 ゾーンの共有で CloudKitに素早く 取り掛かる方法をお伝えしました
これらの機能についてや さらに役に立つ情報を developer.apple.comで ご覧いただけます 「Explore CloudKit」コレクションに 関連する数々のセッションを ご用意しています CloudKitを基に作られた 共有機能である Core Dataもその一つです 引き続きWWDCをお楽しみください [明るい音楽]
-
-
3:34 - CloudKit: Existing convenience API
// Sample code using existing Convenience API /// Delete the last person record. /// - Parameter completionHandler: An optional handler to process completion `success` or `failure`. func deleteLastPerson(completionHandler: ((Result<Void, Error>) -> Void)? = nil) { database.delete(withRecordID: lastPersonRecordId) { recordId, error in if let recordId = recordId { os_log("Record with ID \(recordId.recordName) was deleted.") } if let error = error { self.reportError(error) // If there is a completion handler, pass along the error here. completionHandler?(.failure(error)) } else { // If there is a completion handler, like during tests, call it back now. completionHandler?(.success(())) } } }
-
4:04 - CloudKit: Async convenience API
// Sample code updated to CloudKit Async API /// Delete the last person record. func deleteLastPerson() async throws { do { let recordId = try await database.deleteRecord(with: lastPersonRecordId) os_log("Record with ID \(recordId.recordName) was deleted.") } catch { self.reportError(error) throw error } }
-
5:39 - CloudKit: Existing completion blocks
// Error reporting in CKFetchRecordsOperation extension CKFetchRecordsOperation { var perRecordCompletionBlock: ((CKRecord?, CKRecord.ID?, Error?) -> Void)? var fetchRecordsCompletionBlock: (([CKRecord.ID : CKRecord]?, Error?) -> Void)? } fetchRecordsOp.perRecordCompletionBlock = { record, recordID, error in // error is CKError.unknownItem. } fetchRecordsOp.fetchRecordsCompletionBlock = { recordsByRecordID, operationError in // operationError is CKError.partialFailure. // operationError.partialErrorsByItemID[missingRecordID] is CKError.unknownItem. }
-
6:35 - CloudKit: Result type completion blocks
// Error reporting in CKFetchRecordsOperation extension CKFetchRecordsOperation { var perRecordResultBlock: ((CKRecord.ID, Result<CKRecord, Error>) -> Void)? var fetchRecordsResultBlock: ((Result<Void, Error>) -> Void)? } fetchRecordsOp.perRecordResultBlock = { recordID, result in // result is .failure(CKError.unknownItem) or .success(record). } fetchRecordsOp.fetchRecordsResultBlock = { result in // result is .success. }
-
9:14 - CloudKit: Delete single item
// Single item delete func deleteLastPerson() async throws { do { let recordId = try await database.deleteRecord(with: lastPersonRecordId) os_log("Record with ID \(recordId.recordName) was deleted.") } catch { self.reportError(error) throw error } }
-
9:37 - CloudKit: Delete batch
// Batched modifications func deleteLastPeople() async throws { do { let recordIds = [lastPersonRecordId, penultimatePersonRecordId] let (_, deleteResults) = try await database.modifyRecords(deleting: recordIds) for (recordId, deleteResult) in deleteResults { switch deleteResult { case .failure(let error): self.reportError(error, itemId: recordId) case .success: os_log("Record with ID \(recordId.recordName) was deleted.") } } } catch let operationError { self.reportError(operationError) throw operationError } }
-
13:43 - CloudKit: Encrypted values
extension CKRecord { @NSCopying open var encryptedValues: CKRecordKeyValueSetting { get } }
-
14:29 - CloudKit: Using encrypted values
// Device 1: Encrypt data before calling CKModifyRecordsOperation. myRecord.encryptedValues["encryptedStringField"] = "Sensitive value" // Device 2: Decrypt data after calling CKFetchRecordsOperation. let decryptedString = myRecord.encryptedValues["encryptedStringField"] as? String
-
16:35 - CloudKit: Account status
open func accountStatus(completionHandler: @escaping (CKAccountStatus, Error?) -> Void)
-
16:46 - CloudKit: CKAccountStatus
public enum CKAccountStatus : Int { case couldNotDetermine case available case restricted case noAccount case temporarilyUnavailable }
-
21:10 - CloudKit: Setup a record hierarchy
// Share a record hierarchy let zone = CKRecordZone(zoneName: "MyZone") // Save zone... let fileRecordA = CKRecord(recordType: "File", recordID: CKRecord.ID(zoneID: zone.zoneID)) let fileRecordB = CKRecord(recordType: "File", recordID: CKRecord.ID(zoneID: zone.zoneID)) let folderRecord = CKRecord(recordType: "Folder", recordID: CKRecord.ID(zoneID: zone.zoneID)) fileRecordA.setParent(folderRecord) fileRecordB.setParent(folderRecord) // Save records...
-
21:41 - CloudKit: Record Hierarchy, Share
// Share a record hierarchy let share = CKShare(rootRecord: folderRecord) do { let (saveResults, _) = try await database.modifyRecords(saving: [folderRecord, share]) for (recordID, saveResult) in saveResults { // Handle per-record result. } } catch let operationError { // Handle operation error. }
-
22:51 - CloudKit: Share a Record Zone
// Share a record zone let zone = CKRecordZone(zoneName: "MyZone") // Save zone... let share = CKShare(recordZoneID: zone.zoneID) do { let (saveResults, _) = try await database.modifyRecords(saving: [share]) for (recordID, saveResult) in saveResults { // Handle per-record result. } } catch let operationError { // Handle operation error. }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。