SwiftUI で NavigationBar を覆うように View を表示する #Zaim
株式会社 Zaim で iOS エンジニアをしている TEM です。 この記事はくふうカンパニー Advent Calendar 2019 の16日目の記事となります。
皆さん SwiftUI 触っていますか?
私は最近、業務の一貫で少し触っているのですが、短いコードで View を実装できることが便利です。書いていてとても楽しく、もう UIKit で の開発に戻れないような気になっています。
ただ、SwiftUI は UIKit との開発と比べて、簡単にViewを実装できる分、少し混み入ったことをやろうとすると、途端にコードが複雑化してしまうのが難点です(SwiftUI がリリースされたばかりで、あまり情報がないというのもありそうです)。
今回は、従来の UIKit を使った開発では簡単に実装できたUIを、SwiftUI で実装したときに苦労したことについて書きたいと思います。
画面の最前面に View を表示したい
ローディング画面を表示する時など、最前面 にView を表示したいというケースはよくあります。従来の UIKit を使った開発では、単に自身の navigationView を取得し、それに対し addSubView することで簡単に実装できました。
しかし、SwiftUI では UIKit とは違い、単にコードを一行追加すれば簡単にViewを表示できるわけではありません。
どうすれば最前面に表示できるか
[追記] 記事を書いている最中に気づいたのですが、リンク先に良い実装を見つけたので、こちらを参照していただくと幸せになれると思います(この記事もできたら最後まで読んでくださると嬉しいです...)。
SwiftUI: Global Overlay That Can Be Triggered From Any View
当初、Z 軸に View を並べられる ZStack を使えば、簡単に最前面に画面を配置できると考えていました。しかし、View が NavigationView 配下に存在するからなのか、表示しようとした View が NavigationBar の下に表示されてしまいました。
struct ContentView: View {
@State var isLoading = false
var body: some View {
ZStack {
VStack(alignment: .center) {
Button(action: {
self.isLoading = true
}, label: {
Text("Show Loading")
})
Spacer()
}
ZStack {
Color.gray.opacity(0.6)
ActivityIndicator(isAnimating: $isLoading,
style: .large, color: UIColor.white)
}.edgesIgnoringSafeArea(.all)
}
}
}
ZStack 以外にも Overlay を使用したり、NavigationBar の色を動的に変更したりするなどいろいろ試行錯誤したのですが、考えた通りの描画ができませんでした。そこで考え方を変えて、表示中の View 自体を変更するのではなく、遷移元(NavigationView)を子に持つ View を直接、制御してみました。
struct RootView: View {
@State var isLoading = false
var body: some View {
ZStack {
NavigationView {
VStack(alignment: .center) {
Button(action: {
self.isLoading = true
}, label: {
Text("Show Loading")
})
Spacer()
NavigationLink(destination: ContentView()) {
Text("Content")
}
}
}
if isLoading {
ZStack {
Color.gray.opacity(0.6)
ActivityIndicator(isAnimating: $isLoading,
style: .large, color: UIColor.white)
}.edgesIgnoringSafeArea(.all)
}
}
}
}
上手く表示できました!!!
完成形
このままだとトップの画面でしか制御できないので、 EnvironmentObject を使って、配下にある View でもローディングの表示を切り替られるようにしてみます。
import Combine
import SwiftUI
final class GlobalObservableObject: ObservableObject {
@Published var isLoading = false
}
// ※ RootView().environmentObject(GlobalObservableObject())との指定が必要
struct RootView: View {
@EnvironmentObject var globalObservableObject: GlobalObservableObject
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: ContentView()) {
Text("Content")
}
}
if globalObservableObject.isLoading {
ZStack {
Color.gray.opacity(0.6)
ActivityIndicator(isAnimating: $globalObservableObject.isLoading,
style: .large, color: UIColor.white)
}.edgesIgnoringSafeArea(.all)
}
}
}
}
struct ContentView: View {
@EnvironmentObject var globalObservableObject: GlobalObservableObject
var body: some View {
ZStack {
VStack(alignment: .center) {
Button(action: {
self.globalObservableObject.isLoading = true
}, label: {
Text("Show Loading")
})
Spacer()
}
}
}
}
インジケーター
import SwiftUI
import UIKit
struct ActivityIndicator: UIViewRepresentable {
@Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
let color: UIColor
func makeUIView(context _: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
let indigatorView = UIActivityIndicatorView(style: style)
indigatorView.color = color
return indigatorView
}
func updateUIView(_ uiView: UIActivityIndicatorView, context _: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
さいごに
SwiftUI はリリースされたばかりで、ちょっとしたことも上手く実装できず難しいですが、新鮮で楽しいですね。
今後も悩んだことがあれば共有したいと思います。
Zaim では、エンジニアを募集しています。ぜひ話を聞きに来てください!