Parse Server Swift SDKの使い方（データストアクエリー操作）

[ニフクラ 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?
    var timeStamp: Date? = Date()
    var oldScore: Int?
    var isHighest: Bool?
}
```

## クエリーの基本形

クエリーは `query` メソッドを使います。引数は検索条件です。

```swift
let afterDate = Date().addingTimeInterval(-300)
var query = GameScore.query("points" > 50,
                            "createdAt" > afterDate)
    .order([.descending("points")])
```

## クエリーの実行

クエリーの実行は `find` メソッドを使います。非同期での実行は以下の通りです。注意点は、検索結果がない場合 `.failure` になることです。

```swift
query.limit(2)
    .order([.descending("points")])
    .find(callbackQueue: .main) { results in
    switch results {
    case .success(let scores):
        scores.forEach { score in
            guard let createdAt = score.createdAt else { fatalError() }
            print("スコア: \(score)")
        }
    case .failure(let error):
        if error.equalsTo(.objectNotFound) {
            assertionFailure("検索結果はありません")
        } else {
            assertionFailure("実行エラー: \(error)")
        }
    }
}
```

同期での実行は以下の通りです。

```swift
let results = try query.find()
results.forEach { score in
    guard let createdAt = score.createdAt else { fatalError() }
    print("Found score: \(score)")
}
```

## 最初の1件だけを取得

最初の1件だけを取得する場合は、 `first` メソッドを使います。非同期での実行は以下の通りです。この場合はParseObjectが返ってきます。

```swift
query.first { results in
    switch results {
    case .success(let score):
        guard score.objectId != nil,
            let createdAt = score.createdAt else { fatalError() }
        print("スコア: \(score)")
    case .failure(let error):
        if error.containedIn([.objectNotFound, .invalidQuery]) {
            assertionFailure("検索結果がない、またはクエリーが不正です")
        } else {
            assertionFailure("実行エラー: \(error)")
        }
    }
}
```

## 結果行数も取得する

結果行数も取得する場合は、 `withCount` メソッドを使います。非同期での実行は以下の通りです。

```swift
query.withCount { results in
    switch results {
    case .success(let (score, count)):
        print("スコア: \(score) トータル行数: \(count)")
    case .failure(let error):
        if error.containedIn([.objectNotFound, .invalidQuery]) {
            assertionFailure("検索結果がない、またはクエリーが不正です")
        } else {
            assertionFailure("実行エラー: \(error)")
        }
    }
}
```

## 相対的な時間でのクエリー

相対的な時間でのクエリーは `relative` メソッドを使います。非同期での実行は以下の通りです。

```swift
let queryRelative = GameScore.query(relative("createdAt" < "in 10 minutes"))
queryRelative.find { results in
    switch results {
    case .success(let scores):
        print("検索結果: \(scores)")
    case .failure(let error):
        print("実行エラー: \(error)")
    }
}
```

## 結果のフィールドを絞り込む

`select` メソッドを使うと、結果のフィールドを絞り込めます。非同期での実行は以下の通りです。

```swift
let querySelect = query.select("points")
querySelect.first { results in
    switch results {
    case .success(let score):
        guard score.objectId != nil,
            let createdAt = score.createdAt else { fatalError() }
        print("検索結果: \(score)")
    case .failure(let error):
        if let parseError = error.equalsTo(.objectNotFound) {
            assertionFailure("オブジェクトがありません: \(parseError)")
        } else {
            assertionFailure("実行エラー: \(error)")
        }
    }
}
```

逆に取り除く場合には、 `exclude` メソッドを使います。非同期での実行は以下の通りです。

```swift
let queryExclude = query.exclude("points")
queryExclude.first { results in
    switch results {
    case .success(let score):

        guard score.objectId != nil,
            let createdAt = score.createdAt else { fatalError() }
        print("検索結果: \(score)")

    case .failure(let error):
        if let parseError = error.containedIn(.objectNotFound, .invalidQuery) {
            assertionFailure("オブジェクトがありません: \(parseError)")
        } else {
            assertionFailure("実行エラー: \(error)")
        }
    }
}
```

## まとめ

以上、Parse Server Swift SDKのデータストアクエリー操作について解説しました。NCMBの検索方式とは若干異なる印象です。注意点は、検索結果がない場合にエラーになることでしょう。

とはいえ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)