Swiftui - Half Modal

SwiftUI - Half modal?

iOS 16+ (Beta)

It looks like half sheet is finally supported in iOS 16.

To manage the size of sheet we can use PresentationDetent and specifically presentationDetents(_:selection:)

Here's an example from the documentation:

struct ContentView: View {
@State private var showSettings = false
@State private var settingsDetent = PresentationDetent.medium

var body: some View {
Button("View Settings") {
showSettings = true
}
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationDetents:(
[.medium, .large],
selection: $settingsDetent
)
}
}
}

Note that if you provide more that one detent, people can drag the sheet to resize it.

Here are possible values for PresentationDetent:

  • large
  • medium
  • fraction(CGFloat)
  • height(CGFloat)
  • custom<D>(D.Type)

SwiftUI Custom Half Modal Calculator Not Working

The problem is in your func halfSheet() and struct HalfSheetHelper. You have created a UIViewControllerRepresentable view, but it does not handle updates.

Once the sheet has already been shown, the sequence of events happening is:

  1. Pressing a button triggers a change to self.value inside PGQuestion1.
  2. SwiftUI re-renders PGQuestion1, and a new closure is passed to .halfSheet { ... } which uses the new value.
  3. Since the HalfSheetHelper is UIViewRepresentable, SwiftUI calls your updateUIViewController() function. A completely new CustomHostingController is created. Then nothing else happens because showSheet is false.

To handle updates properly, I recommend you create a Coordinator class inside your HalfSheetHelper. The coordinator is a class that SwiftUI will keep alive as long as the view is being used, and it can maintain a persistent reference to the CustomHostingController. Then in updateUIViewController(), you can use the coordinator to access the hosting controller and re-assign its rootView with the newly updated sheet contents. For more on these techniques, see the Interfacing with UIKit tutorial.

I also changed how showSheet is handled so that it becomes false only once the sheet is dismissed. This required adding a custom onDismiss closure to the CustomHostingController which it calls in viewDidDisappear.

(There still seems to be a bug with the half-sheet, where if I swipe down to dismiss the sheet and then show it again, it appears fullscreen instead of half-screen. I'm not familiar enough with the presentationController/detents APIs to figure out why this is happening!)

struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
var sheetView: SheetView
@Binding var showSheet: Bool

class Coordinator {
let dummyController = UIViewController()
let sheetController: CustomHostingController<SheetView>
init(sheetView: SheetView, showSheet: Binding<Bool>) {
sheetController = CustomHostingController(rootView: sheetView, onDismiss: { showSheet.wrappedValue = false })
dummyController.view.backgroundColor = .clear
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(sheetView: sheetView, showSheet: $showSheet)
}
func makeUIViewController(context: Context) -> UIViewController {
return context.coordinator.dummyController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.sheetController.rootView = sheetView

if showSheet && uiViewController.presentedViewController == nil {
uiViewController.present(context.coordinator.sheetController, animated: true)
}
}
}


class CustomHostingController<Content: View>: UIHostingController<Content> {
var onDismiss: (() -> Void)?
convenience init(rootView: Content, onDismiss: @escaping () -> Void) {
self.init(rootView: rootView)
self.onDismiss = onDismiss
}

override func viewDidLoad() {
if let presentationController = presentationController as? UISheetPresentationController {
presentationController.detents = [
.medium(),
.large()
]
presentationController.prefersGrabberVisible = true
}
}

override func viewDidDisappear(_ animated: Bool) {
onDismiss?()
}
}

SwiftUI half modal view not allowing text field on parent view to be tapped on

You could change your overlay modifier by a .background() :

.background(
HalfSheetHelper(sheetView: sheetView(), showSheet: showSheet, onEnd: onEnd)
)

This will solve the problem of TextFields and Buttons which are not clickable when the HalfSheet is not presented.

You still won't be able to enter any text when the HalfSheet is presented: but this is how UISheetPresentationController works if you let the noninteractive dimming view underneath the sheet. When we click outside the modal it is dismissed. If you want to change this behaviour, you can change the smallestUndimmedDetentIdentifier.

Edit :

You will still have a problem: when your view is going to be dismissed, updateUIViewController will be called again and the HalfSheet will be presented again (and dismissed again because showSheet == false).
You need some logic to prevent it from being presented again. For example, we can use presentationControllerWillDismiss @Philip's proposal and check if the half sheet was presented :

struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
var sheetView: SheetView
let controller = UIViewController()
@Binding var showSheet: Bool
var onEnd: ()->()

func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}

func makeUIViewController(context: Context) -> UIViewController {
controller.view.backgroundColor = .clear
return controller
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let presenting = uiViewController.presentedViewController != nil
if showSheet && !presenting {
let sheetController = CustomHostingController(rootView: sheetView)
sheetController.presentationController?.delegate = context.coordinator
uiViewController.present(sheetController, animated: true)
} else if !showSheet && presenting {
uiViewController.dismiss(animated: true)
}
}

class Coordinator:NSObject, UISheetPresentationControllerDelegate {
var parent: HalfSheetHelper
init(parent: HalfSheetHelper) {
self.parent = parent
}

func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.parent.showSheet = false
self.parent.onEnd()
}
}
}


Related Topics



Leave a reply



Submit