背景

最近做 SwiftUI 项目,之前对于 navigationDestination 的用法理解不太深刻,觉得很是难用,最近发现了正确的使用方式,这里记录一下。

场景

假设有一个 TabView 类为 A,A 有 B、C 两个Tab,C 的 Tab 下子界面有 D,D 的子界面有 E。

即有 A -> B 和 A -> C -> D -> E 两条链路。

之前的用法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

struct A: View {
var body: some View {
NavigationStack {
TabView(selection: $selectedTab) {
B()
C()
}
}
}
}

struct B: View {

}

struct C: View {
@State private var navigateToD: Bool = false

var body: some View {
VStack {
xxx
Button {
navigateToD.toggle()
} label: {
Text("NavigateToD")
}
}
.navigationDestination(isPresented: $navigateToD) {
D()
}
}
}

struct D: View {
@State private var navigateToE: Bool = false

var body: some View {
VStack {
xxx
Button {
navigateToE.toggle()
} label: {
Text("NavigateToD")
}
}
.navigationDestination(isPresented: $navigateToE) {
E()
}
}
}

struct E: View {
xxx
}

这里面简单的使用确实没问题,每个界面的返回可以通过 Environment 的dismiss 来实现。但是如果想要实现从 E 返回到 C 就非常麻烦了。而且,这里每一步的跳转都散落在各个类里,没有统一的地方管理,后续维护也不易。

所以针对上面存在的问题,对使用进行了优化,

  1. 针对TabView 的两个子视图,B 和 C,分别用NavigationStack包装。不要把NavigationStack放在TabView的外层,因为遇到了放在这里,针对navigationDestination做跳转的时候,遇到了跳转多次的问题。
  2. 声明一个BNavCoordinatorCNavCoordinator,分别用于管理BC的跳转。在具体的NavCoordinator中,声明一个枚举管理这个页面下的所有子界面。然后创建NavCoordinator,实现pushpoppopToRootnavigator方法。
    示例代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    enum CNavScreens: Hashable {
    case d(param1: Int, param2: String)
    case e
    }

    struct CNavCoordinator: ObservableObject {
    @Published var paths = NavigationPath()

    @ViewBuilder
    func navigate(to screen: CNavScreens) -> some View {
    switch screen {
    case .d(let param1, let param2):
    D(param1: param1, param2: param2)
    case .e:
    E()
    }
    }


    // add screen
    func push(_ screen: TGICustomerScreens) {
    paths.append(screen)
    }

    // remove last screen
    func pop() {
    paths.removeLast()
    }

    // popToRoot
    func popToRoot() {
    paths.removeLast(paths.count)
    }
    }

  3. 然后在具体页面中使用,示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

struct A: View {
var body: some View {
TabView(selection: $selectedTab) {
B()
C()
}
}
}

struct B: View {

}

struct C: View {
@StateObject var cNavCoordinator = CNavCoordinator()

var body: some View {
NavigationStack {
VStack {
xxx
Button {
let screen = CNavScreens.d(param1: param1, param2: param2)
cNavCoordinator.push(screen)
} label: {
Text("NavigateToD")
}
}
.environmentObject(navCoordinator)
.navigationDestination(for: CustomScreens.self) { path in
navCoordinator.navigate(to: path)
.environmentObject(navCoordinator)
.environmentObject(xxx)
.environmentObject(yyy)
}
}
}
}

struct D: View {
@EnvironmentObject var cNavCoordinator: CNavCoordinator

var body: some View {
VStack {
xxx
Button {
let screen = CNavScreens.e
cNavCoordinator.push(screen)
} label: {
Text("NavigateToD")
}
}
}
}

struct E: View {
@EnvironmentObject var cNavCoordinator: CNavCoordinator

var body: some View {
VStack {
xxx
Button {
// cNavCoordinator.pop()
cNavCoordinator.popToRoot()
} label: {
Text("BackToC")
}
}
}
}

这样所有的跳转其实都是在根类 B 和 C 中管理,避免了分散到每个页面的逻辑。同时可以方便返回到根视图。