FlowStacks 0.3.6

FlowStacks 0.3.6

John Morgan维护。



FlowStacks

SwiftUI中的协调器模式

FlowStacks 允许您使用一个简单的数组来管理复杂的 SwiftUI 导航和展示流程。这使得您能够将导航状态提升到更高级的协调器中,以便您编写对它们在应用的导航流程中的上下文一无所知的信息化视图。

如果您喜欢这个库,那么:

✅ 您想支持应用中深层嵌套的导航路由的深链接。
✅ 您想能够轻松地在不同的导航上下文中重复使用视图。
✅ 您想容易地返回到根屏幕或导航堆栈中的特定屏幕。
✅ 您想使用协调器模式来将导航逻辑保持在单一位置。
✅ 您想将应用的导航分解成多个可重复使用的协调器并将它们组合在一起。

该库通过将屏幕数组转换为嵌套的NavigationLinks和展示调用层次结构来工作,因此

🚫 它完全不依赖于UIKit。
🚫 它不使用 AnyView 来擦除屏幕类型。
🚫 它不尝试从头开始重新创建NavigationView。

用法

首先,创建一个枚举,包含您的流程可能包含的所有屏幕,例如。

enum Screen {
  case home
  case numberList
  case numberDetail(Int)
}

然后,协调器视图可以管理一个 Route<Screen> 的数组,代表这些屏幕的堆栈,每个屏幕要么是推送要么是展示。在协调器视图的主体中,使用对路由数组的绑定初始化一个 Router,以及一个 ViewBuilder 回调。该回调为特定屏幕构建一个视图,例如。

struct AppCoordinator: View {
  @State var routes: Routes<Screen> = [.root(.home)]
    
  var body: some View {
    Router($routes) { screen, _ in
      switch screen {
      case .home:
        HomeView(onGoTapped: showNumberList)
      case .numberList:
        NumberListView(onNumberSelected: showNumber, cancel: goBack)
      case .numberDetail(let number):
        NumberDetailView(number: number, cancel: goBackToRoot)
      }
    }
  }
    
  private func showNumberList() {
    routes.presentSheet(.numberList, embedInNavigationView: true)
  }
    
  private func showNumber(_ number: Int) {
    routes.push(.numberDetail(number))
  }
    
  private func goBack() {
    routes.goBack()
  }
    
  private func goBackToRoot() {
    routes.goBackToRoot()
  }
}

便捷方法

可以使用标准数组方法管理路由数组,但提供了一些便捷方法来支持常见的转换,例如。

方法 效果
push 将新的屏幕推入堆栈。
presentSheet 以页面的形式展示新的屏幕。†
presentCover 以全屏覆盖的形式展示新的屏幕。†
goBack 在堆栈中返回一个屏幕。
goBackToRoot 返回堆栈中的第一个屏幕。
goBackTo 返回堆栈中的特定屏幕。
pop 弹出当前屏幕(如果它是推送的)。
dismiss 取消展示最新展示的屏幕。

如果要从展示的屏幕中推送屏幕,请传递 embedInNavigationView: true

路由数组自动更新

如果用户点击返回按钮,路由数组将自动更新以反映新的导航状态。使用边缘滑动手势或通过点击返回按钮的长按手势导航回也将自动更新路由数组,同样,滑动以取消展示页面也会这样做。

FlowNavigator

上述示例显示了如何传递闭包到屏幕视图,以显示新屏幕和回退。然而,如果需要通过多个视图层传递闭包,它们会很快变得难以管理。取而代之的是,环境提供了一个FlowNavigator对象,可以访问当前路由数组,并通过所有便利方法来更新它。它可以通过环境从路由内的任何视图访问,例如:

@EnvironmentObject var navigator: FlowNavigator<ScreenType>

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

绑定

路由器可以被配置为与屏幕状态绑定而不是仅提供一个只读值——只需在视图构建器闭包中的屏幕参数前添加$符号。屏幕本身可以负责更新路由数组中的状态。通常使用枚举来表示屏幕,因此可能需要进一步提取与特定屏幕关联的绑定值。您可以使用SwiftUINavigation库来完成此操作,它包含一系列为可选和枚举状态提供帮助的绑定转换,例如:

import SwiftUINavigation

struct BindingExampleCoordinator: View {
  enum Screen {
    case start
    case number(Int)
  }
  
  @State var routes: Routes<Screen> = [.root(.start, embedInNavigationView: true)]
    
