简体   繁体   中英

SwiftUI: How can I determine if a view is presented in a NavigationView, Sheet, or is the root?

I'm working on a project that requires a custom navigation bar that will have custom buttons and title styling, while also allowing an accessory view below the main nav portion.

Essentially, I'd like to abstract away the need to choose the custom back button based on the presentation style. If it's presented in a sheet, I plan to show an X icon. If it is pushed onto a navigation view I want to show a back error. If it's a root view I want to hide the button altogether.

I've mapped the presentationMode environment variable however when I access the isPresented value I always get true, even on the root view of my app.

Here's a general idea of what I'm working on:

import SwiftUI

struct CustomNavigationBar<Content>: View where Content: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    private let title: LocalizedStringKey
    private let content: (() -> Content)?

    private var backButton: AnyView? {

        let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
            // custom image extension, just resolves to a back icon
            Image.Icons.arrowBack
        }

        if (presentationMode.wrappedValue.isPresented) {
            return AnyView(button)
        } else {
            return nil
        }
    }

    public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
        self.title = title
        self.content = content
    }

    var body: some View {
        VStack {
            content?()
            Divider().foregroundColor(.gray)
        }.navigationBarTitle(title, displayMode: .large)
        .frame(minHeight: 96)
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: backButton)
    }
}

Does anyone have any experience or tips for accessing a view's place in the presentation hierarchy with SwiftUI? Thanks!

You can use SwiftUI-Introspect , used to "Introspect underlying UIKit components from SwiftUI".

Here is a working example of what you are looking for. It is an interactive example, so you can click through the different modes.

import Introspect
import SwiftUI

/* ... */

struct ContentView: View {
    
    @State private var testing = 1
    private let thingsToTest = 3
    
    var body: some View {
        VStack {
            Picker("Testing", selection: $testing) {
                ForEach(1 ... thingsToTest, id: \.self) { index in
                    Text("\(index)")
                        .tag(index)
                }
            }
            .pickerStyle(SegmentedPickerStyle())
            
            Divider()
            
            Spacer()
            
            switch testing {
            case 1:
                PresentationReader { kind in
                    Text("Hello! Kind: \(kind.rawValue)")
                }
                
            case 2:
                NavigationView {
                    PresentationReader { kind in
                        Text("Hello! Kind: \(kind.rawValue)")
                    }
                }
                
            case 3:
                Text("Parent")
                    .sheet(isPresented: .constant(true)) {
                        PresentationReader { kind in
                            Text("Hello! Kind: \(kind.rawValue)")
                        }
                    }
                
            default:
                fatalError("Unavailable")
            }
            
            Spacer()
        }
    }
}
enum Kind: String {
    case navigationView
    case root
    case sheet
}


struct PresentationReader<Content: View>: View {
    typealias PresentedContent = (Kind) -> Content
    
    @State private var kind: Kind = .root
    private let content: PresentedContent
    
    init(@ViewBuilder content: @escaping PresentedContent) {
        self.content = content
    }
    
    var body: some View {
        content(kind)
            .presentationReader(kind: $kind)
    }
}


extension View {
    func presentationReader(kind: Binding<Kind>) -> some View {
        self
            .introspectViewController { vc in
                let rootVC = UIApplication.shared.windows.first?.rootViewController
                let isRoot = vc === rootVC
                var isHosted: Bool { Introspect.findHostingView(from: vc.view) != nil }
                
                if isRoot {
                    kind.wrappedValue = .root
                } else if isHosted {
                    kind.wrappedValue = .navigationView
                } else {
                    kind.wrappedValue = .sheet
                }
            }
    }
}

It works by getting the current view controller the view is in.

  • If the class reference of the root view controller is the same as the current root view controller, this is the root view (meaning it isn't embedded in a NavigationView or .sheet(...) ).
  • If this is not the root, we then check if this view is embedded in a hosting view. If it is, it is in a NavigationView .
  • If the view is neither the root or in a NavigationView , it is therefore in a .sheet(...) .

This is now what your CustomNavigationBar will look like with these 3 changes:

struct CustomNavigationBar<Content>: View where Content: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State private var kind: Kind = .root  // <--- CHANGE #1
    
    private let title: LocalizedStringKey
    private let content: (() -> Content)?

    private var backButton: AnyView? {

        let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
            // custom image extension, just resolves to a back icon
            Image.Icons.arrowBack
        }

        if kind == .navigationView {  // <--- CHANGE #2
            return AnyView(button)
        } else {
            return nil
        }
    }

    public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
        self.title = title
        self.content = content
    }

    var body: some View {
        VStack {
            content?()
                .presentationReader(kind: $kind)  // <--- CHANGE #3
            
            Divider().foregroundColor(.gray)
        }.navigationBarTitle(title, displayMode: .large)
        .frame(minHeight: 96)
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: backButton)
    }
}

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