非同期通信中にActivityIndicatorを表示させてみる

Swift
非同期通信中にActivityIndicatorを表示させてみる

はじめに

UIKitで提供されているUIのUIActivityIndicatorViewをSwiftUIで使えるようにラップしました。せっかく作ったので、使ってみましょう。

目標

これまで散々弄り倒してきたMongoDBからドキュメントを操作するアプリを使います。
現状は、リクエストを出して結果が返ってくるまでの間、非同期通信を行っていますが、バックグラウンド処理なので見かけ上何も起こっていないように見えます。

ドキュメントの量や通信環境が良好ならば、結果が返ってくるまで時間がかからないと思います。しかしながら、状況によっては結果が返ってくるまで時間がかかるかもしれません。そのときにはちゃんと作業中であることの意思表示は必要です。返ってきた情報を使って後続の処理が行われるときは、返ってくるまで待たなくては行けないですし。

ですので、せめて作業中であることの意思表示として、UIActivityIndicatorViewをラップしたActivityIndicatorを表示させます。代表でGETリクエストを出してドキュメントを全件取得するメソッドで示します。

元の状態

通信処理側

class Access2Mongo:NSObject {
    func get() {
        let url: URL = URL(string: [接続先アドレス] + "/api/books/")!
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        let task:URLSessionTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let dataString = data {
                print("data:" + (String(data: dataString, encoding: .utf8) ?? "nil"))
            }
            print("response: \(String(describing: response))")
            print("error: \(String(describing: error))")
        }
        task.resume()
    }
}

ネーミングセンスは相変わらずです。

UI側

struct ContentView: View {
  var access2Mongo:Access2Mongo = Access2Mongo()
  var body: some View {
    VStack {
      Button(action: {
        self.access2Mongo.get()
      }) {
        Text("Get Document")
      }
    }
  }
}

方針

UI側では状態を保持せず、通信処理側から通知を出すことでUIの制御をしていく方針で進めます。

通信処理側

ObservableObjectプロトコルに準拠させる

データを保持する側のクラスをObservableObjectプロトコルに準拠させます。準拠させると言っても、ここでは保持しているデータが変わる都度自動的に通知するだけなので、特にメソッドを実装したりする必要はありません。

通知対象のプロパティに@Publised属性をつける

通知したいデータを保持しているプロパティには@Published属性を付けておきます。これを付けておくことで、プロパティの値が変わる都度、SwiftUIに対して自動的に通知を出してくれます。

今回は、作業中であるかどうかを示すものと、リクエストを送って得られた結果を保持するものの2つを用意したいと思います。

プロパティに値を代入する

用意したプロパティに値を入れていきます。値を入れないと、せっかく通知対象にしたのに何も起こらずしょんぼりします。

処理開始時点と終了時点、返ってきた結果を代入します。
なお、終了時点のプロパティ設定時は、メインスレッドで行ってください。

最終的なコード

class Access2Mongo:NSObject {
  @Published var data:Data = Data()
  @Published var isProgress:Bool = false
  func get() {
    self.isProgress = true
    let url: URL = URL(string: [接続先アドレス] + "/api/books/")!
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    let task:URLSessionTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
      if let dataString = data {
        self.data = dataString
        print("data:" + (String(data: dataString, encoding: .utf8) ?? "nil"))
      }
      print("response: \(String(describing: response))")
      print("error: \(String(describing: error))")
      DispatchQueue.main.async {
          self.isProgress = false
      }
    }
    task.resume()
  }
}

UI側

通知が送られてくるクラスに@ObservedObject属性を付ける

さてお次は通知を受け取る側です。通知を送ってくるクラスのインスタンスに@ObservedObject属性を付けて、プロパティの変更を受け取ることができるようにします。

プロパティの値を利用する

普通にプロパティの値を参照しておくだけです。これだけでプロパティの値が変わると自動的にUIが更新されます。

ActivityIndicatorisAnimatingに非同期通信の開始と終了を通知し、リクエストの結果を受け取ったら表示するようにしておきます。

最終的なコード

var body: some View {
  @ObservedObject var access2Mongo:Access2Mongo = Access2Mongo()
  VStack {
    ActivityIndicator(isAnimating: $access2Mongo.isProgress, hidesWhenStopped: true, style: .large, color: .purple)
    Button(action: {
      self.access2Mongo.get()
    }) {
    Text("Get Document")
    Text("\(access2Mongo.data)")
  }
}

動かしてみる

取得前の状態
取得した後の状態

肝心のぐるぐるは、はぐれメタル級の素早さで消えたため、撮れませんでした。
リクエスト結果をそのままDataそのままを持ってきたためただのダンプになっていますが、ご了承ください。

まとめ

無事非同期通信にぐるぐる(ActivityIndicator)を表示させることができました。

状態保持とデータのフローはちょっとややこしかったです。Combineフレームワークはきっちりと押さえておく必要がありそうです。

余談

はぐれメタルのようなぐるぐるを捉えんがため、sleepメソッドで少し余裕をもたせることにしてみました。結果としてぐるぐるを捉えることができたのですが、引数がミリ秒だと思い込んでいて2000とか入れてしまい、消えないぐるぐるをしばらく眺めていたのは内緒です。引数がミリ秒なのはusleepメソッドでしたね。