スクリーンショット_2019-12-16_13

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 の下に表示されてしまいました。

画像1

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)
           }
       }
   }
}


上手く表示できました!!!

画像2

完成形

このままだとトップの画面でしか制御できないので、 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 では、エンジニアを募集しています。ぜひ話を聞きに来てください!


この記事が気に入ったらサポートをしてみませんか?