Skip to content

Instantly share code, notes, and snippets.

@peterfriese
Last active April 10, 2024 06:44
Show Gist options
  • Save peterfriese/8fb3d76bdbe21b84495b79b3a86bf898 to your computer and use it in GitHub Desktop.
Save peterfriese/8fb3d76bdbe21b84495b79b3a86bf898 to your computer and use it in GitHub Desktop.
This is an enhanced version of Apple's `interactiveDismissDisabled` view modifier which allows you to act on the user's attempt to dismiss a sheet. See my article for more details. I filed a feedback for a feature request to add this to SwiftUI: FB9782213 (https://openradar.appspot.com/FB9782213)
import SwiftUI
extension View {
public func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttemptToDismiss: (() -> Void)? = nil) -> some View {
InteractiveDismissableView(view: self, isDisabled: isDisabled, onAttemptToDismiss: onAttemptToDismiss)
}
public func interactiveDismissDisabled(_ isDisabled: Bool = true, attemptToDismiss: Binding<Bool>) -> some View {
InteractiveDismissableView(view: self, isDisabled: isDisabled) {
attemptToDismiss.wrappedValue.toggle()
}
}
}
private struct InteractiveDismissableView<T: View>: UIViewControllerRepresentable {
let view: T
let isDisabled: Bool
let onAttemptToDismiss: (() -> Void)?
func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}
func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
context.coordinator.dismissableView = self
uiViewController.rootView = view
uiViewController.parent?.presentationController?.delegate = context.coordinator
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
var dismissableView: InteractiveDismissableView
init(_ dismissableView: InteractiveDismissableView) {
self.dismissableView = dismissableView
}
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
!dismissableView.isDisabled
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
dismissableView.onAttemptToDismiss?()
}
}
}
struct ContentView: View {
@State var showingSheet = false
@State var name: String = "Johnny Appleseed"
var body: some View {
Form {
Section("User Profile") {
Text(name)
}
Button("Edit", action: { showingSheet.toggle() })
}
.sheet(isPresented: $showingSheet) {
EditView(name: $name)
}
}
}
private class ViewModel: ObservableObject {
@Published var name: String
private var original: String
var isModified: Bool {
print("\(name) - \(original)")
return name != original
}
init(name: String) {
self.name = name
self.original = name
}
}
private struct EditView: View {
@Environment(\.dismiss) var dismiss
@Binding var name: String
@StateObject private var viewModel: ViewModel
@State var showingConfirmationDialog = false
init(name: Binding<String>) {
self._name = name
self._viewModel = StateObject(wrappedValue: ViewModel(name: name.wrappedValue))
}
var body: some View {
NavigationView {
Form {
TextField("Enter your name", text: $viewModel.name)
}
.navigationTitle("Edit")
.navigationBarTitleDisplayMode(.inline)
}
.interactiveDismissDisabled(viewModel.isModified) {
showingConfirmationDialog.toggle()
}
.confirmationDialog("", isPresented: $showingConfirmationDialog) {
Button("Save") {
name = viewModel.name
dismiss()
}
Button("Discard", role: .destructive) {
dismiss()
}
Button("Cancel", role: .cancel) { }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@pryder-fleetaero
Copy link

Did anything in iOS 16 get announced which would implement this functionality natively in SwiftUI?

@esnssr
Copy link

esnssr commented Apr 26, 2023

Did anything in iOS 16 get announced which would implement this functionality natively in SwiftUI?

nope, hoping for WWDC 23 :)

@esnssr
Copy link

esnssr commented Apr 27, 2023

@peterfriese hey there. So I was trying out your solution, andI am looking for the exact behavior this code providers, but i noticed something.
The interactiveDismissDisabled doesn't work unless you edit something in the view. Meaning that, if you had a view, and had any editable view in it, (TextField, etc), and you add your interactiveDismissDisabled to it, and set it to always true, always disable dismissing, it won't work unless the user edits something in the view.

i noticed this, uiViewController.parent?.presentationController?.delegate = context.coordinator will not be set the first time because uiViewController.parent?.presentationController will be nil the first time, but after you edit something in the screen, it will call updateUIViewController again which will set the delegate because it won't be nil. See the pic down.

SCR-20230427-pdhc

so if say we had a view with only a button in it, or text, and add interactiveDismissDisabled with always true, it won't work, because we don't edit anything in the screen, which means that the delegate will only be set the first time when it's nil.

i am not sure why this is happening, but if we can fix it so that we can have more control over this, and possibly have the option to always disable the dismissal, even if we don't edit anything, and even for views that don 't have anything editable in the screen.

thanks!

@isaiah-a97
Copy link

Hey man, how do i get rid of the white spaces in the safe areas?

@shawn-shen
Copy link

shawn-shen commented Apr 10, 2024

@peterfriese hey there. So I was trying out your solution, andI am looking for the exact behavior this code providers, but i noticed something. The interactiveDismissDisabled doesn't work unless you edit something in the view. Meaning that, if you had a view, and had any editable view in it, (TextField, etc), and you add your interactiveDismissDisabled to it, and set it to always true, always disable dismissing, it won't work unless the user edits something in the view.

i noticed this, uiViewController.parent?.presentationController?.delegate = context.coordinator will not be set the first time because uiViewController.parent?.presentationController will be nil the first time, but after you edit something in the screen, it will call updateUIViewController again which will set the delegate because it won't be nil. See the pic down.

SCR-20230427-pdhc

so if say we had a view with only a button in it, or text, and add interactiveDismissDisabled with always true, it won't work, because we don't edit anything in the screen, which means that the delegate will only be set the first time when it's nil.

i am not sure why this is happening, but if we can fix it so that we can have more control over this, and possibly have the option to always disable the dismissal, even if we don't edit anything, and even for views that don 't have anything editable in the screen.

thanks!

Making isDisabled a binding should fix it as a binding adds this view to the view update notification chain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment