Last active
September 20, 2024 07:50
Present UIKit based sheet as simple as SwiftUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
#Preview { | |
struct SampleView: View { | |
@State var presentsSheet = false | |
var body: some View { | |
Button { presentsSheet.toggle() } label: { Text("Toggle Sheet") } | |
.uiKitSheet( | |
isPresented: $presentsSheet, | |
onDismiss: { print("dismissed!") }, | |
sheetConfiguration: { | |
$0.detents = [.custom(identifier: .medium) { _ in 340.0 }] | |
$0.largestUndimmedDetentIdentifier = .medium | |
$0.prefersGrabberVisible = false | |
$0.preferredCornerRadius = 20 | |
} | |
) { | |
SheetContentView() | |
} | |
} | |
} | |
struct SheetContentView: View { | |
@Environment(\.dismiss) private var dismiss | |
var body: some View { | |
Button { dismiss() } label: { Text("dismiss sheet") } | |
} | |
} | |
return SampleView() | |
} | |
// MARK: - View+UIKitSheet | |
extension View { | |
func uiKitSheet( | |
isPresented: Binding<Bool>, | |
onDismiss: (() -> Void)? = nil, | |
sheetConfiguration: UIKitSheetConfiguration? = nil, | |
@ViewBuilder sheet: () -> some View | |
) -> some View { | |
modifier(UIKitSheetViewModifier( | |
isPresented: isPresented, | |
onDismiss: onDismiss, | |
sheetConfiguration: sheetConfiguration, | |
sheet: sheet | |
)) | |
} | |
} | |
// MARK: UIKitSheetViewModifier | |
private struct UIKitSheetViewModifier<Sheet: View>: ViewModifier { | |
@Binding var isPresented: Bool | |
let sheet: Sheet | |
let onDismiss: (() -> Void)? | |
let sheetConfiguration: UIKitSheetConfiguration? | |
init( | |
isPresented: Binding<Bool>, | |
onDismiss: (() -> Void)?, | |
sheetConfiguration: UIKitSheetConfiguration?, | |
@ViewBuilder sheet: () -> Sheet | |
) { | |
self._isPresented = isPresented | |
self.onDismiss = onDismiss | |
self.sheetConfiguration = sheetConfiguration | |
self.sheet = sheet() | |
} | |
func body(content: Content) -> some View { | |
UIKitSheet( | |
isPresented: $isPresented, | |
onDismiss: onDismiss, | |
sheetConfiguration: sheetConfiguration, | |
content: { content }, | |
sheet: { sheet } | |
) | |
} | |
} | |
// MARK: - UIKitSheetConfiguration | |
typealias UIKitSheetConfiguration = (UISheetPresentationController) -> Void | |
// MARK: UIKitSheet | |
private struct UIKitSheet<Content: View, Sheet: View>: UIViewControllerRepresentable { | |
let content: Content | |
let sheet: Sheet | |
let onDismiss: (() -> Void)? | |
let sheetConfigurationBlock: UIKitSheetConfiguration? | |
@Binding var isPresented: Bool | |
@Environment(\.colorScheme) private var colorScheme | |
init( | |
isPresented: Binding<Bool>, | |
onDismiss: (() -> Void)?, | |
sheetConfiguration: UIKitSheetConfiguration?, | |
@ViewBuilder content: () -> Content, | |
@ViewBuilder sheet: () -> Sheet | |
) { | |
self.content = content() | |
self.sheet = sheet() | |
self.onDismiss = onDismiss | |
self.sheetConfigurationBlock = sheetConfiguration | |
self._isPresented = isPresented | |
} | |
func makeUIViewController(context: Context) -> UIViewController { | |
UIHostingController(rootView: content) | |
} | |
func updateUIViewController(_ viewController: UIViewController, context: Context) { | |
switch (isPresented, viewController.presentedViewController) { | |
case let (true, .some(sheet)): | |
sheet.sheetPresentationController.map(sheetConfigurationBlock ?? { _ in }) | |
case (true, nil): | |
viewController.present( | |
{ | |
let sheetController = DismissableUIHostingViewController(rootView: sheet) | |
sheetController.sheetPresentationController.map(sheetConfigurationBlock ?? { _ in }) | |
sheetController.overrideUserInterfaceStyle = UIUserInterfaceStyle(colorScheme) | |
sheetController.onDismiss = { | |
isPresented = false | |
// NOTE: calls onDismiss manualy when dismissed by gesture | |
onDismiss?() | |
} | |
return sheetController | |
}(), | |
animated: true | |
) | |
case let (false, .some(sheet)): | |
sheet.dismiss(animated: true) | |
default: | |
break | |
} | |
} | |
} | |
// MARK: - DismissableUIHostingViewController | |
private class DismissableUIHostingViewController<Content: View>: UIHostingController<Content> { | |
var onDismiss: (() -> Void)? | |
override func viewDidDisappear(_ animated: Bool) { | |
super.viewDidDisappear(animated) | |
onDismiss?() | |
} | |
} |
Work great on portrait mode, but on landscape it take whole screen and can't swipe off..
@duan-nguyen Thank you for reporting the issue.
But it seems common behavior of UISheetPresentationController when you use default detent. (medium or large)
Setting prefersEdgeAttachedInCompactHeight true and detent size smaller than medium on landscape (in sheetConfiguration block) may help you :)
like below (I've tested it)
.uiKitSheet(
isPresented: $presentsSheet,
onDismiss: { print("dismissed!") },
sheetConfiguration: {
$0.detents = [.custom(identifier: .init("smaller than medium")) { _ in 100 }]
$0.largestUndimmedDetentIdentifier = .init("smaller than medium") // set if you don't want dimmed background
$0.prefersEdgeAttachedInCompactHeight = true
}
) {
SheetContentView()
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Notion (Korean)