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秒間だけisLoading
がtrue
になるような実装をしてあるため、3秒間だけLoading画面が出るようになっています。
またLoading画面中が表示されている間は、青いボタンはタップできないようになっています。
実装詳細
①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()
でisAnimation
をtrue
に、非表示になる 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
に流すと、isLoading
がtrue
の時にLoading画面が非表示になってしまうので、!self.isLoading
として、Booleanの値を入れ替えてhidden
に渡しています。
まとめ
SwiftUIは1つのファイルにまとめすぎると、かなり読みづらくなる印象があるので、カスタムのViewやModifier、extensionなどを作成して、コードを分割することで、可読性がかなり上がるなと思いました。
まだまだSwiftUIはUIKitと比較するとできることは限られていますが、これからはきっとSwiftUIが主流になっていくと思うので、SwiftUIのキャッチアップも進めていきたいですね。