SwiftUI中NavigationStack使用以及与NavigationView的区别(NavigationLink,navigationDestination,NavigationPath)

在iOS开发中,导航视图无疑是最常用的组件之一。当SwiftUI首次发布时,它附带了一个名为NavigationView的视图,用于构建基于导航的用户界面。随着iOS 16的发布,苹果已经弃用了旧的导航视图,并引入了一个名为NavigationStack的新视图来呈现视图堆栈。最重要的是,开发人员可以利用这个新视图来构建数据驱动的导航。

在iOS 16及以后,NavigationView将会被弃用,取而代之则是NavigationStack

首先,我们将研究如何实现NavigationView。接下来,我们将看一个如何实现NavigationStack的示例。

NavigationView

先看一下NavigationView的用法:

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

  var body: some View {
    NavigationView {
      List(colors, id: \.self) { color in
        NavigationLink {
          ColorView(color: color)
        } label: {
          Text("\(color.description.capitalized)")
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
    }
  }
}

struct ColorView: View {
  let color: Color

  init(color: Color) {
    self.color = color
    print("\(color.description)")
  }

  var body: some View {
    color
      .ignoresSafeArea()
  }
}

上面的代码采用NavigationViewNavigationLink的组合,加上List组件,显示了一组颜色,并且点击的时候调转到另一个界面显示该颜色。

在这里插入图片描述
当我们用模拟器或者真机测试的时候,我们要跳转的ColorView都已经创建出来了,我们在ColorViewinit方法里面添加了打印。运行起来的结果看下面gif中的输出部分。

在这里插入图片描述
这种提前创建出来并不友好,上面我们是用了有复用机制的List组件,那么提前创建出来的ColorView实例并不多,但是如果用其他组件渲染,可能对内存有一定的影响。

NavigationStack

随着iOS 16 + 引入了NavigationStack,为了兼容之前的NavigationView,我们可以直接把NavigationView换成NavigationStack,其他的不变。

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

  var body: some View {
    NavigationStack {
      List(colors, id: \.self) { color in
        NavigationLink {
          ColorView(color: color)
        } label: {
          Text("\(color.description.capitalized)")
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
    }
  }
}

不过如果只是这种改变,那我们就没有发挥出NavigationStack的最大好处。

NavigationStack引入了一个名为navigationDestination修饰符,它将目标视图与呈现的数据类型关联起来。

func navigationDestination<D, C>(
    for data: D.Type,
    @ViewBuilder destination: @escaping (D) -> C
) -> some View where D : Hashable, C : View
  • data: 和目标视图匹配的数据类型。比如上面示例中,遍历colors数组,点击再跳转到目标界面,那么这个匹配的数据类型就是Color.self.
  • destination: 一个视图构造器,返回一个目标视图。当导航栏堆栈状态中包含了data类型的值,那么就显示这个目标视图。这个构造器带了一个data类型的参数。

如果使用了navigationDestination修饰符,那么在NavigationLink中我们也不需要添加目标视图了。而是采用下面这个NavigationLink初始化方法:

init<P>(
    value: P?,
    @ViewBuilder label: () -> Label
) where P : Hashable
  • value: 一个可选的值,当用户点击的时候,SwiftUI保存一个该value的副本,当传入nil的时候,界面也销毁了。
  • label:一个描述当前navigation link的文本。

还是上面的示例,我们改一下代码,如下:

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

  var body: some View {
    NavigationStack {
      List(colors, id: \.self) { color in
        NavigationLink(value: color) {
          Text("\(color.description.capitalized)")
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
      .navigationDestination(for: Color.self) { color in
        ColorView(color: color)
      }
    }
  }
}

上面NavigationLinkvalue传入了color值,当点击的时候,如果SwiftUI在包含它的NavigationStack的视图层次结构中找到了一个匹配的修饰符(navigationDestination修饰符绑定的类型和NavigationLinkvalue的类型相同),它就会把这个修饰符对应的目标视图推入到堆栈中。
如果没有匹配的navigationDestination修饰符,那么无法执行跳转。

navigationDestination修饰符的构造器闭包中返回的参数即是NavigationLinkvalue

另外采用这种方式,之前NavigationView提前创建目标视图的问题也没有了。

多navigationDestination处理

如果List中有不同的类型数据,那么怎么支持跳转呢?

navigationDestination修饰符可以添加一次,也可以添加多次,只要绑定不同的类型即可。比如上面的代码中我们在添加水果信息。

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

  let fruits: [String] = ["apple", "banana", "orange"]

  var body: some View {
    NavigationStack {
      List {
        Section("Colors") {
          ForEach(colors, id: \.self) { color in
            NavigationLink(value: color) {
              Text("\(color.description.capitalized)")
            }
          }
        }
        Section("Fruits") {
          ForEach(fruits, id: \.self) { fruit in
            NavigationLink(value: fruit) {
              Text("\(fruit.capitalized)")
            }
          }
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
      .navigationDestination(for: Color.self) { color in
        ColorView(color: color)
      }
      .navigationDestination(for: String.self) { fruit in
        FruitView(fruit: fruit)
      }
    }
  }
}

代码中除了.navigationDestination(for: Color.self),还添加了.navigationDestination(for: String.self),显示水果的时候,我们用的是String类型,NavigationLink中也绑定了对应的水果值,如:NavigationLink(value: fruit)

在这里插入图片描述
特别提示:不要将navigationDestination修饰符添加到懒加载容器控件的内部,比如List或者LazyVStack等,这些懒加载容器控件只在需要在屏幕上呈现子视图时才创建子视图。必须在这些懒加载容器控件外部添加navigationDestination修饰符,以便导航堆栈始终可以看到目的地。

导航堆栈管理状态(Navigation state)

默认情况下,导航堆栈管理状态以跟踪堆栈上的视图。但是,我们的代码可以通过绑定到创建的数据值集合来初始化堆栈,从而实现对状态的控制。堆栈在向堆栈添加视图时向集合添加元素,并在删除视图时删除元素。
NavigationStack视图有另一个初始化方法,它接受一个path参数,该参数绑定到堆栈的导航状态。

@MainActor
init(
    path: Binding<Data>,
    @ViewBuilder root: () -> Root
) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable

下面代码中添加了一个名为path的状态变量,它是一个Color数组,用于记录导航状态。在NavigationStack的初始化过程中,我们传递并绑定它来管理堆栈。当导航堆栈的状态发生变化时,path变量的值将自动更新,比如点击red进入到下一个界面后,path数组就将red添加到数据中。
如果初始化一个空数组,那代码导航栏堆栈中没有任何视图。

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

  @State private var path: [Color] = []

  let fruits: [String] = ["apple", "banana", "orange"]

  var body: some View {
    NavigationStack(path: $path) {
      List {
        Section("Colors") {
          ForEach(colors, id: \.self) { color in
            NavigationLink(value: color) {
              Text("\(color.description.capitalized)")
            }
          }
        }
        Section("Fruits") {
          ForEach(fruits, id: \.self) { fruit in
            NavigationLink(value: fruit) {
              Text("\(fruit.capitalized)")
            }
          }
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
      .navigationDestination(for: Color.self) { color in
        ColorView(color: color)
      }
      .navigationDestination(for: String.self) { fruit in
        FruitView(fruit: fruit)
      }
    }
  }
}

在初始化的时候,我们也可以给path添加一些元素,这意味着导航栏堆栈中添加了这些对应的值,程序运行起来后直接就显示了堆栈顶部的值关联的界面。
在这里插入图片描述

上面的代码中,绑定的path是我们创建的@State private var path: [Color] = [],是一个具体数据类型的状态数组,这种情况下,如果List中显示了不同的类型的数据,那么只有与path类型相同的数据才能跳转到下一个界面,比如上面代码中,点击color就行跳转到下一个界面,而点击fruit则毫无反应。如果要解决这个问题,在初始化NavigationStack的时候,在传入绑定path的时候,传入一个NavigationPath类型的状态值。

@State private var path = NavigationPath()
@MainActor
init(
    path: Binding<NavigationPath>,
    @ViewBuilder root: () -> Root
) where Data == NavigationPath

这样就支持不同类型的跳转了。如果想往导航栏堆栈中提前添加一些元素,就直接往path数组中追加即可。

struct NavigationStackDemo: View {

  let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

//  @State private var path: [Color] = [.red, .gray, .green, .orange]

  @State private var path = NavigationPath()

  let fruits: [String] = ["apple", "banana", "orange"]

  var body: some View {
    NavigationStack(path: $path) {
      List {
        Section("Colors") {
          ForEach(colors, id: \.self) { color in
            NavigationLink(value: color) {
              Text("\(color.description.capitalized)")
            }
          }
        }
        Section("Fruits") {
          ForEach(fruits, id: \.self) { fruit in
            NavigationLink(value: fruit) {
              Text("\(fruit.capitalized)")
            }
          }
        }
      }
      .listStyle(.plain)
      .navigationTitle("NavigationView")
      .navigationBarTitleDisplayMode(.inline)
      .navigationDestination(for: Color.self) { color in
        ColorView(color: color)
      }
      .navigationDestination(for: String.self) { fruit in
        FruitView(fruit: fruit)
      }
    }
    .onAppear {
      path.append("apple")
      path.append(Color.red)
    }
  }
}

onAppear中先后添加了两个元素,程序运行起来后,导航栏直接push到了red界面,back后到apple界面,再back到主界面。
在这里插入图片描述

写在最后

NavigationStackNavigationView要强大了很多,允许我们手动管理导航栏堆栈,如果我们的App最低支持iOS 16,那么就将NavigationStack用起来吧。

最后,希望能够帮助到有需要的朋友,如果您觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值