SwiftUIでハーフモーダルを表示してみる(iOS15~)
概要
iOS15からUIKitにUISheetPresentationControllerというクラスが追加されますが、SwiftUIにはそのようなAPIは追加されませんでした。
今回はUISheetPresentationController
を使用しハーフモーダルを実現し、それをSwiftUIのコードで使用できるようにしていきます。
コードの全容はgithubにアップしているので良ければ参考にしてみてください。
完成イメージ
以下のようにボタンをタップした際、ハーフモーダルが表示されるような形を目指します。
今回はハーフシート内のViewはSwiftUIで書くことができるようにしていきます。
つまりハーフシート部分のみUIViewControllerRepresentable
で表現していきます。
実装
インターフェース定義
UIViewControllerRepresentable
で実装したハーフシート上に、SwiftUIでViewを定義できるようにするため、Viewのextensionに以下を追加します。
extension View {
func halfModal<Sheet: View>(
isShow: Binding<Bool>,
@ViewBuilder sheet: @escaping () -> Sheet,
onEnd: @escaping () -> ()
) -> some View {
return self
.background(
HalfModalSheetViewController(
sheet: sheet(),
isShow: isShow,
onClose: onEnd
)
)
}
}
引数について少し説明します。
引数名 | 内容 |
---|---|
isShow | ハーフモーダルの表示状態を持ちます。Binding としているのは、View とUIViewControllerRepresentable の両方から表示状態を操作したいためです。 |
sheet |
View でハーフモーダル上に表示するUIを描画するためのクロージャです。@ViewBuilder をつけているのは複数のView で構成されるのを想定するためです。 |
onEnd | ハーフモーダルが閉じたことを通知するイベントリスナーです。 |
このインターフェースを定義することで、以下のように扱うことを想定しています。
@State var isShowHalfModal = false
// 省略...
ZStack {
Button("Show half modal") {
isShowHalfModal.toggle()
}
}
.halfModal(isShow: $isShowHalfModal) {
// ここにハーフモーダルシートに表示したいViewを定義する
Text("Test")
} onEnd: {
print("Dismiss half modal")
}
ハーフモーダルシートを実装
後はUISheetPresentationController
を使用してハーフモーダルを表示するだけです。
まずUIViewControllerRepresentable
を使用してハーフモーダルのViewを実装します。
struct HalfModalSheetViewController<Sheet: View>: UIViewControllerRepresentable {
var sheet: Sheet
@Binding var isShow: Bool
var onClose: () -> Void
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ viewController: UIViewController,
context: Context
) {
if isShow {
let sheetController = CustomHostingController(rootView: sheet)
sheetController.presentationController!.delegate = context.coordinator
viewController.present(sheetController, animated: true)
} else {
viewController.dismiss(animated: true) { onClose() }
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UISheetPresentationControllerDelegate {
var parent: HalfModalSheetViewController
init(parent: HalfModalSheetViewController) {
self.parent = parent
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
parent.isShow = false
}
}
class CustomHostingController<Content: View>: UIHostingController<Content> {
override func viewDidLoad() {
super.viewDidLoad()
if let sheet = self.sheetPresentationController {
sheet.detents = [.medium(),]
sheet.prefersGrabberVisible = true
}
}
}
}
present
の引数にCustomHostingController
というUIHostingController
を継承したクラスのオブジェクトを渡しているのは、以下2つの理由のためです。
-
present
の引数はUIViewController
でなければならないため - SwiftUIで定義した
View
をモーダルシートに描画したいため
2について、UIHostingController
のイニシャライザは以下のような定義であり、
init(rootView: Content)
Content
はView
の制約があり且つUIKit
の世界からSwiftUI
の世界にView
を渡すことができるためです。
モーダルシートを表示
あとは以下の様な感じでモーダルシートを表示するだけです。
import SwiftUI
struct ContentView: View {
@State var isShowHalfModal = false
var body: some View {
Button("show half modal") {
isShowHalfModal.toggle()
}
.halfModal(isShow: $isShowHalfModal) {
VStack {
Text("Shown half modal!")
.font(.title.bold())
.foregroundColor(.black)
Button("Close") {
isShowHalfModal.toggle()
}
}
} onEnd: {
print("Dismiss half modal")
}
}
}
まとめ
今まではサードパーティー製のライブラリを使うなどしてハーフモーダルを実現していましたが、iOS15からはサポートしてくれるので嬉しいです。
Discussion
ちょうどswiftuiを勉強し始めて、詰まっていたのでとても参考になりました!
ページトップのgithubリンクが切れているようなのですが、リポジトリは削除されてしまったのでしょうか。
連絡ありがとうございます!ディレクトリ構成の変更に引きずられてURLも変わっていました。
URLを正しいものに設定し直したので参照できるようになっていると思います。
確認できました。ご対応ありがとうございます!