SwiftUIでiOS13含めて対応する場合、UIViewControllerのpresent(_:animated:completion:)
で表示するフルスクリーンモーダルに相当する機能が提供されていません。
UIViewControllerRepresentable
を利用してUIKit側にコントロールを渡して実現出来るように思えますが、modalPresentationStyle
の設定が効いていないらしく結果に反映されません。
出来ました、勘違いですみません。。。
このため、フルスクリーンのモーダルを利用したい場合、それに似たようなViewを独自に定義する必要があります。
この投稿では、SwiftUIの機能だけで擬似的にモーダルを表示させていきます。
モーダルを表示させる領域
単純にモーダルのようなViewを表示させるなら、ZStackにViewが被るように表示させればよいだけですが、
この方法を採用した場合、そのモーダルは親Viewの配下にあるという認識をされるため、Viewのライフサイクルが親Viewに依存することになるので上手い方法ではありません。
親とは別のライフサイクルに乗せるためにはoverlay
を利用することになります。
var body: some View {
NavigationView {
}
.overlay(/* Modal */)
}
モーダル本体を作成する
表示領域が決まったので、今度はモーダルのようにアニメーションして表示されるViewを作成します。
struct ModalView<Content: View>: View {
@Binding var isPresented: Bool
let content: () -> Content
var body: some View {
if self.isPresented {
self.content()
// 画面を覆うためにスクリーンサイズをframeに設定
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
// デフォルトの背景色を設定
.background(Color.white.edgesIgnoringSafeArea(.all))
// 大体0.25秒で本家モーダルのようなアニメーションになる
.animation(.easeOut(duration: 0.25))
// 下からせり上がるトランジションを設定
.transition(.move(edge: .bottom))
} else {
EmptyView()
}
}
}
便利に使えるようにViewExtensionを作成する
overlay
に先程作成したModalView
を直接指定しても良いのですが、もう少し使いやすくするためにViewExtensionを作ります。
extension View {
func fullScreenModal<Content: View>(isPresented: Binding<Bool>, content: @escaping () -> Content) -> some View {
self.overlay(ModalView(isPresented: isPresented, content: content))
}
}
完成
実際に使うには、NavigationBarの扱い方などで調整が必要になりますが、ひとまずはこれで完成です。
extension View {
func fullScreenModal<Content: View>(isPresented: Binding<Bool>, content: @escaping () -> Content) -> some View {
self.overlay(ModalView(isPresented: isPresented, content: content))
}
}
struct ModalView<Content: View>: View {
@Binding var isPresented: Bool
let content: () -> Content
var body: some View {
if self.isPresented {
self.content()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.white.edgesIgnoringSafeArea(.all))
.animation(.easeOut(duration: 0.25))
.transition(.move(edge: .bottom))
} else {
EmptyView()
}
}
}
struct MyView: View {
@State var isModalPresented = false
var body: some View {
NavigationView {
VStack {
Button("Show Modal") {
self.isModalPresented = true
}
}
.fullScreenModal(isPresented: self.$isModalPresented) {
Text("Modal")
}
}
}
}
遷移先の画面からもモーダルが閉じれるようにEnvironmentValueなどを提供出来るようにするとより使いやすくなると思います。