Проблема с навигацией в Swiftui iOS 18, пропуск страницы

У меня есть приложение, которое принимает ObservableObject, известный как AppState(), настраивается как StateObject и передается в MenuView() и так далее как EnvironmentObject из @main struct.

Мой MenuView использует новый TabView с TabSection и Tab. Он вложен в NavigationStack с navigationDestination() внутри и .alert() прикреплен снаружи.

Основной вариант использования этого прикрепленного Alert — переход на определенную страницу из ЛЮБОГО представления в приложении.

ContentView() переходит к PageTwo() с помощью NavigationDestination(), который привязывается к @EnvironmentObject, обновленному внутри Task (фактическое приложение выполняет асинхронные операции, поэтому это необходимо сохранить). Обратите внимание, что NavigationStack также присутствует в этом представлении, а не в остальных.

На PageTwo() при переходе к PageThree() навигация осуществляется так же, как и раньше, но также запускается таймер на 10 секунд.

Как только таймер истечет, появится всплывающее предупреждение. Это дает возможность снова перейти к ThirdPage(). Кажется, это хорошо работает из любого представления внутри приложения.

Проблема, с которой я столкнулся, заключается в том, что как только произошел переход к PageThree() ЧЕРЕЗ ПРЕДУПРЕЖДЕНИЕ, нажатие кнопки «Назад» в левом верхнем углу пропустит PageTwo() и сразу вернется к ContentView().

Нежелательное поведение.

Есть ли что-то, что я делаю неправильно? Я неправильно настраиваю навигацию? Я неправильно обработал navigationDestination?

Мин репро:

@main
struct FirstAppApp: App {
    
    @StateObject private var app = AppState()
    
    var body: some Scene {
        WindowGroup {
            MenuView()
                .environmentObject(app)
        }
    }
}

struct MenuView: View {
    
    @EnvironmentObject var app: AppState
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        NavigationStack {
            Group {
                if app.showDisclaimer {
                    Legal()
                } else {
                    TabView {
                        ForEach(SidebarItem.allCases, id: \.self) { tab in
                            TabSection("Planning") {
                                Tab("Plan", systemImage: "house") {
                                    ContentView()
                                }.customizationID("plan")
                            }
                        }
                    }
                    .tabViewStyle(.sidebarAdaptable)
                    .tabViewCustomization($customisation)
                }
            }
            .navigationDestination(isPresented: $app.atisUpdated) {
                PageThree()
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    Task {
                        app.atisUpdated = true
                    }
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    Task {
                        app.atisTimer.invalidate()
                    }
                })
            )
        }
    }
}

enum SidebarItem: String, Identifiable, CaseIterable {
    case planner = "Planner"
    var id: String { self.rawValue }
    var iconName: String {
        switch self {
        case .planner:
            return "house"
        }
    }
    var customizationID: String {
        switch self {
        case .planner:
            return "planner"
        }
    }
}


struct ContentView: View {
    
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    Task {
                        app.readyToNavigate = true
                    }
                }
            }
            .navigationDestination(isPresented: $app.readyToNavigate) {
                PageTwo()
            }
        }
    }
}


struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                Task {
                    app.atisButtonPressed = true
                    await app.startTimer()
                }
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}


struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 3")
            }
        }
    }
}



class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false

    @MainActor
    func startTimer() async {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
            self.newAtisAvailable = true
        }
    }
}

Проблема в том, что у вас несколько NavigationStack. Вы можете попробовать удалить их все, кроме одного в ContentView (у меня работает). Обратите внимание, что все ваши Task{...} в кнопках не нужны, удалите их, кроме PageTwo.

workingdog support Ukraine 16.07.2024 07:54

Я только что попробовал это, но, похоже, это не работает. Я получаю: «Не помещайте модификатор пункта назначения навигации внутри «ленивого» контейнера, например List или LazyVStack». Фиолетовое предупреждение, потому что .navigationDestination находится за пределами NavigationStack.

PW1990 16.07.2024 11:24

уберите, конечно, .navigationDestination(isPresented: $app.atisUpdated) { PageThree() } в MenuView. Я уверен, что вы сможете найти решение, а не ждать, пока кто-то другой сделает это за вас.

workingdog support Ukraine 16.07.2024 14:05

Попробуйте заменить class AppState на @State.

malhal 16.07.2024 20:05

