NCMBのSwift SDKを使って掲示板アプリを作る（その1：Viewの説明とSwift SDKの初期化）

ニフクラ mobile backend（NCMB）では各種言語向けにSDKを提供しています。その中で最も新しいSwift SDKについて、その使い方を紹介します。

今回から複数回に分けて、掲示板アプリを作ってみます。Swift SDKの使い方はもちろん、NCMBの基本的な利用法を学ぶのに役立ててください。

なお、今回のコードは[NCMBMania/Swift_Forum_Demo: Swift SDKで作る掲示板アプリのデモコードです](https://github.com/NCMBMania/Swift_Forum_Demo)にアップしてあります。

## 掲示板アプリの機能について

掲示板アプリは一般的なスレッド、コメント形式の掲示板となっています。認証は匿名認証を使っており、自分で立てたスレッドやコメントは削除できます。スレッドにはアイコンとして画像をアップロードできます。

## 利用する機能について

掲示板アプリで利用するNCMBの機能は次の通りです。

- 認証機能
  - 匿名認証
- データストア
  - スレッドクラスへのデータ登録、一覧表示、削除
  - コメントクラスへのデータ登録、一覧表示、削除
- ファイルストア
  - スレッドクラスに紐付いた写真のアップロード
  - 写真のダウンロード

## 画面について

掲示板アプリでは次の画面（View）を用意しています。

- 全般
  - ImagePickerView  
  画像を撮影、またはフォトライブラリからピックアップする用
  - ImageView  
  画像を表示するためのView
- スレッド用
  - ThreadListView  
  スレッド一覧用
  - ThreadListRow  
  スレッド一覧のリスト表示部分用
  - ThreadView  
  スレッド詳細用
  - AddThreadView  
  スレッド追加用
- コメント用
  - CommentListRow  
  スレッド詳細でのコメントリスト表示部分用
  - AddCommentView  
  コメント投稿用

### ContentView

スレッド一覧用のViewを読み込んでいるだけです。

```swift
struct ContentView: View {
    var body: some View {
        ZStack {
            ThreadListView()
        }
    }
}
```

### ThreadListView

![simulator_screenshot_AC78BBD8-519D-439D-94B4-975297AB23BE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/7fac59eb-41ca-f4c9-27b3-4038ef5ea956.png)


スレッドのリスト表示と、ナビゲーションバーにスレッドを新規作成させるモーダルを表示させるプラスアイコンを表示しています。スレッド一覧をタップすると `ThreadView` に遷移します。

```swift
struct ThreadListView: View {
    @State var threads: [NCMBObject] = []
    @State private var showModal = false
    @State private var showAlert = false
    
    var body: some View {
        NavigationView {
            ZStack(alignment: .bottomTrailing) {
                List {
                    ForEach(self.threads, id: \.objectId) { thread in
                        NavigationLink(
                            destination: ThreadView(thread: thread)
                        ) {
                            ThreadListRow(thread: thread)
                        }
                    }
                    .onDelete(perform: delete)
                }
                .navigationBarTitle("掲示板", displayMode: .inline)
                .navigationBarItems(trailing:
                                        Button(action: {
                                            showModal.toggle()
                                        }, label: {
                                            Image(systemName: "plus")
                                                .resizable()
                                                .padding(6)
                                                .frame(width: 24, height: 24)
                                                .foregroundColor(.blue)
                                        })
                )
            }
        }
        .onAppear {
            getThread()
        }
        .sheet(isPresented: $showModal, content: {
            AddThreadView()
        })
        .onChange(of: showModal, perform: { value in
            if (!showModal) {
                getThread()
            }
        })
        .alert(isPresented: $showAlert, content: {
            Alert(title: Text("削除に失敗しました。権限がないようです。"))
        })
    }
}
```

### ThreadListRow

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/dca50384-89a1-74ba-3869-b8e3b8e3400e.png)


スレッドの一覧表示用です。ImageViewを使って指定した画像を表示します。

```swift
struct ThreadListRow: View {
    var thread: NCMBObject
    var body: some View {
        HStack {
            ImageView(fileName: (thread["fileName"] ?? "") as String, size: 50)
            VStack {
                Text((thread["title"] ?? "") as String)
                    .font(.title3)
                Text((thread["description"] ?? "") as String)
                    .font(.caption)
                    .foregroundColor(.gray)
            }
            Spacer()
        }
    }
}
```

