Skip to content

Instantly share code, notes, and snippets.

@wotjd
Last active February 25, 2023 13:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wotjd/e2c2ae6da63800315e25e52df76a29cf to your computer and use it in GitHub Desktop.
Save wotjd/e2c2ae6da63800315e25e52df76a29cf to your computer and use it in GitHub Desktop.
Present UIKit based sheet as simple as SwiftUI
import SwiftUI
struct SampleView: View {
@State 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") }
}
}
import SwiftUI
// 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?()
}
}
@wotjd
Copy link
Author

wotjd commented Jan 7, 2023

Notion (Korean)

@duan-nguyen
Copy link

Work great on portrait mode, but on landscape it take whole screen and can't swipe off..

@wotjd
Copy link
Author

wotjd commented Feb 25, 2023

@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