简体   繁体   中英

SwiftUI: @Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:

struct JobsTab: View {

@State private var jobFilter: JobFilter = JobFilter()

var filter: some View {
        Button {
            self.showFilter = true
        } label: {
            Image(systemName: "line.horizontal.3.decrease.circle")
                .renderingMode(.original)
        }
        .sheet(isPresented: $showFilter) {
            FilterView($jobFilter, categoriesViewModel, jobsViewModel)
        }
    }

However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:

struct FilterView: View {
@Environment(\.presentationMode) var presentationMode
    @ObservedObject var categoriesViewModel: CategoriesViewModel
    @ObservedObject var jobsViewModel: JobsViewModel
    let filterViewModel: FilterViewModel
    
    @Binding var jobFilter: JobFilter
    
    @State private var draft: JobFilter
    @State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
        _jobFilter = jobFilter
        _draft = State(wrappedValue: jobFilter.wrappedValue)
        self.categoriesViewModel = categoriesViewModel
        self.jobsViewModel = jobsViewModel
        self.filterViewModel = FilterViewModel()
    }
...
.toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
                        presentationMode.wrappedValue.dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("FilterView.Button.Done.Text".capitalizedLocalization) {
                        let request = Job.defaultRequest()
                        
                        request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
                        request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
                        
                        jobsViewModel.filteredJobsFetchRequest = request
                        self.jobFilter = self.draft
                        presentationMode.wrappedValue.dismiss()
                    }
                }
            }

I have also tried with a @Binding like Paul says here but there's no luck. Is there any workaround, or am I doing something wrong?

Thanks in advance!

EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft . What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a @State ) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false ? I don't know to be honest.

EDIT2: I have also tried `` if #available(iOS 15.0, *) { let _ = Self._printChanges() } else { // Fallback on earlier versions }

in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged

In the sheet try adding this instead.

@Environment(\.dimiss) var dismiss 

Button("Some text") {
    // Code  
    dismiss() 
} 

According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own. Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create @Binding or @State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works. I don't have your full code, but I created a similar situation to your problem as below code:

import SwiftUI

struct Main: View {
@State private var showFilter = false
var body: some View {
    Button {
        self.showFilter = true
    } label: {
        Image(systemName: "line.horizontal.3.decrease.circle")
            .renderingMode(.original)
    }
    .sheet(isPresented: $showFilter) {
       FilterView()
    }
}
}

struct FilterView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
    NavigationView {
        VStack {
            Text("Filter View")
        }.toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("cancel")
                }
            }
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("okay")
                }
            }
        }
    }
}
}

缺少参数onDismiss

.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }

View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an @State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:

struct SheetConfig {
    var isPresented = false
    var data: [String] = []

    mutating func show(initialData: [String] ) {
        isPresented = true
        data = initialData
    }

    mutating func hide() {
        isPresented = false
    }

}

struct ContentView: View {

    @State var config = SheetConfig()

    var body: some View {
        Button {
            config.show(intialData: ...)
        } label: {

        }
        .sheet(isPresented: $config.isPresented) {
            FilterView($config)
        }

Here is what I did to handle this issue.

  1. Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
    
    /// A method which should be triggered whenever dismiss is needed.
    /// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
    func dismissHostedView(presentationMode: Binding<PresentationMode>)
}

public extension UIViewControllerHostedView {
    func dismissHostedView(presentationMode: Binding<PresentationMode>) {
        // Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
        if #available(iOS 15, *) {
            presentationMode.wrappedValue.dismiss()
        } else {
            self.topViewController?.dismisOrPopBack()
        }
    }
}
  1. Extend UIWindow and UIApplication
import UIKit

public extension UIWindow {
    
    // Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
    func visibleViewController() -> UIViewController? {
        if let rootViewController: UIViewController = self.rootViewController {
            return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
        }
        return nil
    }
    
    class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
        switch(vc){
        case is UINavigationController:
            let navigationController = vc as! UINavigationController
            return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
            
        case is UITabBarController:
            let tabBarController = vc as! UITabBarController
            return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
            
        default:
            if let presentedViewController = vc.presentedViewController {
                if let presentedViewController2 = presentedViewController.presentedViewController {
                    return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
                }
                else{
                    return vc;
                }
            }
            else{
                return vc;
            }
        }
        
    }
    
}


@objc public extension UIApplication {
    
    /// LCUIComponents: Returns the current visible top most window of the app.
    var topWindow: UIWindow? {
        return windows.first(where: { $0.isKeyWindow })
    }
    
    var topViewController: UIViewController? {
        return topWindow?.visibleViewController()
    }
    
}

  1. Extend View to handle UIHostingController presented View`
public extension View {
    weak var topViewController: UIViewController? {
        UIApplication.shared.topViewController
    }
}
  1. Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {

    // MARK: - Properties
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

var body some View {
    Button {
     // Note: this part trigger a method in a created protocol above.
     dismissHostedView(presentationMode: presentationMode)
     } label: {
       Text("Tap to dismiss")
   }
} 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM