The original article was posted on my blog www.fatbobman.com

In this article we will discuss how to implement new features in a SwiftUI 3.0 – interactiveDismissDisabled enhanced version; How to create more Swiftui-like feature extensions.

demand

Since data entry in the health note is all done in the Sheet, in order to prevent users from losing data due to misoperation (using gesture to cancel the Sheet) during the entry process, I have been using various means to strengthen the control over the Sheet since the initial version.

Last September, I introduced the health Note 2.0 control implementation for Sheet in SwiftUI. Goal is:

  • Code controls whether to allow the gesture to cancel Sheet
  • Users are notified when they use the gesture to cancel the Sheet, giving them more control

The final result is as follows:

When the user has unsaved data, canceling the Sheet with a gesture will be prevented and the user must explicitly choose to save or discard the data.

The end result is pretty much what I wanted, but unfortunately it’s not very intuitive to use (see article for details).

SwiftUI version 3.0 of this year, apple has added a new View extension: interactiveDismissDisabled, this extension has realized the first requirement of the above – control whether to allow gestures to cancel Sheet through the code.

struct ExampleView: View {
       @State private var show: Bool = false
       
       var body: some View {
           
           Button("Open Sheet") {
               self.show = true
           }
           .sheet(isPresented: $show) {
               print("finished!")
           } content: {
               MySheet()}}}struct MySheet: View {
       @Environment (\.presentationMode) var presentationMode
       @State var disable = false
       var body: some View {
           Button("Close") {
               self.presentationMode.wrappedValue.dismiss()
           }
           .interactiveDismissDisabled(disable)
       }
   }

Copy the code

Can add interactiveDismissDisabled in controlled view, does not affect other parts of the code logic. This implementation is what I love and what inspires me.

In our WWDC 2021 Review, we’ve discussed how SwiftUI3.0 will affect the thinking and implementation of the SwiftUI extensions written by a large number of third-party developers.

Although interactiveDismissDisabled implementation is very elegant, but health is still not completed notes need a second function: when the user use gestures to cancel the Sheet can be notified, and have more control. So I decided to implement it in a similar way.

The principle of

entrust

Starting from 13 iOS, apple adjusted the modal view of entrust agreement (UIAdaptivePresentationControllerDelegate). Among them:

  • presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool

    Determines whether dismiss sheet is allowed by gesture

  • presentationControllerWillDismiss(_ presentationController: UIPresentationController)

    Execute this method when the user tries to cancel using a gesture

When a user use gestures to cancel Sheet, the system will be the first to perform presentationControllerWillDismiss, then derives from the presentationControllerShouldDismiss whether to allow to cancel.

By default, the UIViewController that presents the Sheet has no delegate set. Therefore, the above requirements can be achieved simply by injecting a defined delegate instance into a particular view controller in the view.

injection

Create an empty UIView (via UIViewRepresentable) in which the UIViewController A that holds it is found. So A’s presentationController is the view controller we need to inject the delegate into.

In previous versions, the notification of gesture cancellation was separated from the rest of the logic, which was cumbersome and affected the look and feel of the code. This time we will solve the problem together.

implementation

Delegate

final class SheetDelegate: NSObject.UIAdaptivePresentationControllerDelegate {
    var isDisable: Bool
    @Binding var attempToDismiss: UUID

    init(_ isDisable: Bool.attempToDismiss: Binding<UUID> = .constant(UUID())) {
        self.isDisable = isDisable
        _attempToDismiss = attempToDismiss
    }

    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        !isDisable
    }

    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        attempToDismiss = UUID()}}Copy the code

UIViewRepresentable

struct SetSheetDelegate: UIViewRepresentable {
    let delegate:SheetDelegate

    init(isDisable:Bool.attempToDismiss:Binding<UUID>){
        self.delegate = SheetDelegate(isDisable, attempToDismiss: attempToDismiss)
    }

    func makeUIView(context: Context) -> some UIView {
        let view = UIView(a)return view
    }

    func updateUIView(_ uiView: UIViewType.context: Context) {
        DispatchQueue.main.async {
            uiView.parentViewController?.presentationController?.delegate = delegate
        }
    }
}
Copy the code

Only one empty view (UIView) needs to be created in the makeUIView. Since there is no guarantee that the view in the Sheet will be displayed properly when executing the makeUIView, the best time to inject is updateUIView.

To make it easy to find the UIController holding the UIView, we need to extend the UIView:

extension UIView {
    var parentViewController: UIViewController? {
        var parentResponder: UIResponder? = self.next
        while parentResponder ! = nil {
            if let viewController = parentResponder as? UIViewController {
                return viewController
            }
            parentResponder = parentResponder?.next
        }
        return nil}}Copy the code

You can then inject a delegate to the view controller that presents the Sheet using the following code

uiView.parentViewController?.presentationController?.delegate = delegate
Copy the code

View Extension

The same method name as the system is used

public extension View{
    func interactiveDismissDisabled(_ isDisable:Bool.attempToDismiss:Binding<UUID>) -> some View{
        background(SetSheetDelegate(isDisable: isDisable, attempToDismiss: attempToDismiss))
    }
}
Copy the code

The results of

It is used in much the same way as the native function:

struct ContentView: View {
    @State var sheet = false
    var body: some View {
        VStack {
            Button("show sheet") {
                sheet.toggle()
            }
        }
        .sheet(isPresented: $sheet) {
            SheetView()}}}struct SheetView: View {
    @State var disable = false
    @State var attempToDismiss = UUID(a)var body: some View {
        VStack {
            Button("disable: \(disable ? "true" : "false")") {
                disable.toggle()
            }
            .interactiveDismissDisabled(disable, attempToDismiss: $attempToDismiss)
        }
        .onChange(of: attempToDismiss) { _ in
            print("try to dismiss sheet")}}}Copy the code

The code for this article can be viewed in Gist

conclusion

SwiftUI has been around for more than two years, and developers are getting the hang of adding new features to the app. By learning and understanding the native API, we can make our implementation more SwiftUI style, the overall code more unified.

I hope this article has been helpful to you.

This article originally appeared on my blog [Elbow’s Swift Notepad]

Welcome to subscribe to my public number: Elbow Swift Notepad

Other recommendations:

Core Data with CloudKit 1-6

How to preview SwiftUI view with Core Data elements in Xcode

www.fatbobman.com/posts/uikit…

Enhance SwiftUI’s navigation view with NavigationViewKit

@ AppStorage research