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:
- Pressing a button triggers a change to
self.value
insidePGQuestion1
. - SwiftUI re-renders
PGQuestion1
, and a new closure is passed to.halfSheet { ... }
which uses the newvalue
. - Since the
HalfSheetHelper
isUIViewRepresentable
, SwiftUI calls yourupdateUIViewController()
function. A completely new CustomHostingController is created. Then nothing else happens becauseshowSheet
isfalse
.
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 @Philip's proposal and check if the half sheet was presented :presentationControllerWillDismiss
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
How to Cast Self to Unsafemutablepointer≪Void≫ Type in Swift
Append Text or Data to Text File in Swift
Get Class Name of Object as String in Swift
When Should I Access Properties With Self in Swift
How to Declare a Variable That Has a Type and Implements a Protocol
How to Do a Swift For-In Loop With a Step
Any Reason Not Use Use a Singleton "Variable" in Swift
Alternative to Performselector in Swift
Swiftui Ios14 - Navigationview + List - Won't Fill Space
Real Time Nstask Output to Nstextview With Swift
How to Round a Double to the Nearest Int in Swift
How to Create Array of Unique Object List in Swift