Parse Server Swift SDKの使い方（データストアオブジェクトの複数CRUD操作）

[ニフクラ mobile backendは3月末で終了します](https://mbaas.nifcloud.com/info/2023/12/post-171.html)。

ニフクラ mobile backendからの移行先として、お勧めしているのが[Parse Server](https://parseplatform.org/)です。設計思想が近く、変更するコード量が少なく済むのではないかと思います。

Parse ServerではiOS向けにSDKを提供していますが、Swift SDKについてはあまりドキュメントが充実していません。そこで、各機能の使い方を解説します。今回はデータストアオブジェクトの使い方で、特に複数データをまとめて処理する方法を解説します。

## Swift SDKとObjective-C SDKのどちらを使うべきか

Parse ServerにはSwift SDKと[Objective-C SDK](https://docs.parseplatform.org/ios/guide/)が用意されています。機能的にいうと、Objective-C SDKの方が多いようです。しかし、公式メッセージとしてはSwift SDKを使っていくのを奨励しています。

## Parse Server Swift SDKのインストール

Swift SDKのインストールは、CocoaPodsやCarthageなどが使えます。しかし、一番簡単なのはXcodeのPackage Dependenciesを使う方法でしょう。

XcodeのFileメニューより、Add Package Dependenciesを選択して、出てきたダイアログで以下のURLを指定します。

```
https://github.com/parse-community/Parse-Swift.git
```

そして、利用するファイルでSDKをインポートします。

```swift
import ParseSwift
```

## 初期化

SwiftUIの例です。初期化は `(アプリ名)App.swift` にて行います。そして、初期化は `ParseSwift.initialize` にて行います。指定するアプリケーションID、クライアントキー、サーバーURLはそれぞれParse Serverを立ち上げる際に指定したものを使います。

マスターキーも指定できるようですが、アプリ側では使わない方が良いかと思います。

```swift
import SwiftUI
import ParseSwift

@main
struct ParseDemoApp: App {
    init() {
        ParseSwift.initialize(applicationId: "YOUR_APP_ID", clientKey: "YOUR_CLIENT_KEY", serverURL: URL(string: "https://example.com/parse")!)
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

## データストアオブジェクトの定義

データストアで利用するオブジェクトは `ParseObject` を継承して定義します。

```swift
struct GameScore: ParseObject {
    // 必須のプロパティ
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    var originalData: Data?

    // 独自のプロパティ
    var points: Int?

    /*:
		独自メソッド
     */
    func merge(with object: Self) throws -> Self {
        var updated = try mergeParse(with: object)
        if updated.shouldRestoreKey(\.points,
                                     original: object) {
            updated.points = object.points
        }
        return updated
    }
}
```


## 複数オブジェクトを保存する

データストアのオブジェクトを配列にし、 `saveAll` メソッドを使えば一回で保存処理を実行できます。

```swift
[score, score2].saveAll { results in
    switch results {
    case .success(let otherResults):
        var index = 0
        otherResults.forEach { otherResult in
            switch otherResult {
            case .success(let savedScore):
                print("""
                    Saved \"\(savedScore.className)\" with
                    points \(String(describing: savedScore.points)) successfully
                """)
                if index == 1 {
                    score2ForFetchedLater = savedScore
                }
                index += 1
            case .failure(let error):
                assertionFailure("Error saving: \(error)")
            }
        }
    case .failure(let error):
        assertionFailure("Error saving: \(error)")
    }
}
```

### 一括保存時のトランザクション

これはMongoDBの設定によっては機能しないとのことですが、トランザクションも利用できます。

```swift
[score, score2].saveAll(transaction: true) { results in
    switch results {
    case .success(let otherResults):
        var index = 0
        otherResults.forEach { otherResult in
            switch otherResult {
            case .success(let savedScore):
                print("Saved \"\(savedScore.className)\" with points \(savedScore.points) successfully")
                if index == 1 {
                    score2ForFetchedLater = savedScore
                }
                index += 1
            case .failure(let error):
                assertionFailure("Error saving: \(error)")
            }
        }

    case .failure(let error):
        assertionFailure("Error saving: \(error)")
    }
}
```

## 複数データの取得

objectIdを指定してフェッチできますが、これも複数データに対して一括処理を行えます。

```swift
let score = GameScore(objectId: "ABCDE")
let score2 = GameScore(objectId: "01234")

[scoreToFetch, score2ToFetch].fetchAll { result in
    switch result {
    case .success(let fetchedScores):
        fetchedScores.forEach { result in
            switch result {
            case .success(let fetched):
                print("取得成功しました: \(fetched)")
            case .failure(let error):
                print("取得失敗しました: \(error)")
            }
        }
    case .failure(let error):
        assertionFailure("エラーが発生しました: \(error)")
    }
}
```

`fetchAll` も同期処理が可能です。

```swift
do {
    let fetchedScores = try [scoreToFetch, score2ToFetch].fetchAll()
    fetchedScores.forEach { result in
        switch result {
        case .success(let fetched):
            fetchedScore = fetched
            print("Successfully fetched: \(fetched)")
        case .failure(let error):
            print("Error fetching: \(error)")
        }
    }
} catch {
    assertionFailure("Error fetching: \(error)")
}
```

## 更新の一括処理

更新は `save` メソッドを使います。同期、非同期ともに、保存時と同じように処理します。

## 削除の一括処理

削除は `deleteAll` を使います。

```swift
[scoreToFetch, score2ToFetch].deleteAll { result in
    switch result {
    case .success(let deletedScores):
        deletedScores.forEach { result in
            switch result {
            case .success:
                print("Successfully deleted score")
            case .failure(let error):
                print("Error deleting: \(error)")
            }
        }
    case .failure(let error):
        assertionFailure("Error deleting: \(error)")
    }
}
```

同期処理は `try` を使う点も同じです。

```swift
do {
    let fetchedScores = try [scoreToFetch, score2ToFetch].deleteAll()
    fetchedScores.forEach { result in
        switch result {
        case .success(let fetched):
            print("Successfully deleted: \(fetched)")
        case .failure(let error):
            print("Error deleted: \(error)")
        }
    }
} catch {
    assertionFailure("Error deleting: \(error)")
}
```

## まとめ

NCMBにはデータの一括処理がなかったので、これはParse Server特有の機能です。複数データをまとめて登録する際などに便利そうです。

Parse ServerとNCMBのデータストア操作は、SDKの仕組みで異なる部分が多いです。載せ替える際には、コードの修正が必要になるでしょう。

とはいえ、データの管理方法などはParse ServerとNCMBで似ています。他のmBaaSと比べると、修正量はそこまで多くないと思われます。載せ替え先として検討に挙げてください。

[parse\-community/Parse\-Swift: The Swift SDK for Parse Platform \(iOS, macOS, watchOS, tvOS, Linux, Android, Windows\)](https://github.com/parse-community/Parse-Swift)