SwiftUIでもUIActivityIndicatorViewを使いたい

Swift
SwiftUIでもUIActivityIndicatorViewを使いたい

はじめに

WWDC始まってますね!毎年の楽しみです。睡眠不足が身体に堪えるので、徹夜は控えています。なので、一足遅れて情報が入ってくる状態です。早くキャッチアップして遅れを取り戻していきたいですね。

さて、もしかしたらすでにSwftUIに用意されているかもしれないですが、UIActivityIndicatorViewをSwiftUIでも使えるようにしてみたいと思います。

「小並感」に比べて、より一層実用的です。

UIActivityIndicatorViewをSwiftUI用にラップする

UIViewRepresentableに準拠した構造体を作る

struct ActivityIndicator:UIViewRepresentable {
}

できました。エラーが出ると思いますが安心してください。これから作っていきます。

ビューで保持する必要があるプロパティを用意する

さっき作った構造体にビューで保持する必要があるプロパティを用意していきます。
UIActivityIndicatorViewが持つプロパティは以下の通り。

  • isAnimating(ぐるぐるしているかどうかを表す)
  • hidesWhenStopped(ぐるぐるが止まったときにインジケータを隠すかどうかを指定する)
  • style(インジケータの大きさを指定する)
  • color(インジケータの色を指定する)

このうち、isAnimatingはSwiftUIとUIKitの間で値を共有するために使おうと思いますので、@Binding属性を付けておきます。これは、本家UIKitでの使い方とちょっと違います。
UIActivityIndicatorViewを動かすか止めるかは、startAnimating()メソッドとstopAnimating()メソッドで行いますが、SwiftUIではビュー間でそれらのメソッドを呼び出せません。なので、プロパティを介して動かすか止めるかを制御することにしました。

これを踏まえて、構造体に追記していきます。

struct ActivityIndicator:UIViewRepresentable {
  @Binding var isAnimating: Bool
  public var hidesWhenStopped = false
  public var style = UIActivityIndicatorView.Style.medium
  public var color = UIColor.gray
}

名前をUIKit側と合わせておきました。また、適宜初期値を与えておきました。
まだXCode上ではエラーが出ていて落ち着かないですが、もう少し我慢してください。

UIKitのビューをSwiftUI向けに提供するmakeUIViewを作る

UIKitのビューをSwiftUI向けに生成するメソッドを作ってみます。これは構造体をUIViewRepresentableに準拠させるために必要なものです。

このメソッドは直接呼ぶのではなく、イニシャライザを呼び出したときに裏で勝手に呼ばれます。

struct ActivityIndicator:UIViewRepresentable {
    @Binding var isAnimating: Bool
    public var hidesWhenStopped = false
    public var style = UIActivityIndicatorView.Style.medium
    public var color = UIColor.gray
    func makeUIView(context: Context) -> UIActivityIndicatorView {
        let indicator = UIActivityIndicatorView(style: self.style)
        indicator.color = self.color
        indicator.hidesWhenStopped = self.hidesWhenStopped
        return indicator
    }
}

各プロパティを与えて初期化しています。makeUIViewメソッドにより、内々でUIKitのビューのインスタンスを生成しています。

SwiftUIでの変化をUIKitのビューに伝えるupdateUIViewを作る

仕上げです。
@Binding属性が付けられたプロパティisAnimatingの値が変化するたびに呼び出されるupdateUIViewメソッドを作っていきます。

struct ActivityIndicator:UIViewRepresentable {
    @Binding var isAnimating: Bool
    public var hidesWhenStopped = false
    public var style = UIActivityIndicatorView.Style.medium
    public var color = UIColor.gray
    func makeUIView(context: Context) -> UIActivityIndicatorView {
        let indicator = UIActivityIndicatorView(style: self.style)
        indicator.color = self.color
        indicator.hidesWhenStopped = self.hidesWhenStopped
        return indicator
    }
    func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
        if isAnimating {
            uiView.startAnimating()
        }
        else {
            uiView.stopAnimating()
        }
    }
}

使ってみる

うまく動くか試してみます。適当にドドドッと。

VStack {
    Text("Hello, World!")
    Toggle(isOn: $isAnimating) {
        Text("isAnimating")
    }
    ActivityIndicator(isAnimating: .constant(false))
    ActivityIndicator(isAnimating: .constant(true))
    ActivityIndicator(isAnimating: $isAnimating, hidesWhenStopped: true, style: .medium, color: .orange)
    ActivityIndicator(isAnimating: $isAnimating, hidesWhenStopped: true, style: .large, color: .purple)
    ActivityIndicator(isAnimating: $isAnimating, hidesWhenStopped: false, style: .medium, color: .red)
    ActivityIndicator(isAnimating: $isAnimating, hidesWhenStopped: false, style: .large, color: .blue)
}
ぐるぐるストップした時
ぐるぐるスタートした時

ちゃんとインジケータのプロパティも効いています。画像は止まっていますが、ちゃんと回っていました。

まとめ

これでSwiftUIでもカラフルなインジケータを表示させることができるようになりました。これを使えば重たい処理の間にぐるぐる三昧!同様の手順で、他のUIKitもラップしてSwiftUIで使えるようになると思います。SwiftUIを使う前から作りためていたものをどんどんラップしてSwiftUIでも使ってみましょう。

ただし、ここで示した手順はUIKit側からSwiftUI側へイベントなどを伝える必要がないパターンなのでご注意を!