-
-
Save peterfriese/8fb3d76bdbe21b84495b79b3a86bf898 to your computer and use it in GitHub Desktop.
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() | |
} | |
} |
Did anything in iOS 16 get announced which would implement this functionality natively in SwiftUI?
nope, hoping for WWDC 23 :)
@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.
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!
Hey man, how do i get rid of the white spaces in the safe areas?
@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 yourinteractiveDismissDisabled
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 becauseuiViewController.parent?.presentationController
will be nil the first time, but after you edit something in the screen, it will callupdateUIViewController
again which will set the delegate because it won't be nil. See the pic down.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.
Did anything in iOS 16 get announced which would implement this functionality natively in SwiftUI?