はじめに
iOS15からUISheetPresentationController
が使用できるようになり、簡単にハーフモーダルを実装できるようになりました。
SwiftUIでも使えるようにhalfScreenCover
として実装します。
完成形
fullScreenCover
と全く同じように使えるようにします。
実装
halfScreenCover
extension View {
public func halfScreenCover<T: View>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: @escaping () -> T) -> some View {
return modifier(HalfScreenModifier(isPresented: isPresented, contentT: content))
}
}
HalfScreenModifier
struct HalfScreenModifier<T: View>: ViewModifier {
@Binding private var isPresented: Bool
private let onDismiss: (() -> Void)?
private let contentT: () -> T
init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, contentT: @escaping () -> T) {
self._isPresented = isPresented
self.onDismiss = onDismiss
self.contentT = contentT
}
func body(content: Content) -> some View {
ZStack {
HalfScreenViewController($isPresented) {
contentT()
}
content
}
}
}
HalfScreenViewController
struct HalfScreenViewController<T: View>: UIViewRepresentable {
@Binding private var isPresented: Bool
private let onDismiss: (() -> Void)?
private let content: () -> T
init(_ isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> T) {
self._isPresented = isPresented
self.onDismiss = onDismiss
self.content = content
}
func makeUIView(context: Context) -> UIView {
let view = UIView()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
let viewController = UIViewController()
let hostingController = UIHostingController(rootView: content())
viewController.addChild(hostingController)
viewController.view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.leftAnchor.constraint(equalTo: viewController.view.leftAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: viewController.view.topAnchor).isActive = true
hostingController.view.rightAnchor.constraint(equalTo: viewController.view.rightAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor).isActive = true
hostingController.didMove(toParent: viewController)
if let sheetController = viewController.presentationController as? UISheetPresentationController {
// モーダルのサイズ
sheetController.detents = [.medium()]
// 初期位置の設定
sheetController.selectedDetentIdentifier = .medium
// 背景を暗くする(おそらくタップ範囲の大きさ)
sheetController.largestUndimmedDetentIdentifier = .large
// medium時点でスクロールを可能にするか
sheetController.prefersScrollingExpandsWhenScrolledToEdge = true
// 上部にある短いバーを表示するか
sheetController.prefersGrabberVisible = false
// MARK: iPhone
// prefersEdgeAttachedInCompactHeight
/// true: ❌
/// false: ✅
// widthFollowsPreferredContentSizeWhenEdgeAttached
/// true: ✅
/// false: ✅
// prefersEdgeAttachedInCompactHeight & widthFollowsPreferredContentSizeWhenEdgeAttached
/// true & true: ❌
/// true & false: ❌
/// false & true: ✅
/// false & false: ✅
// MARK: iPad
// prefersEdgeAttachedInCompactHeight
/// true: ✅
/// false: ✅
// widthFollowsPreferredContentSizeWhenEdgeAttached
/// true: ✅
/// false: ✅
// prefersEdgeAttachedInCompactHeight & widthFollowsPreferredContentSizeWhenEdgeAttached
/// true & true: ✅
/// true & false: ✅
/// false & true: ✅
/// false & false: ✅
sheetController.prefersEdgeAttachedInCompactHeight = false
sheetController.widthFollowsPreferredContentSizeWhenEdgeAttached = false
// 角丸サイズ
sheetController.preferredCornerRadius = 20
}
viewController.presentationController?.delegate = context.coordinator
if isPresented {
uiView.window?.rootViewController?.present(viewController, animated: true)
} else {
uiView.window?.rootViewController?.dismiss(animated: true)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(isPresented: $isPresented)
}
class Coordinator: NSObject, UISheetPresentationControllerDelegate {
@Binding private var isPresented: Bool
private let onDismiss: (() -> Void)?
init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil) {
self._isPresented = isPresented
self.onDismiss = onDismiss
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
isPresented = false
if let onDismiss = onDismiss {
onDismiss()
}
}
}
}
注意
prefersEdgeAttachedInCompactHeight
をtrue
にするとiPhoneの横画面でクラッシュします。
使い方
ContentView
import SwiftUI
struct ContentView: View {
@State private var isPresented: Bool = false
var body: some View {
ZStack {
Button("開く") {
isPresented = true
}
}
.halfScreenCover(isPresented: $isPresented, content: {
ZStack(alignment: .center) {
Color(UIColor.yellow).edgesIgnoringSafeArea(.all)
Text("ハーフシート")
}
})
}
}
おわり
ハーフモーダルの高さとか変えられるようになったらいいなぁ