Неполный жест прокрутки назад приводит к неправильному управлению NavigationPath

Я ищу решения для следующей ошибки в моем примере кода ниже. Я попытался реализовать шаблон навигатора с помощью SwiftUI 4 и набора изменений API навигации iOS 16.0.

Приведенный ниже пример будет скомпилирован в Xcode 14.0+, и при запуске в симуляторе или на устройствах с iOS 16.0 возникнет ошибка, которую я описываю. Мне интересно, это недостаток знаний или ошибка платформы. С моими журналами я вижу, что когда я вызываю ошибку с помощью неполного жеста прокрутки назад, количество элементов пути навигации увеличивается до 2, хотя на самом деле он должен вернуться к 0 в корне и удерживать только 1 элемент на первом уровне вид.

Есть ли способ лучше управлять путем для такой иерархии представлений? Или это ошибка уровня платформы?

      import SwiftUI

enum AppViews: Hashable {
    case kombuchaProductsView
    case coffeeProductsView
    case customerCartView
}

struct RootView: View {
    @StateObject var drinkProductViewModel = DrinkProductViewModel()
    
    var body: some View {
        NavigationStack(path: self.$drinkProductViewModel.navPath) {
            List {
                Section("Products") {
                    NavigationLink(value: AppViews.kombuchaProductsView) {
                        HStack {
                            Text("View all Kombuchas")
                            Spacer()
                            Image(systemName: "list.bullet")
                        }
                    }
                    NavigationLink(value: AppViews.coffeeProductsView) {
                        HStack {
                            Text("View all Coffees")
                            Spacer()
                            Image(systemName: "list.bullet")
                        }
                    }
                }
                Section("Checkout") {
                    NavigationLink(value: AppViews.customerCartView) {
                        HStack {
                            Text("Cart")
                            Spacer()
                            Image(systemName: "cart")
                        }
                    }
                }
            }
            .navigationDestination(for: AppViews.self) { appView in
                switch appView {
                    case .kombuchaProductsView:
                        KombuchaProductsView(drinkProductViewModel: self.drinkProductViewModel)
                    case .coffeeProductsView:
                        CoffeeProductsView(drinkProductViewModel: self.drinkProductViewModel)
                    case .customerCartView:
                        Text("Not implemented")
                }
            }
        }
        .onAppear {
            print("RootView appeared.")
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (RootView)")
        }
    }
}

struct KombuchaProductsView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ForEach(drinkProductViewModel.kombuchaProducts, id: \.self) { kombucha in
                    NavigationLink {
                        KombuchaView(
                            drinkProductViewModel: self.drinkProductViewModel,
                            kombucha: kombucha
                        )
                    } label: {
                        HStack {
                            Text(kombucha.name)
                            Spacer()
                            Text("$\(kombucha.price)")
                            Image(systemName: "chevron.right")
                                .foregroundColor(.gray)
                        }
                    }
                    Divider()
                }
                .padding()
            }
        }
        .navigationTitle("Kombucha Selection")
        .onAppear {
            print("KombuchaProductsView appeared.")
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (KombuchaProductsView)")
        }
        .onDisappear {
            print("KombuchaProductsView disappeared")
        }
    }
}

struct CoffeeProductsView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ForEach(drinkProductViewModel.coffeeProducts, id: \.self) { coffee in
                    NavigationLink {
                        CoffeeView(
                            drinkProductViewModel: self.drinkProductViewModel,
                            coffee: coffee
                        )
                    } label : {
                        HStack {
                            Text(coffee.name)
                            Spacer()
                            Text("$\(coffee.price)")
                            Image(systemName: "chevron.right")
                                .foregroundColor(.gray)
                        }
                    }
                    Divider()
                }
                .padding()
            }
        }
        .navigationTitle("Coffee Selection")
        .onAppear {
            print("CoffeeProductsView appeared")
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (CoffeeProductsView)")
        }
        .onDisappear {
            print("CoffeeProductsView disappeared")
        }
    }
}

struct KombuchaView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    @State var kombucha: Kombucha
    var body: some View {
        VStack {
            Text("Price:")
                .font(.title)
            Text("\(kombucha.price)")
                .font(.callout)
        }
        .navigationTitle(kombucha.name)
        .onAppear {
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (KombuchaView)")
        }
    }
}

struct CoffeeView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    @State var coffee: Coffee
    var body: some View {
        VStack {
            Text("Price:")
                .font(.title)
            Text("\(coffee.price)")
                .font(.callout)
        }
        .navigationTitle(coffee.name)
        .onAppear {
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (CoffeeView)")
        }
    }
}

