【NCMBハンズオン】Swift SDKを使ってTodoアプリを作ろう

## 必要なもの

- Xcode
- Ruby
- CocoaPods
- NCMBのアカウント

---

## ベースアプリについて

ベースアプリは[NCMBMania/Swift_Todo_Handson](https://github.com/NCMBMania/Swift_Todo_Handson)になります。まずGitリポジトリをクローンしてください。

```
git clone git@github.com:NCMBMania/Swift_Todo_Handson.git
```

---

### Swift SDKの導入について

該当リポジトリにはすでにPodfileを用意しています。

```
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'todoapp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for todoapp
  pod 'NCMB', :git => 'https://github.com/NIFCLOUD-mbaas/ncmb_swift.git'
end
```

---

CocoaPodsのインストールはターミナルで次のように実行します。

```
$ gem install cocoapods
```

---

NCMB SDKをCocoaPodsでインストールします。

```
$ pod install
```

---

## todoappApp.swiftの編集

todoappApp.swiftはアプリが立ち上がった際の初期設定を行います。まずNCMB SDKを読み込みます。

```swift
import NCMB
```

---

## SDKの初期化

`// NCMBの初期化処理をここに書きます` 以下に記述していきます。

---

SDKを初期化します。アプリケーションキー、クライアントキーはあらかじめ取得してください。

```swift
// アプリがアクティブになったタイミングでNCMBを初期化
NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
```

---

## 匿名認証を有効にする

NCMBの管理画面にて、匿名認証を有効にしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/eb941831-aecb-659d-8172-26c761cc4c2d.png)

---

さらにSwift SDKで匿名認証を有効にします。

```swift
// 匿名認証を有効にする
NCMBUser.enableAutomaticUser()
```

---

## ログインユーザを取得する

現在ログインしているユーザの情報を取得します。

```swift
// 現在ログインしているかチェック
let user = NCMBUser.currentUser
```

---

## 認証状態のチェック

user が nil かどうかで判定します

```swift
if user == nil {
    // ログインしていない場合
} else {
    // ログインしている場合
}
```

---

## ログインしていない場合

ログインしていない場合は匿名認証を行います。

```swift
// 匿名認証を実施
NCMBUser.automaticCurrentUserInBackground(callback: { result in
    // 認証結果の判定
    switch result {
            case .success:
                // ログインに成功した場合の処理
                print("匿名ユーザーでのログインに成功しました")
            case let .failure(error):
                // ログインに失敗した場合の処理
                print("匿名ユーザーでのログインに失敗しました: \(error)")
        }
})
```

---

## 認証している場合

認証している場合はセッションの有効性チェックを行います。エラーだった場合はログアウトします。

```swift
// セッションの有効性チェック
// 適当なデータストア（今回はTodoクラス）にアクセス
var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
query.limit = 1 // レスポンス件数を最小限にする
// アクセス
query.findInBackground(callback: { results in
    // 結果の判定
    switch results {
    case let .success(obj):
        // 返ってくれば問題なし
        print(obj)
    case let .failure(error):
        // エラーの場合は強制ログアウト
        NCMBUser.logOutInBackground(callback: { result in
            print("強制ログアウト")
        })
    }
})
```

---

## ContentView.swiftの修正

ContentViewにはアプリの実装を行います。

---

## ContentViewの実装内容

以下の画面があります。

- 一覧画面
- タスク追加画面
- タスク編集画面

---

## 一覧画面

一覧画面ではタスクを取得して一覧表示、さらに一覧からタスクの削除を行います。

```swift
// 記述済み
struct ContentView /* 省略 */
```

---

## タスク追加画面

タスク追加画面では新しいタスクの名前を入力します。

```swift
// 記述済み
struct InputView: View {
}
```

---

## タスク編集画面

タスク編集画面では一覧画面で選択されたタスクを表示、編集します。

```swift
// 記述済み
struct EditView: View {
}
```

---

# SDKの読み込み

ContentView.swiftでもNCMB SDKを読み込みます。

```swift
import NCMB
```

---

# ObservableObjectの用意

タスク一覧を管理するObservableObjectを用意します。

```swift
// NCMBObjectを直接設定できないので ObservableObject を経由させます
class Todos: ObservableObject {
    @Published var todos: [NCMBObject] = []
}
```

---

ObservableObjectをContentViewの中で読み込みます。

```swift
struct ContentView: View, InputViewDelegate, EditViewDelegate {
    // ObservedObjectとして定義します
    @ObservedObject var Todo = Todos()
```

---

## 追加画面への遷移

メイン画面で Add ボタンをタップすると追加画面に遷移します。

```swift
// 記述済み
NavigationLink(destination: InputView(delegate: self, text: "")) {
    Text("Add")
        .foregroundColor(Color.white)
        .font(Font.system(size: 20))
}
```