  var body: some View {
    Router($routes) { $screen, _ in
      if let number = Binding(unwrapping: $screen, case: /Screen.number) {
        // Here number is a Binding<Int>, so EditableNumberView can change its
        // value in the routes array.
        EditableNumberView(number: number)
      } else {
        StartView(goTapped: goTapped)
      }
    }
  }
  
  func goTapped() {
    routes.push(.number(42))
  }
}

子协调器

协调器只是视图本身,因此它们可以显示、推送到、添加到TabViewWindowGroup中,并以所有常规方式配置。它们甚至可以将它们推送到父协调器的导航堆栈上,允许您将导航流程的各个部分分开成独立的子协调器。在这种情况下,最好始终将子协调器置于父路由堆栈的顶部,因为它将承担推送和显示新屏幕的责任。否则,如果子协调器已经在推送屏幕,父协调器可能会尝试推送屏幕,导致冲突。

使用视图模型

在协调器模式中使用Router在处理视图模型时也运作得很好。在这些情况下,导航状态可以位于协调器自己的视图模型中,而屏幕枚举可以包括每个屏幕的视图模型。使用视图模型时,上面的第一个例子可以重写为:

enum Screen {
  case home(HomeViewModel)
  case numberList(NumberListViewModel)
  case numberDetail(NumberDetailViewModel)
}

class AppCoordinatorViewModel: ObservableObject {
  @Published var routes: Routes<Screen>
    
  init() {
    self.routes = [.root(.home(.init(onGoTapped: showNumberList)))]
  }
    
  func showNumberList() {
    routes.presentSheet(.numberList(.init(onNumberSelected: showNumber, cancel: goBack)), embedInNavigationView: true)
  }
    
  func showNumber(_ number: Int) {
    routes.push(.numberDetail(.init(number: number, cancel: goBackToRoot)))
  }
    
  func goBack() {
    routes.goBack()
  }
    
  func goBackToRoot() {
    routes.goBackToRoot()
  }
}

struct AppCoordinator: View {
  @ObservedObject var viewModel: AppCoordinatorViewModel
    
  var body: some View {
    Router($viewModel.routes) { screen in
      switch screen {
      case .home(let viewModel):
        HomeView(viewModel: viewModel)
      case .numberList(let viewModel):
        NumberListView(viewModel: viewModel)
      case .number(let viewModel):
        NumberView(viewModel: viewModel)
      }
    }
  }
}

制作复杂的导航更新

SwiftUI不允许在一次更新中推送、显示或关闭多于一个屏幕。这使得对导航状态进行大量更新变得复杂,例如直接深度链接到导航层次结构中的视图、回退多层以到达根视图或恢复任意导航状态时。使用FlowStacks,您可以将此类更改包裹在withDelaysIfUnsupported调用中,并且库将把大更新分解为SwiftUI支持的一系列较小更新。

$routes.withDelaysIfUnsupported {
  $0.goBackToRoot()
}

或者,如果使用视图模型

RouteSteps.withDelaysIfUnsupported(self, \.routes) {
  $0.push(...)
  $0.push(...)
  $0.presentSheet(...)
}

固定根屏幕

在一个屏幕流程中,根屏幕通常保持静态不变 - 总是同一个屏幕位于根位置。在这种情况下,您可以使用根屏幕视图上的showing函数来简化问题。它接受与Router初始化器相同的参数

struct ShowingCoordinator: View {
  enum Screen {
    case detail, edit, confirm
  }
  
  @State var routes: Routes<Screen> = []
  
  var body: some View {
    HomeView(onGoTapped: { routes.presentSheet(.detail) })
      .showing($routes) { $number, index in
        ...
      }
  }
}

它是如何工作的?

这篇博客文章概述了如何将屏幕数组转换为视图和NavigationLink的层次结构。Router使用类似方法允许导航和显示。

注意事项

目前仅支持.stack导航视图样式。与其他导航视图样式(如.column)有一些意外的行为,使得它不适用于本库使用的方法。

请确保您的屏幕不会意外地观察到导航状态,例如,如果您向屏幕传递协调器对象作为ObservableObjectEnvironmentObject。这不仅会导致在导航状态更改时屏幕不必要的重新渲染,而且还可能导致SwiftUI的导航状态与您的应用状态不一致。

使用组合架构?

请参见TCACoordinators,它使用FlowStacks帮助在TCA中进行导航。