FlowStacks

这个包使用了 SwiftUI 强大且熟悉的 NavigationStack API,并赋予其更强大的功能,让您不仅可以将相同的 API 用于推送导航,还可以用于呈现 sheet 和全屏覆盖。而且因为它使用了旧版 SwiftUI 中可用的导航 API 实现,所以您甚至可以在更早版本的 iOS、tvOS、watchOS 和 macOS 上使用它。

如果您有以下需求,您可能会喜欢这个库:

✅ 您希望支持深度链接到应用程序中深度嵌套的导航路由。
✅ 您希望在不同的导航上下文中轻松地重复使用视图。
✅ 您希望轻松返回到根屏幕或导航堆栈中的特定屏幕。
✅ 您希望使用协调器模式将导航逻辑保存在一个地方。
✅ 您希望将应用程序的导航分解为多个可重用的协调器并将它们组合在一起。

熟悉的 API

如果您已经了解 SwiftUI 的 NavigationStack API,那么 FlowStacks 应该会让您感到熟悉且直观。只需在类型和函数名称中将“Navigation”替换为“Flow”即可。

NavigationStack -> FlowStack

NavigationLink -> FlowLink

NavigationPath -> FlowPath

navigationDestination -> flowDestination

NavigationStack 的完整 API 均已复制,因此您可以使用绑定到 Array 的绑定、绑定到 FlowPath 的绑定或者根本没有绑定的方式来初始化 FlowStack。唯一的区别是,数组应该是一个 [Route<MyScreen>]s 而不是 [MyScreen]Route 枚举将目标数据与有关所使用的呈现样式的信包含在一起。类似地,当您创建 FlowLink 时,您还必须指定路由样式,例如 .push.sheet.cover。与 NavigationStack 一样,如果用户点击后退按钮或滑动以关闭 sheet,则路由数组将自动更新以反映新的导航状态。

示例

点击展开示例
import FlowStacks
import SwiftUI

struct ContentView: View {
  @State var path = FlowPath()
  @State var isShowingWelcome = false

  var body: some View {
    FlowStack($path, withNavigation: true) {
      HomeView()
        .flowDestination(for: Int.self, destination: { number in
          NumberView(number: number)
        })
        .flowDestination(for: String.self, destination: { text in
          Text(text)
        })
        .flowDestination(isPresented: $isShowingWelcome, style: .sheet) {
          Text("Welcome to FlowStacks!")
        }
    }
  }
}

struct HomeView: View {
  @EnvironmentObject var navigator: FlowPathNavigator
  
  var body: some View {
    List {
      ForEach(0 ..< 10, id: \.self) { number in
        FlowLink(value: number, style: .sheet(withNavigation: true), label: { Text("Show \(number)") })
      }
      Button("Show 'hello'") {
        navigator.push("Hello")
      }
    }
    .navigationTitle("Home")
  }
}

struct NumberView: View {
  @EnvironmentObject var navigator: FlowPathNavigator
  let number: Int

  var body: some View {
    VStack(spacing: 8) {
      Text("\(number)")
      FlowLink(
        value: number + 1,
        style: .push,
        label: { Text("Show next number") }
      )
      Button("Go back to root") {
        navigator.goBackToRoot()
      }
    }
    .navigationTitle("\(number)")
  }
}

其他功能

除了复制新的 NavigationStack API 的标准功能之外,还添加了一些有用的实用程序。

FlowNavigator

可以通过环境访问 FlowNavigator 对象,从而可以访问当前的路由数组,并可以通过多种便捷方法更新它。可以通过环境访问导航器,例如,对于一个 FlowPath 支持的堆栈。

@EnvironmentObject var navigator: FlowPathNavigator

或者对于由路由数组支持的 FlowStack,例如 [Route<ScreenType>]

@EnvironmentObject var navigator: FlowNavigator<ScreenType>

这是一个使用 FlowNavigator 的示例:

@EnvironmentObject var navigator: FlowNavigator<ScreenType>

var body: some View {
  VStack {
    Button("View detail") {
      navigator.push(.detail)
    }
    Button("Go back to profile") {
      navigator.goBackTo(.profile)
    }
    Button("Go back to root") {
      navigator.goBackToRoot()
    }
  }
}

