У меня есть приложение, которое принимает 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
}
}
}
Я только что попробовал это, но, похоже, это не работает. Я получаю: «Не помещайте модификатор пункта назначения навигации внутри «ленивого» контейнера, например List
или LazyVStack
». Фиолетовое предупреждение, потому что .navigationDestination находится за пределами NavigationStack.
уберите, конечно, .navigationDestination(isPresented: $app.atisUpdated) { PageThree() }
в MenuView
. Я уверен, что вы сможете найти решение, а не ждать, пока кто-то другой сделает это за вас.
Попробуйте заменить class AppState
на @State
.
@workingdogsupportUkraine Я просто не очень в этом разбираюсь. Я думал, что кто-то с лучшими знаниями, чем я, сможет увидеть проблему, которую я не вижу. Я столько раз передвигал его, пытаясь достать. Кажется, что навигация происходит только между представлением, к которому прикреплен .navigationDestination, и представлением назначения. Имея это в виду, я пробовал разные Navstacks и т. д. forums.developer.apple.com/forums/thread/756647 в каждом представлении написано navstack.
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, поддержите Украину, ваша версия с путем работает хорошо! Я очень ценю ваши ответы, они всегда просты и полезны.
Проблема в том, что у вас несколько
NavigationStack
. Вы можете попробовать удалить их все, кроме одного вContentView
(у меня работает). Обратите внимание, что все вашиTask{...}
в кнопках не нужны, удалите их, кромеPageTwo
.