NavigationStack in SwiftUI

在 SwiftUI 中,NavigationStack 和 NavigationView 都可用于构建导航界面,主要区别是:

  1. avigationStack 是新一代导航容器,NavigationView 将被逐步淘汰。
  2. NavigationStack 使用路径来定义导航状态,更灵活
  3. 更加节省资源
SwiftUI 复制代码
struct NavigationStackSample: View {
    private var countries: [String] = ["China", "Japan", "France", "Korea", "United state"]
    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 40, content: {
                    ForEach(countries, id: .self) { country in
                        NavigationLink {
                            CountryView(country: country)
                        } label: {
                            Text(country)
                        }
                    }
                })
            }
            .navigationTitle("NavigationStack")
        }
    }
}

struct CountryView: View {
    var country: String
    
    init(country: String) {
        self.country = country
        print("【Log】: (country)")
    }
    
    var body: some View {
        Text(country)
    }
}

例子中使用数组循环了一个列表,点击列表中的列,可以导航到下一级页面

我们可以看到控制台输出的log,可以确定循环的列表中的页面都已经被初始化了

如果数组有100条数据,那么就会初始化CountryView 100 次。不管用不用,都先初始化了,这样其实很浪费资源,100个不一定你每一个都会去看。如果是新闻列表,如果你不继续把列表往下翻,那么屏幕以外加载的数据就白白浪费了

我们把上述例子中使用的NavigationView改成NavigationStack看看效果。

NavigationStack需要和一些固定的搭配来使用,下面是官方的一个示例

SwiftUI 复制代码
NavigationStack(path: $presentedParks) {
         List(parks) { park in
             NavigationLink(park.name, value: park)
         }
         .navigationDestination(for: Park.self) { park in
             ParkDetails(park: park)
         }
}

他需要和navigationDestination 方法来搭配使用。

我们也来模仿改改

SwiftUI 复制代码
struct NagationStackSample: View {
    private var countries: [String] = ["China", "Japan", "France", "Korea", "United state"]
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 40, content: {
                    ForEach(countries, id: .self) { country in
                        NavigationLink(value: country) {
                            Text("country (country)")
                        }
                    }
                })
            }
            .navigationTitle("NavigationStack")
            .navigationDestination(for: String.self) { country in
                CountryView(country: country)
            }
        }
    }
}

改成以上代码后,效果和之前的NavigationView效果一样。但是控制台无任何输出。当你点击某一条数据时才会真正的去初始化

点击后代码会走navigationDestination 方法,这时会真的去初始化。到目前为止一切都是正常的。我们再添加一些数据代码,用Int类型循环一些数据

SwiftUI 复制代码
ForEach(0..<10) { i in
  NavigationLink(value: i) {
      Text("Index: (i)")
  }
}

我们把以上代码添加到主视图的VStack里面。

当我们点击新加入的行,此时却没有任何反应。控制台输了一些信息

SwiftUI 复制代码
A NavigationLink is presenting a value of type "Int" but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.

Note: Links search for destinations in any surrounding NavigationStack, then within the same column of a NavigationSplitView.

意思是我在navigationDestination 方法中给的是String 类型,但是点击的数据是Int类型。所以导致无法工作。

此时我们只需要再补充一个 navigationDestination 方法即可。只不过这里的类型要改成Int

SwiftUI 复制代码
.navigationDestination(for: Int.self, destination: { i in
    Text("View (i)")
})

这样就可以正常来使用。

所以在NavigationStack 中的navigationDestination方法参数是一个要注意的地方。

SwiftUI 复制代码
struct NavigationStackSample: View {
    private var countries: [String] = ["China", "Japan", "France", "Korea", "United state"]
    var body: some View {
        NavigationStack {
 ScrollView { 
                VStack(spacing: 40, content: {
                    ForEach(countries, id: .self) { country in
                        NavigationLink(value: country) {
                            Text("country (country)")
                        }
                    }
                    
                    ForEach(0..<10) { i in
                        NavigationLink(value: i) {
                            Text("Index: (i)")
                        }
                    }
                })
            }
           .navigationDestination(for: String.self, destination: { country in
                CountryView(country: country)
            })
            .navigationDestination(for: Int.self, destination: { i in
                Text("View (i)")
            }) 
            .navigationTitle("NavigationStack")
        }
    }
}

自定义试图,我们就需要使用NavigationStack初始化的另一个方法来实现。方法定义如下,它需要传入一个Binding 类型的数据.

