はじめに
SwiftUIで開発中のアプリが、sheetの中でAPIを叩く処理があり、通信中はローディング画面を表示するという仕様だったのですが、
調べてみても、大抵はZStackを使ってローディング画面を重ねていたので、sheetのような画面では、画面全体を覆うことができず、かなり悩んだ末とりあえず実装できたので参考になればと思い記事にしました。
注意点
私が開発しているアプリではローディング画面を表示・非表示するタイミングで、sheetでのアニメーションやメソッドが効かないことがあり、現在も改善できないかと悩んでいます。
もしかすると、この記事を参考にしても、仕様どおりの実装が出来ないかもしれません。
もし原因や対処法などがわかる方がいれば、教えていただけると助かります。
実装要領
実装の大まかなポイントは
1.fullScreenCoverを透明にする
2.Sheetの中でfullScreenCoverを呼び出す
3.fullScreenCoverでローディング画面を表示する
4.fullScreenCoverの表示・非表示のアニメーションを無くす
です。
View
以下は簡易的な画面のコードです。
import SwiftUI
struct ContentView: View {
@State private var isOpen = false
var body: some View {
ZStack{
Color.orange
VStack{
Spacer()
Text("MainView")
Spacer()
Button("Sheetを開く") { self.showingSheet.toggle() }
.sheet(isPresented: $isOpen) {
SheetView(isOpen: $showingSheet)
}
Spacer()
}
}.ignoresSafeArea(.all)
}
}
struct SheetView: View {
@Binding var isOpen: Bool
@ObservedObject var executionAPI: ExecutionAPI = ExecutionAPI()
var body: some View {
ZStack{
Color.green
VStack{
Spacer()
Text("SheetView")
Spacer()
Button("Sheetviewを閉じる"){
isOpen = false
}
Spacer()
Button("通信開始"){
executionAPI.startAPI()
}
.fullScreenCover(isPresented: $executionAPI.isLoading){
Loading()
// 自作した背景を透明にするモディファイア
.clearSheet()
}
Spacer()
}
}.ignoresSafeArea(.all)
}
}
#Preview {
ContentView()
}
以下はローディング画面のコードです。
import SwiftUI
struct Loading: View {
var body: some View {
ZStack{
Color.black.opacity(0.5)
.ignoresSafeArea()
ProgressView()
}.ignoresSafeArea(.all)
}
}
以下は、fullScreenCoverを透明にするコードです。
import SwiftUI
struct BackgroundClearView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let view = UIView()
Task {
view.superview?.superview?.backgroundColor = .clear
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
extension View {
func clearSheet() -> some View {
background(BackgroundClearView())
}
}
ViewModel
APIなどを実行するクラスのコードです。
import SwiftUI
class ExecutionAPI: ObservableObject {
@Published var isLoading: Bool = false
private var transaction = Transaction()
func startAPI () {
// この記載でアニメーションを消しています
self.transaction.disablesAnimations = true
withTransaction(self.transaction) {
self.isLoading = true
}
DispatchQueue.global().async {
// API実行処理など
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
print("3秒経ちました。")
// この記載でアニメーションを消しています
withTransaction(self.transaction) {
self.isLoading = false
}
}
}
}
}
最後に
以下はこの実装で出た不具合を解決した方法です。
・API通信などが失敗した時は、アラートを出す仕様
ローディング画面でアラートを表示し、アラートのボタンを押すことで、アラートとローディングを消すというやり方で実装しました。
・API通信が成功したときは、sheetを閉じる仕様
sheetの中でonChangeを使って、API通信が成功後、画面側の値を書き換えることで実装できました。
もしかしたらViewで定義していたisOpenをViewModel側で定義しても実装できるかもしれません。
他にも、ちょこちょこ不具合が発生しているので、頭を抱えています。
UIKitでローディングを作るのは簡単のようですが、SwiftUIは画面の前後の扱いが弱いのかなと感じています。
SwiftUIでローディング画面を簡単に実装できる方法があれば、コメントだったり、記事にしていただけると助かります。