macOS SwiftUI Navigation for a Single View

后端 未结 3 1201
梦如初夏
梦如初夏 2021-01-05 02:47

I\'m attempting to create a settings view for my macOS SwiftUI status bar app. My implementation so far has been using a NavigationView, and NavigationLin

相关标签:
3条回答
  • 2021-01-05 03:13

    You can get full screen navigation with

     .navigationViewStyle(StackNavigationViewStyle())
    
    0 讨论(0)
  • 2021-01-05 03:14

    Here is a simple demo of possible approach for custom navigation-like solution. Tested with Xcode 11.4 / macOS 10.15.4

    Note: background colors are used for better visibility.

    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")
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-05 03:38

    I've expanded upon Asperi's great suggestion and created a generic, reusable StackNavigationView for macOS (or even iOS, if you want). Some highlights:

    • It supports any number of subviews (in any layout).
    • It automatically adds a 'Back' button for each subview (just text for now, but you can swap in an icon if using macOS 11+).

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

    More info on @ViewBuilder and generics used can be found here.

    Here's a basic example of it in use. The parent view tracks current selection and display status (using @State), allowing anything inside its subviews to trigger state changes.

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

    Note: Generics like this require all subviews to be of the same type. If that's not so, you can wrap them in AnyView, like I've done here. The AnyView wrapper isn't required if you're using a consistent type for all subviews (the root view’s type doesn’t need to match).

    0 讨论(0)
提交回复
热议问题