iOS

SwiftUIで通信中にIndicatorを表示できるようにする

swiftui-loading

SwiftUIで通信中にIndicatorを表示できるようにする

SwiftUIでLoadingの画面を表示したい!

先に現在できているコードを記載します。 また今回のコードのIndicator部分については、tsuzuki817さんの作りながら学ぶ! SwiftUIアニメーション インジケーター編を参考にさせていただきました。なのでIndicatorの実装に関する詳細は省略します。

struct LoadingIndicatorView: View {
    let isLoading: Bool
    @State private var isAnimating = false
    private let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                // ①Loading中に画面をタップできないようにするためのほぼ透明なLayer
                Color(.black)
                    .opacity(0.01)
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .edgesIgnoringSafeArea(.all)
                    .disabled(self.isLoading)
                Circle()
                    .trim(from: 0, to: 0.6)
                    .stroke(AngularGradient(gradient: Gradient(colors: [.gray, .white]), center: .center),
                            style: StrokeStyle(
                                lineWidth: 8,
                                lineCap: .round,
                                dash: [0.1, 16],
                                dashPhase: 8))
                    .frame(width: 48, height: 48)
                    .rotationEffect(.degrees(self.isAnimating ? 360 : 0))
                    // ②アニメーションの実装
                    .onAppear() {
                        withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
                            self.isAnimating = true
                        }
                    }
                    .onDisappear() {
                        self.isAnimating = false
                    }
            }
             // ③Loading中だけLoading画面が表示されるようにする。
            .hidden(!self.isLoading)
        }
    }
}

使う時は、以下のように書きます。これでisLoadingの表示/非表示が切り替わります。

@State private var isLoading = false

var body: some View {
    ZStack {
        Text("Hello world!")
        LoadingIndicatorView(isLoading: self.isLoading)
    }
}

以下のGifの画面では、真ん中の青いボタンをタップすると、3秒間だけisLoadingtrueになるような実装をしてあるため、3秒間だけLoading画面が出るようになっています。
またLoading画面中が表示されている間は、青いボタンはタップできないようになっています。

swiftui-loading-demo

実装詳細

①Loading中に画面をタップできないようにするためのほぼ透明なLayer

Color(.black)
    .opacity(0.01)
    .frame(width: geometry.size.width, height: geometry.size.height)
    .edgesIgnoringSafeArea(.all)
    .disabled(self.isLoading)

なぜ「ほぼ」透明になっているかというと、Color(.clear)にしてしまうと言うとdisabledを指定しても、画面へのジェスチャーが効いてしまうからです。
これに気づくまで結構時間がかかりました。綺麗な書き方も思いつかなかったので、とりあえず黒いLayerを作成し、それに
.opacity(0.01)を設定することで、限りなく透明で、ユーザーのジェスチャーを無効化するLayerを作成することになんとか成功しました。これはSwiftUIのバグではないのか、、、、
ちなみに色を
.clear以外に指定すると、ちゃんとdisabledが効きます。 ②アニメーションの実装 参考資料では以下のように書かれていましたが、これだとLoadingのアニメーションが実行されるたびに、インジェクターが時計回りと反時計回り、交互に回転するようになってしました。

.onAppear() {
    withAnimation(
        Animation
            .linear(duration: 1)) {
                self.isAnimation.toggle()
    }

そこでViewが表示されるonAppear()isAnimationtrueに、非表示になる onDisappear()falseにする実装に変更しました。 これで何度Loading画面を表示しても、ずーと時計回りにIndicatorが回転しています。

.onAppear() {
    withAnimation(self.animation) {
        self.isAnimating = true
    }
}
.onDisappear() {
    self.isAnimating = false
}

③Loading中だけLoading画面が表示されるようにする。 親ViewのisLoadingフラグに合わせて、Loading画面の表示/非表示が切り替わるような実装を行います。 該当箇所は以下の一行です。

.hidden(!self.isLoading)

hiddenは、カスタムのModifierです。詳細の実装については、こちらにまとめてあります。
問題はisLoadingの値をそのままhiddenに流すと、isLoadingtrueの時にLoading画面が非表示になってしまうので、!self.isLoadingとして、Booleanの値を入れ替えてhiddenに渡しています。

まとめ

SwiftUIは1つのファイルにまとめすぎると、かなり読みづらくなる印象があるので、カスタムのViewやModifier、extensionなどを作成して、コードを分割することで、可読性がかなり上がるなと思いました。
まだまだSwiftUIはUIKitと比較するとできることは限られていますが、これからはきっとSwiftUIが主流になっていくと思うので、SwiftUIのキャッチアップも進めていきたいですね。

参考資料

0

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA