SwiftUI:如何确定一个视图是在 NavigationView 中呈现,Sheet,还是根视图?

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

我正在处理一个需要自定义导航栏的项目,该导航栏将具有自定义按钮和标题样式,同时还允许在主导航部分下方有一个附件视图。

基本上,我想抽象出根据演示样式选择自定义后退按钮的需要。如果它显示在 sheet 中,我打算显示一个 X 图标。如果它被推到导航视图上,我想显示一个返回错误。如果它是根视图,我想完全隐藏按钮。

我已经映射了 presentationMode 环境变量,但是当我访问 isPresented 值时,我总是得到 true,即使在我的应用程序的根视图中也是如此。

以下是我正在从事的工作的总体思路:

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)
    }
}

有没有人有使用 SwiftUI 访问视图在表示层次结构中的位置的经验或技巧?谢谢!

您可以使用 SwiftUI-Introspect,用于“从 SwiftUI 内省底层 UIKit 组件”。

这是您正在寻找的工作示例。这是一个交互式示例,因此您可以单击不同的模式。

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
                }
            }
    }
}

它通过获取视图所在的当前视图控制器来工作。

  • 如果根视图控制器的 class 引用与当前根视图控制器相同,则这是根视图(意味着它没有嵌入 NavigationView.sheet(...)).
  • 如果这不是根视图,我们将检查此视图是否嵌入到托管视图中。如果是,则在 NavigationView.
  • 如果视图既不是根视图也不在 NavigationView 中,因此它在 .sheet(...) 中。

这就是您的 CustomNavigationBar 经过这 3 项更改后的样子:

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)
    }
}