Для тех, кто заинтересован в точной компиляции моего примера, вот моя фиктивная ViewModel ниже (она просто содержит статические данные — она была создана исключительно для этого исследования):

      class DrinkProductViewModel: ObservableObject {
    
    @Published var navPath = NavigationPath()
    
    @Published var customerCart = [Any]()
    
    @Published var kombuchaProducts = [Kombucha]()
    
    @Published var coffeeProducts = [Coffee]()
    
    init() {
        // Let's ignore networking, and assume a bunch of static data
        self.kombuchaProducts = [
            Kombucha(name: "Ginger Blast", price: 4.99),
            Kombucha(name: "Cayenne Fusion", price: 6.99),
            Kombucha(name: "Mango Tango", price: 4.49),
            Kombucha(name: "Clear Mind", price: 5.39),
            Kombucha(name: "Kiwi Melon", price: 6.99),
            Kombucha(name: "Super Berry", price: 5.99)
        ]
        self.coffeeProducts = [
            Coffee(name: "Cold Brew", price: 2.99),
            Coffee(name: "Nitro Brew", price: 4.99),
            Coffee(name: "Americano", price: 6.99),
            Coffee(name: "Flat White", price: 5.99),
            Coffee(name: "Espresso", price: 3.99)
        ]
    }
    
    func addToCustomerCart() {
        
    }
    
    func removeFromCustomerCart() {
        
    }
}

Обратите внимание: под незавершенным жестом смахивания я подразумеваю, что пользователь начинает перетаскивать экран от переднего края, затем удерживает его, возвращает в исходное положение и отпускает, так что пользователь остается в текущем виде, не переходя назад. Затем возврат к родительскому представлению (не root) приведет к тому, что навигационные ссылки исчезнут.

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

Возврат к корневому представлению обычно очищает этот сценарий во время выполнения и восстанавливает функциональность NavigationLink.

3 ответа

У меня тоже такая же проблема.

Кажется, что стек навигации не работает.

Та же проблема с жестом «наполовину смахнуть назад» присутствует даже в официальном образце Apple: https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app

Я думаю, что после выполнения так называемого «полупролистывания назад» навигационный путь сломался внутри.

Я ценю, если вы разъясните некоторые отзывы от службы поддержки Apple!

Похоже, это исправлено в iOS 16.1.

Построен на Xcode 14.1, сначала установлен на iOS 16.0.3, возникла проблема. Затем обновился до iOS 16.1, протестировал то же приложение (без повторной сборки или переустановки), проблема исчезла. Вероятно, ошибка SwiftUI

iOS 16.0+ (проверено на iOS 16.1)

Модели для пути стека навигации (основа для ваших основанных на значениях):

      enum ProductViews: Hashable {
    case allKombuchas([Kombucha])
    case allCoffees([Coffee])
}

enum DrinkProduct: Hashable {
    case kombucha(Kombucha)
    case coffee(Coffee)
}

Модели (соответствие является передовой практикой и предотвращает необходимость использования\.selfвListс илиForEachвиды и т. д. Модели, не соответствующиеIdentifiableможет вызвать состояние гонки или другие проблемы сNavigationStack):

      struct Kombucha: Hashable, Identifiable {
    let id = UUID()
    var name: String
    var price: Double
}

struct Coffee: Hashable, Identifiable {
    let id = UUID()
    var name: String
    var price: Double
}

Корневой вид (путь навигации может находиться вViewModelобъект, или он мог бы жить как свой собственный@Stateчлен в представлении, которое технически все еще является MVVM — обратите внимание, что вы также можете использовать пользовательские типы для вашего , например, массив[MyCustomTypes], а затем нажмите и извлеките значения в этот настраиваемый типизированный путь):

      struct ParentView: View {
    
    @StateObject var drinkProductViewModel = DrinkProductViewModel()
    
    var body: some View {
        ZStack {
            NavigationStack(path: self.$drinkProductViewModel.navPath) {
                List {
                    Section("Products") {
                        NavigationLink(value: ProductViews.allKombuchas(self.drinkProductViewModel.kombuchaProducts)) {
                            HStack {
                                Text("Kombuchas")
                                Spacer()
                                Image(systemName: "list.bullet")
                            }
                        }
                        NavigationLink(value: ProductViews.allCoffees(self.drinkProductViewModel.coffeeProducts)) {
                            HStack {
                                Text("Coffees")
                                Spacer()
                                Image(systemName: "list.bullet")
                            }
                        }
                    }
                }
                .navigationDestination(for: ProductViews.self) { productView in
                    switch productView {
                    case .allKombuchas(_):
                        KombuchaProductsView(drinkProductViewModel: self.drinkProductViewModel)
                    case .allCoffees(_):
                        CoffeeProductsView(drinkProductViewModel: self.drinkProductViewModel)
                    }
                }
            }
        }
    }
}