### ImageView

指定された画像をファイルストアからダウンロードして、表示します。パラメータとして、表示する大きさを指定できます。

```swift
struct ImageView: View {
    var fileName: String?
    var size: CGFloat
    var body: some View {
        if let image = image() {
            Image(uiImage: image)
                .resizable()
                .frame(width: size, height: size)
        } else {
            Image(systemName: "bubble.left.and.bubble.right.fill")
                .resizable()
                .frame(width: size, height: size)
                .foregroundColor(.blue)
        }
    }
}
```

### AddThreadView

![simulator_screenshot_EF3F865E-86E5-4331-8CB4-F2F4FBB1C66B.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/98314441-a739-2514-3ac6-18db202fe130.png)


スレッド追加用のモーダルです。画像追加をタップすると、フォトライブラリから写真を選択する表示になります。

```swift
struct AddThreadView: View {
    @Environment(\.presentationMode) private var presentationMode
    @State private var title = ""
    @State private var description = ""
    @State var showingPicker = false
    @State var image: UIImage?
    var body: some View {
        VStack {

            NavigationView {
                Form {
                    TextField("スレッドのタイトルを入力してください", text: $title)
                    TextField("スレッドの紹介文を入力してください", text: $description)
                    Button(action: {
                        showingPicker.toggle()
                    }) {
                        Text("画像追加")
                    }
                    if let image = image {
                        Image(uiImage: image)
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                    }
                    Button(action: {
                        add()
                    }) {
                        Text("スレッド追加")
                    }
                }
            }
        }
        .sheet(isPresented: $showingPicker) {
            ImagePickerView(image: $image, sourceType: .library)
        }
    }
}
```

### ThreadView

![simulator_screenshot_10BB3F68-BCC1-4405-BD22-6E1820A9846D.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8b6cdd7f-f373-17c5-ae71-1a845be671a6.png)


ThreadViewはスレッド詳細とコメント一覧を表示します。スレッド情報を前画面から受け取ります。

```swift
struct ThreadView: View {
    var thread: NCMBObject
    @State private var showModal = false
    @State private var comments: [NCMBObject] = []
    @State private var showAlert = false
    
    var body: some View {
        VStack {
            ImageView(fileName: (thread["fileName"] ?? "") as String, size: 200)
            Text("コメント").font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
            List {
                ForEach(self.comments, id: \.objectId) { comment in
                    CommentListRow(comment: comment)
                }
                .onDelete(perform: delete)
            }
            .navigationBarTitle((thread["title"] ?? ""), displayMode: .inline)
            .navigationBarItems(trailing:
                                    Button(action: {
                                        showModal.toggle()
                                    }, label: {
                                        Image(systemName: "bubble.middle.bottom.fill")
                                            .resizable()
                                            .padding(6)
                                            .frame(width: 24, height: 24)
                                            .foregroundColor(.blue)
                                    })
            )
        }
        .onAppear {
            getComments()
        }
        .sheet(isPresented: $showModal, content: {
            AddCommentView(thread: thread)
        })
        .onChange(of: showModal, perform: { value in
            if (!showModal) {
                getComments()
            }
        })
        .alert(isPresented: $showAlert, content: {
            Alert(title: Text("削除に失敗しました。権限がないようです。"))
        })
    }
}
```

### CommentListRow

コメントの一行表示用のViewです。

```swift
struct CommentListRow: View {
    var comment: NCMBObject
    var body: some View {
        HStack {
            Text((comment["text"] ?? "") as String)
        }
    }
}
```

### AddCommentView

![simulator_screenshot_2AA69A41-F390-43ED-BF4E-C9C0A3EDB0FC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/15c90a07-2826-2bf2-2ec3-2b467eef4fa9.png)


コメントを追加する際のモーダルを表示します。

```swift
struct AddCommentView: View {
    @Environment(\.presentationMode) private var presentationMode
    
    var thread: NCMBObject
    @State private var text = ""
    
    var body: some View {
        VStack {
            NavigationView {
                        Form {
                            TextField("コメントを入力してください", text: $text)
                            Button(action: {
                                add()
                            }) {
                                Text("コメントする")
                            }
                        }
                    }
        }
    }
}
```

## 今回のプロジェクト

今回は言語がSwift、インタフェースがSwiftUI、ライフサイクルはSwiftUI Appとしています。

## SDKのインストール

