CloudKitのデータをCSVにExportする方法(Swift5)

iOS アプリを開発するときに、データの格納先としてCloudKit を選択することは多いのではないだろうか。CloudKit はiCloud 上のデータベースで、アプリ単位1PB まで利用できる。 

CloudKit Dashboard という管理機能も備え、スキーマやレコードの管理もでき非常に便利なのだが、一つ大きな欠点がある。


CloudKit Dashboard からデータをエクスポートできない

報告や分析するときにデータをエクスポートして加工することは多いのではないだろうか。残念ながらCloudKit Dashboard 上では、そこまでグラフィカルな表現はできず、またデータのエクスポート機能も備えていない。

そこで今回はSwift5 でCloudKit のデータをCSV にExport する方法を書く。


Swift5 でCloudKit のデータをCSV にExport する

ソースコード

    private func exportCloudKit(){
            let df = DateFormatter()
            df.dateFormat = "yyyy-MM-dd HH:mm:ss"
            // Change header
            var text = "Created,Code,Name\n";

            cloudKitLoadRecords() { (records, error) -> Void in
                if let error = error {
                    print(error)
                } else {
                    if let records = records {
                        for record in records{
                            // Change Fields
                            let date = record.creationDate != nil ? df.string(from: record.creationDate!) : ""
                            let code = record["code"] as! String
                            let name = record["name"] != nil ? record["name"] as! String : ""
                            text += date + "," + code + "," + name + "," + "\n"
                        }
                    }
                    
                    let dir = FileManager.default.urls(
                        for: .documentDirectory,
                        in: .userDomainMask
                    ).first!
                    let fileUrl = dir.appendingPathComponent("CloudKitExport.csv")

                    if FileManager.default.createFile(
                                    atPath: fileUrl.path,
                                    contents: text.data(using: .utf8),
                                    attributes: nil
                                    ) {
                    } else {
                        print("Failed to create file")
                    }
                    
                    print("Processed")
                }
            }
        }

    func cloudKitLoadRecords(result: @escaping (_ objects: [CKRecord]?, _ error: Error?) -> Void) {
        // Change recordType
        let cloudKitQuery = CKQuery(recordType: "Sample", predicate: NSPredicate(value: true))

        var records = [CKRecord]()

        // If you use Private Database, Chage here 'CKContainer.default().privateCloudDatabase'
        let database = CKContainer.default().publicCloudDatabase

        var recurrentOperationsCounter = 101
        func recurrentOperations(cursor: CKQueryOperation.Cursor?){
            let recurrentOperation = CKQueryOperation(cursor: cursor!)
            recurrentOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
                recurrentOperationsCounter += 1
                records.append(record)
            }
            recurrentOperation.queryCompletionBlock = { (cursor: CKQueryOperation.Cursor?, error: Error?) -> Void in
                if ((error) != nil) {
                    print(String(describing: error))
                    result(nil, error)
                } else {
                    if cursor != nil {
                        recurrentOperations(cursor: cursor!)
                    } else {
                        result(records, nil)
                    }
                }
            }
            database.add(recurrentOperation)
        }

        var initialOperationCounter = 1
        let initialOperation = CKQueryOperation(query: cloudKitQuery)
        initialOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
            initialOperationCounter += 1
            records.append(record)
        }
        initialOperation.queryCompletionBlock = { (cursor: CKQueryOperation.Cursor?, error: Error?) -> Void in
            if ((error) != nil) {
                print(String(describing: error))
                result(nil, error)
            } else {
                if cursor != nil {
                    recurrentOperations(cursor: cursor!)
                } else {
                    result(records, nil)
                }
            }
        }
        database.add(initialOperation)
    }

使い方

今回の例では手間を省くためXCode のApp デフォルトで作成されるものをベースに、ContentView.swift に↑のfunc を置いてbody を以下のように変更した。

    var body: some View {
        List {
        }
        .toolbar {
            Button(action: exportCloudKit) {
                Label("export", systemImage: "square.and.arrow.down")
            }
        }
    }

CloudKitExporter 画面

右上のボタンを押すとCSV を出力する。

CSV の出力先は、ユーザ名\ライブラリ\Containers\ツール名\Data\Documents になるので、場所がわからないときはファイル名(CloudKitExport.csv)で検索する。


使うときに変更する箇所

CSV のヘッダ。今回はCreated, Code, Name の3つを出力している

// Change header
var text = "Created,Code,Name\n";

出力したいフィールド。値がない可能性があるフィールドは、name のようにnilチェックする

// Change Fields
let date = record.creationDate != nil ? df.string(from: record.creationDate!) : ""
let code = record["code"] as! String
let name = record["name"] != nil ? record["name"] as! String : ""
text += date + "," + code + "," + name + "," + "\n"

出力したいRecordType。

// Change recordType
let cloudKitQuery = CKQuery(recordType: "Sample", predicate: NSPredicate(value: true))

データベースがPublic かPrivate か

// If you use Private Database, Chage here 'CKContainer.default().privateCloudDatabase'
let database = CKContainer.default().publicCloudDatabase

設定方法は後述するが、Production かDevelopment か

entitlements 画面


ハマったポイント

100レコードまでしか出力されない

最初はperformQuery を使っていたが、100レコードまでしか出力されなかった。CKQueryOperation を使って回す必要があった。


デバッグ実行でProduction からデータを出力したい

特に設定しない場合は、Development からデータが出力された。以下の設定をすることでProduction から出力できた。

  • entitlements を開く
  • +ボタンを押して、com.apple.developer.icloud-container-environment のキーを追加する
  • Type をString 、Value をProduction に設定する。

entitlements 画面

コメント

このブログの人気の投稿

メールのURL(リンク)が途切れる問題に対応するアドイン(Outlook)

メールからRedmineのチケットを作成するアドイン(Outlook)

複数行テキストの「表示数を増やす」を自動で開くChrome拡張機能(SharePointモダンリスト)

もしもアフィリエイト かんたんリンク文字化け対策ツール(全角→半角記号変換)

Google Meetの参加リクエストを自動承諾するChrome拡張機能