Дочерние представления (важно использоватьNavigationLinks, иначе вы можете вызвать состояние гонки или другие ошибки с новым API навигации):

      struct KombuchaProductsView: View {
    @State var drinkProductViewModel: DrinkProductViewModel
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ForEach(drinkProductViewModel.kombuchaProducts) { kombucha in
                    NavigationLink(value: kombucha) {
                        HStack {
                            Text(kombucha.name)
                            Spacer()
                            Text("$\(kombucha.price)")
                            Image(systemName: "chevron.right")
                                .foregroundColor(.gray)
                        }
                    }
                }
                .padding()
            }
        }
        .navigationDestination(for: Kombucha.self) { kombucha in
            KombuchaView(
                drinkProductViewModel: self.drinkProductViewModel,
                kombucha: kombucha
            )
        }
        .navigationTitle("Kombucha Selection")
        .onDisappear {
           print("KombuchaProductsView disappeared")
           print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (KombuchaProductsView)")
        }
    }
}

struct CoffeeProductsView: View {
    @State var drinkProductViewModel: DrinkProductViewModel
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                ForEach(drinkProductViewModel.coffeeProducts) { coffee in
                    NavigationLink(value: coffee) {
                        HStack {
                            Text(coffee.name)
                            Spacer()
                            Text("$\(coffee.price)")
                            Image(systemName: "chevron.right")
                                .foregroundColor(.gray)
                        }
                    }
                    Divider()
                }
                .padding()
            }
        }
        .navigationDestination(for: Coffee.self) { coffee in
            CoffeeView(
                drinkProductViewModel: self.drinkProductViewModel,
                coffee: coffee
            )
        }
        .navigationTitle("Coffee Selection")
        .onDisappear {
            print("CoffeeProductsView disappeared")
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (CoffeeProductsView)")
        }
    }
}

struct KombuchaView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    @State var kombucha: Kombucha
    var body: some View {
        VStack {
            Text("Price:")
                .font(.title)
            Text("\(kombucha.price)")
                .font(.callout)
        }
        .navigationTitle(kombucha.name)
        .onAppear {
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (KombuchaView)")
        }
    }
}

struct CoffeeView: View {
    @ObservedObject var drinkProductViewModel: DrinkProductViewModel
    @State var coffee: Coffee
    var body: some View {
        VStack {
            Text("Price:")
                .font(.title)
            Text("\(coffee.price)")
                .font(.callout)
        }
        .navigationTitle(coffee.name)
        .onAppear {
            print("Nav stack count: \(self.drinkProductViewModel.navPath.count) (CoffeeView)")
        }
    }
}

ViewModel(для фиктивных целей... опять же,NavigationPathможет просто жить в корневом представлении, но это также показывает возможности):

      class DrinkProductViewModel: ObservableObject {
    
    @Published var navPath = NavigationPath()
    
    @Published var customerCart = [Any]()
    
    @Published var kombuchaProducts = [Kombucha]()
    
    @Published var coffeeProducts = [Coffee]()
    
    init() {
        // Let's ignore networking, and assume a bunch of static data
        self.kombuchaProducts = [
            Kombucha(name: "Ginger Blast", price: 4.99),
            Kombucha(name: "Cayenne Fusion", price: 6.99),
            Kombucha(name: "Mango Tango", price: 4.49),
            Kombucha(name: "Clear Mind", price: 5.39),
            Kombucha(name: "Kiwi Melon", price: 6.99),
            Kombucha(name: "Super Berry", price: 5.99)
        ]
        self.coffeeProducts = [
            Coffee(name: "Cold Brew", price: 2.99),
            Coffee(name: "Nitro Brew", price: 4.99),
            Coffee(name: "Americano", price: 6.99),
            Coffee(name: "Flat White", price: 5.99),
            Coffee(name: "Espresso", price: 3.99)
        ]
    }
    
    func addToCustomerCart() {
        
    }
    
    func removeFromCustomerCart() {
        
    }
}

Наконец, важно, чтобы вы учитывали тот факт, что вы можете использовать несколько перечислений в своей кодовой базе, чтобы вы могли правильно использовать их. Вам не нужно, чтобы вся иерархия представлений вашего приложения существовала в одной модели, или вы можете быть вынуждены использовать одну.navigationDestinationи изо всех сил пытаетесь передать свойства или объекты в ваши дочерние представления.

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