macOS SwiftUI单视图导航

29

我正在尝试为我的macOS SwiftUI状态栏应用程序创建一个设置视图。 我目前的实现方式是使用NavigationViewNavigationLink,但这种解决方案会产生半个视图,因为设置视图将父视图推到一边。以下是屏幕截图和代码示例。

导航侧栏

输入图像描述

struct ContentView: View {
    var body: some View {
        VStack{
            NavigationView{
            NavigationLink(destination: SecondView()){
                Text("Go to next view")
                }}
        }.frame(width: 800, height: 600, alignment: .center)}
}

struct SecondView: View {
    var body: some View {
        VStack{

                Text("This is the second view")

        }.frame(width: 800, height: 600, alignment: .center)
    }
}

我能找到的少量信息表明,使用SwiftUI在macOS上是不可避免的,因为iOS上的“全屏”NavigationViewStackNavigationViewStyle)在macOS上不可用。

在SwiftUI中有没有一种简单或复杂的方式实现转换到占据整个框架的设置视图?如果没有,是否可能使用AppKit调用用SwiftUI编写的View对象?

同时也是Swift新手 - 请温柔一些。

5个回答

23

这是一个简单的示例,展示了一种自定义导航类解决方案的可能方法。经Xcode 11.4 / macOS 10.15.4测试。

演示

注:背景颜色用于更好的可见性。

struct ContentView: View {
    @State private var show = false
    var body: some View {
        VStack{
            if !show {
                RootView(show: $show)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .transition(AnyTransition.move(edge: .leading)).animation(.default)
            }
            if show {
                NextView(show: $show)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.green)
                    .transition(AnyTransition.move(edge: .trailing)).animation(.default)
            }
        }
    }
}

struct RootView: View {
    @Binding var show: Bool
    var body: some View {
        VStack{
            Button("Next") { self.show = true }
            Text("This is the first view")
        }
    }
}

struct NextView: View {
    @Binding var show: Bool
    var body: some View {
        VStack{
            Button("Back") { self.show = false }
            Text("This is the second view")
        }
    }
}

1
这是一个很好的解决方案。有什么办法可以阻止.animation()修饰符也应用于所有子视图吗? - TheNeil
使用这种方法,是否可以将数据从列表传递到详细信息页面? - Mane Manero

12

我在 Asperi 的建议基础上进行了扩展,并创建了一个通用的、可重用的 StackNavigationView,适用于 macOS(如果需要,也可用于 iOS)。以下是一些亮点:

  • 支持任意数量的子视图(以任何布局方式)。
  • 自动为每个子视图添加“返回”按钮(目前只有文本,但如果使用 macOS 11+,可以更换图标)。

macOS 上的示例视图

Swift v5.2:

struct StackNavigationView<RootContent, SubviewContent>: View where RootContent: View, SubviewContent: View {
    
    @Binding var currentSubviewIndex: Int
    @Binding var showingSubview: Bool
    let subviewByIndex: (Int) -> SubviewContent
    let rootView: () -> RootContent
    
    var body: some View {
        VStack {
            VStack{
                if !showingSubview { // Root view
                    rootView()
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .transition(AnyTransition.move(edge: .leading)).animation(.default)
                }
                if showingSubview { // Correct subview for current index
                    StackNavigationSubview(isVisible: self.$showingSubview) {
                        self.subviewByIndex(self.currentSubviewIndex)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .transition(AnyTransition.move(edge: .trailing)).animation(.default)
                }
            }
        }
    }
    
    init(currentSubviewIndex: Binding<Int>, showingSubview: Binding<Bool>, @ViewBuilder subviewByIndex: @escaping (Int) -> SubviewContent, @ViewBuilder rootView: @escaping () -> RootContent) {
        self._currentSubviewIndex = currentSubviewIndex
        self._showingSubview = showingSubview
        self.subviewByIndex = subviewByIndex
        self.rootView = rootView
    }
    
    private struct StackNavigationSubview<Content>: View where Content: View {
        
        @Binding var isVisible: Bool
        let contentView: () -> Content
        
        var body: some View {
            VStack {
                HStack { // Back button
                    Button(action: {
                        self.isVisible = false
                    }) {
                        Text("< Back")
                    }.buttonStyle(BorderlessButtonStyle())
                    Spacer()
                }
                .padding(.horizontal).padding(.vertical, 4)
                contentView() // Main view content
            }
        }
    }
}

有关使用泛型和 @ViewBuilder 的更多信息,请参见此处

下面是一个基本示例的用法。父视图跟踪当前选择和显示状态(使用 @State),允许其子视图中的任何内容触发状态更改。

struct ExampleView: View {
    
    @State private var currentSubviewIndex = 0
    @State private var showingSubview = false
    
    var body: some View {
        StackNavigationView(
            currentSubviewIndex: self.$currentSubviewIndex,
            showingSubview: self.$showingSubview,
            subviewByIndex: { index in
                self.subView(forIndex: index)
            }
        ) {
            VStack {
                Button(action: { self.showSubview(withIndex: 0) }) {
                    Text("Show View 1")
                }
                Button(action: { self.showSubview(withIndex: 1) }) {
                    Text("Show View 2")
                }
                Button(action: { self.showSubview(withIndex: 2) }) {
                    Text("Show View 3")
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.blue)
        }
    }
    
    private func subView(forIndex index: Int) -> AnyView {
        switch index {
        case 0: return AnyView(Text("I'm View One").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.green))
        case 1: return AnyView(Text("I'm View Two").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.yellow))
        case 2: return AnyView(VStack {
            Text("And I'm...")
            Text("View Three")
        }.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.orange))
        default: return AnyView(Text("Inavlid Selection").frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red))
        }
    }
    
    private func showSubview(withIndex index: Int) {
        currentSubviewIndex = index
        showingSubview = true
    }
}

注意:像这样的泛型要求所有子视图都是相同类型的。如果不是这样,您可以将它们包装在AnyView中,就像我在这里所做的那样。如果您对所有子视图使用一致的类型(根视图的类型不需要匹配),则不需要AnyView包装器。


1
使用这种方法,是否可以将数据从列表传递到详细信息页面? - Mane Manero

0

嘿,我遇到的一个问题是我想要有多个NavigationView层,我不确定这是否也是你的尝试,但如果是的话:MacOS不会继承NavigationView。 这意味着,您需要为其提供自己的DetailView(或在您的情况下是SecondViewNavigationView。因此,只需像[...], destination: NavigationView { SecondView() }) [...]一样嵌入即可解决问题。

但是,小心!对iOS目标执行相同操作将导致意外行为。因此,如果您同时针对两者,请确保使用#if os(macOS)

然而,在制作设置视图时,我建议您还要查看由Apple提供的Settings Scene


0

看起来这个问题在Xcode 13中没有得到解决。

在Xcode 13 Big Sur上测试过,但尚未在Monterrey上测试...输入图像描述


-8

您可以通过以下方式获得全屏导航:

 .navigationViewStyle(StackNavigationViewStyle())

2
啊,如果那样就好了。我遇到一个构建错误:“在macOS中不可用的StackNavigationViewStyle”。 - Husker28
刚在苹果文档中看到,它只适用于使用Mac Catalyst创建的应用程序。 - Volker88
这个方法也适用于将数据传递给LazyGrid() -> Detail吗?我尝试过,但无法使其工作,因为需要同时加载两个视图。 - Mane Manero
1
目前 Xcode 13 beta 3 / macOS 12 SDK 尚不支持此功能。不能再等一年了。 - Tong
1
我发现这非常有用,以前不知道这个风格。 - Vinod Madigeri

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