便捷方法

FlowNavigator(以及原始 FlowPath 或路由数组)交互时,可以使用多种便捷方法来简化导航,包括:

方法 效果
push 将新屏幕推送到堆栈上。
presentSheet 将新屏幕显示为 sheet。†
presentCover 将新屏幕显示为全屏覆盖。†
goBack 返回堆栈中的上一个屏幕。
goBackToRoot 返回堆栈中的第一个屏幕。
goBackTo 返回堆栈中的特定屏幕。
pop 如果已推送,则弹出当前屏幕。
dismiss 关闭最近呈现的屏幕。

† 如果您希望能够从呈现的屏幕推送屏幕,请传递 embedInNavigationView: true

深度链接

在引入 NavigationStack API 之前,SwiftUI 不支持在单个状态更新中推送多个屏幕,例如,当深度链接到导航层次结构中多个层级的屏幕时。FlowStacks 解决了此限制:您可以进行任何此类更改,该库将在后台将较大的更新分解为 SwiftUI 支持的一系列较小的更新,必要时会在两者之间添加延迟。

绑定

可以将 flow 目标配置为使用对其路由数组中屏幕状态的绑定,而不仅仅是只读值 - 只需在 flowDestination 函数的视图构建器闭包中的屏幕参数之前添加 $ 即可。然后,屏幕本身可以负责更新其在路由数组中的状态,例如。

import SwiftUINavigation

struct BindingExampleCoordinator: View {
  @State var path = FlowPath()
    
  var body: some View {
    FlowStack($path, withNavigation: true) {
      FlowLink(value: 1, style: .push, label: { Text("Push '1'") })
        .flowDestination(for: Int.self) { $number in
          EditNumberScreen(number: $number) // This screen can now change the number stored in the path.
        }
    }
  }

如果您使用的是类型化的路由数组,您可能正在使用枚举来表示屏幕,因此可能需要进一步提取该枚举的特定情况的关联值作为绑定。您可以使用 SwiftUINavigation 库来完成此操作,该库包含许多有用的可选和枚举状态绑定转换,例如。

点击展开一个使用绑定到类型化的基于枚举的路由数组中的值的示例
import FlowStacks
import SwiftUI
import SwiftUINavigation

enum Screen: Hashable {
  case number(Int)
  case greeting(String)
}

struct BindingExampleCoordinator: View {
  @State var routes: Routes<Screen> = []

  var body: some View {
    FlowStack($routes, withNavigation: true) {
      HomeView()
        .flowDestination(for: Screen.self) { $screen in
          if let number = Binding(unwrapping: $screen, case: /Screen.number) {
            // Here `number` is a `Binding<Int>`, so `EditNumberScreen` can change its
            // value in the routes array.
            EditNumberScreen(number: number)
          } else if case let .greeting(greetingText) = screen {
            // Here `greetingText` is a plain `String`, as a binding is not needed.
            Text(greetingText)
          }
        }
    }
  }
}

struct HomeView: View {
  @EnvironmentObject var navigator: FlowPathNavigator

  var body: some View {
    VStack {
      FlowLink(value: Screen.number(42), style: .push, label: { Text("Show Number") })
      FlowLink(value: Screen.greeting("Hello world"), style: .push, label: { Text("Show Greeting") })
    }
  }
}

struct EditNumberScreen: View {
  @Binding var number: Int

  var body: some View {
    Stepper(
      label: { Text("\(number)") },
      onIncrement: { number += 1 },
      onDecrement: { number -= 1 }
    )
  }
}

子 flow 协调器

FlowStack 旨在可组合,因此您可以拥有多个 flow 协调器,每个协调器都有自己的 FlowStack,并且您可以从父协调器呈现或推送子协调器。有关更多信息,请参阅 嵌套 FlowStacks

它是如何工作的?

该库通过将路由数组转换为嵌套 NavigationLinks 和呈现调用的层次结构来实现,扩展了 NavigationBackport 中使用的技术。

从早期版本迁移

请参阅 迁移文档


ko-fi