FileメニューからSwift Packages > Add Package Dependencyと選択します。

![ScreenShot_ 2021-08-02 9.35.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/dea9dcdc-3368-7992-67d4-8375355bf2ff.png)

出てきたダイアログでSwift SDKのGitリポジトリURLを入力します。GitHubのリポジトリでHTTPSとして取得できるもの、または下記URLになります。

![ScreenShot_ 2021-08-02 9.35.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/8d5d62cd-cf3f-f535-8f71-f1e61569674a.png)

```
https://github.com/NIFCLOUD-mbaas/ncmb_swift.git
```

バージョンは最新のものでかまいません。

![ScreenShot_ 2021-08-02 9.36.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/9e873825-38be-97e2-edd3-44776cc72d22.png)

後はFinishボタンを押せば完了です。

![ScreenShot_ 2021-08-02 9.36.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/7f74527e-8de8-d5cf-b433-c16b538c24f1.png)

## 初期化

今回はSwiftUIを利用しています。ライフサイクルもSwiftUIです。

まずSDKをインポートします。

```swift
import SwiftUI
import NCMB
```

次に `scenePhase` を追加します。

```swift
@main
struct forumApp: App {
    // 追加
    @Environment(\.scenePhase) private var scenePhase
```

後は body 内で `onChange` を使って初期化します。

```swift
var body: some Scene {
    WindowGroup {
        ContentView()
    }.onChange(of: scenePhase) { scene in
        switch scene {
        case .active:
            // キーの設定
            let applicationKey = "YOUR_APPLICATION_KEY"
            let clientKey = "YOUR_CLIENT_KEY"
            // 初期化
            NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey)
        case .background: break
        case .inactive: break
        default: break
        }
    }
}
```

## 匿名認証

今回は匿名認証（ID、パスワードを使わない、デバイス固有に生成したUUIDを使った認証）を利用します。そこで、初期化した後にSwift SDKの匿名認証を有効にします。

```swift
NCMBUser.enableAutomaticUser()
```

### 認証状態の確認

次に認証状態を確認する `checkAuth` 関数を呼び出します。内容は次の通りです。 `NCMBUser.currentUser` が nil の場合は認証されていないので、匿名認証を実行します。

```swift
func checkAuth() -> Void {
    // 認証データがあれば処理は終了
    if NCMBUser.currentUser != nil {
        return;
    }
    // 匿名認証実行
    _ = NCMBUser.automaticCurrentUser()
}
```

### セッションの有効性チェック

認証されている場合でも、セッションの有効性は確認していません。ローカルにある認証データを復元している状態のためです。そこで、データストアに一度アクセスを行い、API通信の有効性を確認します。

```swift
func checkSession() -> Bool {
    var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
    query.limit = 1 // レスポンス件数を最小限にする
    // アクセス
    let results = query.find()
    // 結果の判定
    switch results {
    case .success(_): break
    case .failure(_):
        // 強制ログアウト処理
        _ = NCMBUser.logOut()
        return false
    }
    return true
}
```

上記コードでログアウト処理を実行していますが、これは必ず失敗します。セッションが無効になっているため、ログアウトAPIの実行もまた、失敗するためです。そこで `NCMB/NCMBUser.swift` を開いて `logOutInBackground` を次のように修正します。

```swift
public class func logOutInBackground(callback: @escaping NCMBHandler<Void>) -> Void {
    NCMBLogoutService().logOut(callback: {(result: NCMBResult<NCMBResponse>) -> Void in
        // レスポンスに関係なくログアウト処理
        deleteFile()
        _currentUser = nil
        switch result {
            case .success(_):
                callback(NCMBResult<Void>.success(()))
                break
            case let .failure(error):
                callback(NCMBResult<Void>.failure(error))
                break
        }
    })
}
```

### NCMBの管理画面を修正

最後にNCMBの管理画面でアプリ設定を開き、匿名認証を有効にします。

![FireShot_Capture_219_-_ニフクラ_mobile_backend_-_console_mbaas_nifcloud_com_15-23-35.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/36d4c49f-5d86-56c4-671a-5a25c0115fcd.jpeg)


これでSwift SDKの初期化と匿名認証処理が完了になります。

## まとめ

今回は掲示板アプリの画面に関する説明と、Swift SDKの初期化、そして匿名認証の実装を行いました。次回はスレッドの作成と、一覧表示、削除を実装します。