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

5

我正在开发一个项目,需要定制导航栏并添加自定义按钮和标题样式,同时还需要在主导航部分下方添加辅助视图。

本质上,我想抽象出根据展示风格选择自定义返回按钮的需要。如果以模态视图的形式呈现,则计划显示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时能够访问视图在呈现层次结构中的位置,并可以提供一些经验或提示?谢谢!


presentationMode 是一种绑定,允许视图关闭自身。可以使用 preferencekey 作为子视图向上发送数据的一种方式。 - New Dev
1个回答

4
你可以使用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
                }
            }
    }
}

它的工作原理是获取视图所在的当前视图控制器。
  • 如果根视图控制器的类引用与当前根视图控制器相同,则这是根视图(这意味着它没有嵌入在NavigationView.sheet(...)中)。
  • 如果这不是根视图,则检查此视图是否嵌入在托管视图中。如果是,则在NavigationView中。
  • 如果该视图既不是根部也不在NavigationView中,则它在.sheet(...)中。
现在,您的CustomNavigationBar将通过这三个更改呈现如下形式:
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)
    }
}

1
哇,这是一个非常有趣的库。感谢你的帮助,乔治,这个解决方案很有效。 - Ben M
1
@BenM 很高兴它能正常工作!我非常喜欢这个包,它是我使用的极少数依赖之一。此外,它还可以修复小的SwiftUI错误,并让您在SwiftUI中做更多事情而不受限制。 - George
当然可以。我有几个其他的解决方案,其中我编写了一个UIViewRepresentable实现,我可能会将其切换到使用SwiftUI实现的这个库中。非常感谢您抽出时间编写这个示例。 - Ben M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接