Навигация в macOS SwiftUI для единого представления

Я пытаюсь создать представление настроек для своего приложения строки состояния macOS SwiftUI. В моей реализации до сих пор использовалсяNavigationView, а также NavigationLink, но это решение создает половину представления, поскольку представление настроек отодвигает родительский вид в сторону. Снимок экрана и пример кода ниже.

Снимок экрана - боковая панель навигации

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, потому что `` полный экран '' NavigationView на iOS (StackNavigationViewStyle) недоступен в macOS.

Есть ли простой или даже сложный способ реализовать переход к представлению настроек, которое занимает весь кадр в SwiftUI для macOS? А если нет, можно ли использоватьAppKit вызвать объект View, написанный на SwiftUI?

Также новичок в Swift - будьте осторожны.

5 ответов

Решение

Вот простая демонстрация возможного подхода к пользовательскому решению, похожему на навигацию. Протестировано с 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")
        }
    }
}

Я расширил замечательное предложение Asperi и создал универсальный многоразовыйStackNavigationViewдля macOS (или даже iOS, если хотите). Некоторые основные моменты:

  • Он поддерживает любое количество подпредставлений (в любом макете).
  • Он автоматически добавляет кнопку "Назад" для каждого подпредставления (пока только текст, но вы можете поменять местами значок, если используете 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
            }
        }
    }
}

Больше информации на @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 оболочка не требуется, если вы используете согласованный тип для всех вложенных представлений (тип корневого представления не обязательно должен совпадать).

Кажется, это не было исправлено в Xcode 13.

Проверено на Xcode 13 Big Sur, но не на Монтеррее ...

Эй, у меня была проблема, что я хотел иметь несколько слоев navigationView, я не уверен, что это тоже ваша попытка, но если это так: MacOS НЕ наследует NavigationView. Это означает, что вам необходимо предоставить свой DetailView (или SecondView в вашем случае) со своим NavigationView. Итак, просто вставляем как [...], destination: NavigationView { SecondView() }) [...] должен сделать свое дело.

Но осторожно! То же самое для целей iOS приведет к неожиданному поведению. Итак, если вы нацелены на оба, убедитесь, что вы используете #if os(macOS)!

Однако при создании представления настроек я бы порекомендовал вам также заглянуть в сцену настроек, предоставленную Apple.

Вы можете получить полноэкранную навигацию с

 .navigationViewStyle(StackNavigationViewStyle())
Другие вопросы по тегам