---

## NCMBの追加処理

タスク追加画面で追加ボタンをタップすると、Delegateを通じて ContentViewの `addTodo` が呼び出されます。

```swift
// タスクの追加処理です
func addTodo(text: String) {
    // 新しいタスクオブジェクトを作成します
    let obj = NCMBObject(className: "Todo")
    // 入力された文字列を適用します
    obj["body"] = text
    // ACL（アクセス権限）を作成します
    var acl = NCMBACL.empty
    // 現在のログインユーザを取得します
    let user = NCMBUser.currentUser!
    // ログインユーザに対して読み込み、書き込み権限を付与します
    acl.put(key: user.objectId!, readable: true, writable: true)
    // ついでにAdminグループに対して読み込み、書き込み権限を付与する例です
    acl.put(key: "role:Admin", readable: true, writable: false)
    // ACLを適用します
    obj.acl = acl
    // 保存します
    obj.saveInBackground(callback: { result in
        // 保存結果の判定
        switch result {
            // 保存成功
            case .success(_):
                // 画面に反映させるのでメインスレッドに戻します
                DispatchQueue.main.async {
                  // Todoに新しいタスクを追加
                  self.Todo.todos.append(obj)
                  self.Todo.objectWillChange.send()
                }
            // 保存失敗
            case let .failure(error):
                print("作成に失敗しました: \(error)")
        }
    })
}
```



---

## 一覧画面の作成

ContentViewのZStack.onAppearの中で、画面表示時にTodoクラスからデータを取得し、Todoに対してデータをセットします。

---

以下の中に記述します。

```swift
// 記述済み
.onAppear {
    // 画面表示された際のイベントです
}
```

---

記述内容です。

```swift
// Todoクラス用のクエリを用意します
let query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
// 検索処理を実行します
// 処理は非同期です
query.findInBackground(callback: { result in
    // 結果の判定です
    switch result {
        // 取得できた場合
        case let .success(array):
            // メインスレッドなので main.async を使います
            DispatchQueue.main.async {
                // 結果を適用します
                self.Todo.todos = array
            }
        // 取得失敗した場合
        case let .failure(error):
            print("取得に失敗しました: \(error)")
    }
})
```

---

## 一覧表示

一覧表示は以下のコード内に記述します。

```swift
// 記述済み
List {
    // Todo変数のデータを一覧表示します
}
```

---

記述内容です。タップした際に編集画面に遷移する処理も加えています。

```swift
// Todo変数のデータを一覧表示します
ForEach(self.Todo.todos, id: \.objectId) { todo in
    // todoはNCMBObjectです
    // タスク文字列をタップしたらEditViewを呼び出します
    NavigationLink(destination: EditView(delegate: self, objectId: todo.objectId!, text: (todo["body"] ?? "") as String)) {
        Text((todo["body"] ?? "") as String)
    }
}
// 削除イベントでdeleteを呼び出します
.onDelete(perform: delete)
```

---

## タスクの削除

一覧画面での onDelete イベントで `ContentView.delete` を呼び出しています。

---

記述は次のようになります。

```swift
// タスクの削除処理です
func delete(at offsets: IndexSet) {
    // 処理対象のタスクを取得します
    let todo = self.Todo.todos[Array(offsets)[0]] as NCMBObject
    // 削除を実行します（同期処理にしていますが非同期でも可）
    let result = todo.delete()
    // 処理結果の判定です
    switch result {
        // 成功した場合
        case .success(_):
            // Todoから該当のタスクを削除します
            self.Todo.todos.remove(atOffsets: offsets)
        // 失敗した場合
        case let .failure(error):
            print(error)
    }
}
```

---

## タスク編集処理

タスク編集画面で編集ボタンをタップすると、Delegateを通じて `ContentView.editTodo` が呼び出されます。

---

内容は次のようになります。

```swift
// タスクの編集処理です
func editTodo(text: String, objectId: String) {
    // 処理対象のタスクを特定します
    if let i = self.Todo.todos.firstIndex(where: { $0.objectId == objectId}) {
        let obj = self.Todo.todos[i]
        // 新しい本文を適用
        obj["body"] = text
        // 更新します
        obj.saveInBackground(callback: { result in
            // 更新結果の判定
            switch result {
                // 更新成功
                case .success(_):
                    // 画面に反映させるのでメインスレッドに戻します
                    DispatchQueue.main.async {
                      // Todoを書き換えます
                      self.Todo.todos[i] = obj
                      self.Todo.objectWillChange.send()
                    }
                // 更新失敗
                case let .failure(error):
                    print("更新に失敗しました: \(error)")
            }
        })
    }
}
```

---

## まとめ

今回のハンズオンでは次の3つの機能を利用しました。

- 会員管理
  - 匿名認証
- データストア登録/更新/削除
- データストア検索