SwiftUI 复制代码
@MainActor public init(
path: Binding<NavigationPath>, 
@ViewBuilder root: () -> Root
) where Data == NavigationPath

我们先定一个NavigationPath()

SwiftUI 复制代码
@State private var path = NavigationPath()

将之前的方法替换为如下方法

SwiftUI 复制代码
NavigationStack(path: $path) {}

如果我们不限定path的类型,就是用NavigationPath() 来进行初始化。如果想限定指定的类型可以使用如下代码:

SwiftUI 复制代码
@State private var path: [String] = []

上面类型限定为String,所以在列表 中,只有数据类型是String的才可以点击触发正常,其他类型就会报错。原理和上面的问题是一样的。

那么NavigationPath是什么呢?

NavigationPath 用于定义 NavigationStack 的导航状态。其主要功能包括:

  1. 描述导航堆栈,NavigationPath维护一个视图的路径堆栈,表示当前的导航状态。
  2. 指定导航顺序,通过在Path内列出视图,可以定义精确的导航顺序。
  3. 标识视图,通过给视图加上标识符,可以唯一定位一个视图在路径中的位置。
  4. 当前视图,NavigationPath可以获取当前展示的视图。
  5. 响应导航事件,路径变化时会触发回调,可以处理导航逻辑。
  6. 绑定处理,通过@Binding可以将NavigationPath绑定到视图中。

说了这么多,好像我也不明白。那么我们来看看例子

我们往ScrollerView里面添加以下代码:

SwiftUI 复制代码
Button {
  path.append("UK")
  path.append("India")
  path.append("Philippines")
  path.append(1000) 
} label: {
  Text("PUSH")
    .padding()
}

效果如下:

它会把所有放在path里面的数据进行push 操作。由于我的path数据类型是NavigationPath , 所以我可以往里面添加StringInt类型的值

这样对于想跳转到指定页面的需求就很好解决了,我们只需维护一个NavigationPath就可以完成这个需求。

全部代码如下:

SwiftUI 复制代码
struct NavigationStackSample: View {
    private var countries: [String] = ["China", "Japan", "France", "Korea", "United state"]
    
 @State private var path = NavigationPath() 
    
    var body: some View {
 NavigationStack(path: $path) { 
            ScrollView {
                Button {
                    path.append("UK")
                    path.append("India")
                    path.append("Philippines")
                    path.append(1000) 
                } label: {
                    Text("PUSH")
                        .padding()
                }

                VStack(spacing: 40, content: {
                    ForEach(countries, id: .self) { country in
                        NavigationLink(value: country) {
                            Text("country (country)")
                        }
                    }
                    
                    ForEach(0..<10) { i in
                        NavigationLink(value: i) {
                            Text("Index: (i)")
                        }
                    }
                })
            }
            .navigationDestination(for: String.self, destination: { country in
                CountryView(country: country)
            })
            .navigationDestination(for: Int.self, destination: { i in
                Text("View (i)")
            }) 
            .navigationTitle("NavigationStack")
        }
    }
}

struct CountryView: View {
    var country: String
    
    init(country: String) {
        self.country = country
        print("【Log】: (country)")
    }
    
    var body: some View {
        Text(country)
    }
}

大家有什么看法呢?欢迎留言讨论。

公众号:RobotPBQ

相关推荐
Swift社区1 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
humiaor2 天前
Xcode报错:No exact matches in reference to static method ‘buildExpression‘
swiftui·xcode
humiaor11 天前
Xcode报错:Return from initializer without initializing all stored properties
swiftui·binding
2401_854391081 个月前
SwiftUI 革命:打造未来派用户界面的艺术
ui·ios·swiftui
Swift社区2 个月前
Swift 中的函数式核心与命令式外壳:单向数据流
ios·swiftui·swift
东坡肘子2 个月前
肘子的 Swift 周报 #042| 经验是柄双刃剑
swiftui·swift·apple
Swift社区2 个月前
SwiftUI 中掌握 ScrollView 的使用:滚动可见性
ios·swiftui·swift
2401_857424522 个月前
SwiftUI革新:Xcode UI开发的新纪元
ui·swiftui·xcode
Swift社区2 个月前
SwiftUI 在 WWDC 24 之后的新变化
ios·swiftui·wwdc
Swift社区2 个月前
苹果将为 Apple Watch X 铺路 | Swift 周报 issue 45
ios·swiftui·swift