不完整的滑动返回手势导致导航路径管理问题。

8
我正在寻找以下示例代码中的一个bug的解决方案。我尝试使用SwiftUI 4和iOS 16.0导航API更改实现Navigator Pattern。
该示例在Xcode 14.0+中编译,如果在模拟器或具有iOS 16.0的设备上运行,则会产生我所描述的bug。我想知道这是缺乏知识还是平台缺陷。通过我的日志,我可以看到,当我用不完整的滑动手势诱发bug时,导航路径的元素计数将上升到2,而实际上应在根处返回到0,并且仅在第一层视图中保留1个元素。
是否有一种方法可以更好地管理此类视图层次结构的路径?或者,这是平台级别的bug吗?
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() {
        
    }
}

请注意:通过不完整的划动手势,我指的是用户从前沿开始拖动屏幕,然后将其保持并放回到起始位置,并释放它,以便用户通过不返回而保持在当前视图中。 然后返回到父视图(而非根视图)将导致导航链接失效。
你可以观察我所描述的错误,方法是未能完成从kombucha或coffee详细视图(最深层子视图)滑动返回手势,然后返回到其中一个产品列表视图,并尝试单击其中一个导航链接(应该无法点击)。
通常情况下,返回到根视图会在运行时清除此场景并恢复NavigationLink功能。

2
我有同样的问题。它只出现在NavigationStack而不是NavigationView中。使用没有路径变量的NavigationStack也有这个bug。看起来与已弃用的NavigationLinks无关,因为我已经全部移除并迁移到仅非弃用的链接。 - Jeyhey
如果您定义了NavigationStack,则导航路径必须有一个单一的真实来源,并由导航引擎自动管理。此外,在应用程序的根级别仅使用一个NavigationStack是建议的。很好,您没有使用已弃用的NavigationLinks。使用基于值的链接将确保您遵守新API的设计。据我所知,模型还必须符合Identifiable以防止出现问题。我可能会在下周有时间继续调试并希望发布解决方案。 - Andre
1
看起来问题在iOS 16.1中已经解决了。 - Jeyhey
请参见我的解决方案:https://github.com/andrejandre/NavStacker,它已被解决。 - Andre
4个回答

7

我会尽力根据我的时间安排更新这个问题。需要考虑的一些要点是使用已被弃用的非基于值的NavigationLinks。将其与NavigationStack一起使用可能会导致问题。同样重要的是,只有一个导航路径源才能防止竞争条件的出现。 - Andre

6

看起来在iOS 16.1中已经修复了。

使用Xcode 14.1构建,在首次安装iOS 16.0.3后遇到了该问题。然后更新到iOS 16.1,在不重新构建或重新安装的情况下测试了相同的应用程序,问题消失了。可能是 SwiftUI 的一个错误。


你能展示一个最小的代码解决方案吗?我认为展示你的观察结果是否无错误地使用过时的NavigationLink,并且使用一种导航路径的真正来源是非常重要的。同时,如果使用已弃用的代码(这可能是无意义的,因为这是一种不好的实践),也可以看到苹果是否解决了这个问题,这将非常有趣。 - Andre

1

这是一种非常奇怪的行为,即使在更简单的情况下也能够复现。看起来这个“半”手势正在干扰NavStack中的某些东西。

我还要注意,在

struct CoffeeProductsView: View {
    @State var drinkProductViewModel: DrinkProductViewModel

@State 对我来说没有太多意义,更应该是一个 @ObservedObject,但它对问题没有影响。


谢谢您的提示 - 我会编辑上面的内容以反映对 @ObservedObject 的更改!这只是在翻译到 SO 时的一个笔误。 - Andre
1
我从苹果支持处收到了一些不错的阅读材料 - 一旦我弄清楚模式,我会尽快更新。 - Andre

0

iOS 16.0+(在iOS 16.1上进行了测试)

导航堆栈路径的模型(作为基于值的NavigationLink的基础):

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

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

模型(符合 Identifiable 是最佳实践,可以避免在 ListForEach 视图中使用 \.self 等。不符合 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 - 请注意,您还可以为NavigationPath使用自定义类型,例如[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)
                    }
                }
            }
        }
    }
}

子视图(使用基于值的NavigationLink很重要,否则可能会在新的导航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。 您不需要将整个应用程序的视图层次结构存在于一个单一模型中,否则您可能会被迫使用单个.navigationDestination并努力将属性或对象传递到子视图中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接