@workingdogsupportUkraine Я просто не очень в этом разбираюсь. Я думал, что кто-то с лучшими знаниями, чем я, сможет увидеть проблему, которую я не вижу. Я столько раз передвигал его, пытаясь достать. Кажется, что навигация происходит только между представлением, к которому прикреплен .navigationDestination, и представлением назначения. Имея это в виду, я пробовал разные Navstacks и т. д. forums.developer.apple.com/forums/thread/756647 в каждом представлении написано navstack.

PW1990 16.07.2024 23:06
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
110
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

I think the problem is that you'r not ensuring that NavStack maintains the correct  state, the root cause is that the navigation state might be overwritten or improperly managed when the alert triggers the navigation.

You must ensure single NavStack in the entire App rather than nesting NavStacks instances within individual views, then Use Binding and ObservableObject properly, you must manage the navigation state more effectively by ensuring state changes are correctly bound and observed across views.

here is an update of your version code : 

import SwiftUI

@main
struct FirstAppApp: App {
    @StateObject private var app = AppState()

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                MenuView()
                    .environmentObject(app)
            }
        }
    }
}

struct MenuView: View {
    @EnvironmentObject var app: AppState
    @AppStorage("MyAppTabViewCustomization") private var customisation: TabViewCustomization

    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: \.self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
                .alert(isPresented: $app.newAtisAvailable) {
                    Alert(
                        title: Text("Press GO to navigate to page 3"),
                        primaryButton: .default(Text("GO"), action: {
                            app.atisUpdated = true
                        }),
                        secondaryButton: .cancel(Text("STOP"), action: {
                            app.atisTimer.invalidate()
                        })
                    )
                }
            }
        }
        .navigationDestination(isPresented: $app.atisUpdated) {
            PageThree()
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 1")
            Button("Go to page 2") {
                app.readyToNavigate = true
            }
        }
        .navigationDestination(isPresented: $app.readyToNavigate) {
            PageTwo()
        }
    }
}

struct PageTwo: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                app.atisButtonPressed = true
                app.startTimer()
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}

struct PageThree: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false

    func startTimer() {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in
            DispatchQueue.main.async {
                self.newAtisAvailable = true
            }
        }
    }
}
Ответ принят как подходящий

Проблема в том, что у вас несколько NavigationStack.

Если вам нужен больший контроль над path из NavigationStack попробуйте использовать NavigationStack(path: $navigationPath) { ....} и соответствующим образом измените путь в своих представлениях.

Вот мой пример кода, который работает для меня.

struct MenuView: View {
    @EnvironmentObject var app: AppState
    
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: \.self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    app.atisUpdated = true
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    app.atisTimer.invalidate()
                })
            )
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    app.readyToNavigate = true
                }
            }
            .navigationDestination(isPresented: $app.readyToNavigate) {
                PageTwo()
            }
        }
    }
}


struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                Task {
                    app.atisButtonPressed = true
                    await app.startTimer()
                }
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}


struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

struct Legal: View {
    var body: some View {
        Text("Legal")
    }
}

Обратите внимание: ссылка на форум, которую вы показываете в своем комментарии, не содержит ...navstack in each view, там написано NavigationStack для каждого Tab (которому это нужно). Смысл NavigationStack не должен быть самым верхним видом, он/они должны быть внутри TabView. Также обратите внимание: вам придется скорректировать свой код .alert(...), поскольку я не совсем понимаю, чего вы хотите с его помощью достичь.

РЕДАКТИРОВАТЬ-1:

Альтернативный подход — использовать NavigationStack(path: $app.path) {...} как показано в этом примере кода:

struct MenuView: View {
    @EnvironmentObject var app: AppState
    
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: \.self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    app.atisUpdated = true
                    app.path.append("Page3")  // <--- here
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    app.atisTimer.invalidate()
                })
            )
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack(path: $app.path) {  // <--- here
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    app.readyToNavigate = true
                    app.path.append("Page2")  // <--- here
                }
            }
            .navigationDestination(for: String.self) { page in  // <--- here
                if page == "Page2" {
                    PageTwo()
                } else if page == "Page3" {
                    PageThree()
                }
            }
        }
    }
}

struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                app.atisButtonPressed = true
                app.path.append("Page3")  // <--- here
                Task {
                    await app.startTimer()
                }
            }
        }
    }
}

struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

struct Legal: View {
    var body: some View {
        Text("Legal")
    }
}

class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false
    
    @Published var path = NavigationPath() // <--- here
    
    @MainActor
    func startTimer() async {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
            self.newAtisAvailable = true
        }
    }
}

Спасибо @workingdog, поддержите Украину, ваша версия с путем работает хорошо! Я очень ценю ваши ответы, они всегда просты и полезны.

PW1990 17.07.2024 13:32

Другие вопросы по теме