Last active
April 10, 2025 03:14
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@isaiah-a97 I encountered the same issue. Turns out I just needed to add
.ignoresSafeArea(.all)
to the view after theinteractiveDismissDisabled
modifier to remove the safe area.