SwiftUIとNCMBを組み合わせた時の処理結果通知について

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

mBaaSの処理は基本的にネットワーク処理の伴うものになります。そのため、処理は非同期です。受け取ったデータを画面上に表示したり、次の処理につなげたりする際の方法について解説します。

## 変数を変更する

一番簡単なのは @State で変数を定義しておき、その変数を変更するというものです。たとえばデータストアのデータを一覧する場合を考えます。


```swift
@State var photos: [NCMBObject] = []
var body: some View {
    ScrollView(.vertical) {
        ForEach (photos, id: \.objectId) { photo in
            // リストでの表示処理を記述
        }
    }
    .onAppear() {
        getAllPhotos()
    }
}
```

ここでonAppearを定義しておくことで、画面が表示された際に `getAllPhotos` が呼ばれます。もちろん非同期です。

getAllPhotosではNCMBQueryを使ってデータを取得します。そして、処理が完了したら `photos` に対して結果データを適用します。

```swift
func getAllPhotos() {
    let query = NCMBQuery.getQuery(className: "Photo")
    query.findInBackground(callback: { result in
        switch result {
            case let .success(array):
                self.photos = array
            case let .failure(error):
                print("取得に失敗しました: \(error)")
        }
    })
}
```

こうすればデータストアからデータを取得し、画面描画に反映するまでが自動で行われます。

## 子ビューと組み合わせる場合

子ビューを使う場合はリファレンスを使います。たとえばログイン状態によって表示を変える場合です。

```swift
@State var isLogin: Bool = NCMBUser.currentUser != nil
var body: some View {
  VStack(spacing:0){
    if isLogin {
      // ログインしている場合の表示
    } else {
      LoginView(isLogin: $isLogin)
    }
  }
}
```

こうすることで、LoginView側でisLoginを変更すれば、ログイン後の表示になります。

```swift
struct LoginView: View {
    @State private var userName: String = ""
    @State private var password: String = ""
    @Binding var isLogin: Bool
    
    var body: some View {
        VStack(spacing: 16) {
            TextField("ユーザ名", text: $userName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            SecureField("パスワード", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            Button(action: {
                signUpOrLogin()
            }, label: {
                Text("新規登録/ログイン")
            })
        }
    }

    func signUpOrLogin() {
        let user = NCMBUser()
        user.userName = userName
        user.password = password
        user.signUpInBackground(callback: { _ in
            NCMBUser.logInInBackground(userName: userName, password: password, callback: { _ in
                self.isLogin = NCMBUser.currentUser != nil
            })
        })
    }
}
```

signUpOrLoginの中ではログイン処理が完了した際に isLogin のフラグを立てています。ここで `NCMBUser.currentUser` を渡しても良いのですが、 `NCMBUser.currentUser` を実行すればどこでも取得できる情報ですし、何度も繰り返す必要はないのでログインしているかどうかだけをフラグ管理するようにした方が簡単でしょう。

## ファイルストアのデータダウンロード

ファイルストアにある画像にHTTPSアクセスではなくWeb API経由で取得する場合です。この場合も onAppear の中で画像を読み込みます。そしてデータがダウンロードできたら、imageDataにそのデータを入れます。そしてデータの有無は imageData.count が0より大きいかどうかで判定できます。

```swift
struct GridImageView: View {
    @State private var imageData: Data? = .init(capacity:0)
    @State var memo: NCMBObject? = nil
    @State private var isShowing = false
    
    var body: some View {
        GeometryReader { geometry in
            if imageData?.count ?? 0 > 0 {
                Image(uiImage: UIImage(data: imageData!)!)
                    .resizable()
                    .scaledToFill()
                    .frame(width: geometry.size.width, height: geometry.size.width)
                    .clipped()
            } else {
                Rectangle().fill(Color.clear)
            }
        }.onAppear() {
            loadImage()
        }.onTapGesture {
            isShowing = true
        }.fullScreenCover(isPresented: $isShowing) {
            ModalView(isActive: $isShowing, memo: memo!, imageData: imageData!)
        }
        
    }

    func loadImage() {
        if let fileName : String = self.memo?["fileName"] {
            let file : NCMBFile = NCMBFile(fileName: fileName)
            file.fetchInBackground(callback: { result in
                switch result {
                    case let .success(data):
                        self.imageData = data
                    case let .failure(error):
                        print(error)
                }
            })
        }
    }
}
```

## メインスレッドで処理をする

場合によって、処理がメインスレッドでないために変数の更新でエラーになることがあります。その場合にはメインスレッドへ戻した上で処理してみてください。

```swift
file.fetchInBackground(callback: { result in
    switch result {
        case let .success(data):
            // メインスレッドに戻す
            DispatchQueue.main.async {
              self.imageData = data
            }
        case let .failure(error):
            print(error)
    }
})
```

## Swift 5.5でどうなるか

Swift 5.5では標準でasync/awaitが実装されます。これを使えばネットワーク処理が同期的に書けるようになるので、コールバックの中でうまく書くといった苦労は大幅に減りそうです。

[WWDC 2021のSwiftの並行処理関連のセッション一覧 - Qiita](https://qiita.com/koher/items/8b162940991a8bbff5f2)

## まとめ

非同期処理が絡むと画面表示がうまくいかなくて苦労する場合があるでしょう。特に複数のネットワーク処理が重なる場合にはネストが深くなりがちです。各画面で責任範囲を明確にし、疎結合にすることで処理がすっきりと書けるようになります。ぜひNCMBをお使いの際にはこの方法をお試しください。

[イントロダクション (Swift) : クイックスタート | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/introduction/quickstart_swift.html)