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を正しいものに設定し直したので参照できるようになっていると思います。
確認できました。ご対応ありがとうございます!