受 SwiftUI 启发,为所有 Swift 平台带来简单而强大的导航工具。
这个库包含一系列工具,它们构成了为 Apple 平台(如 SwiftUI、UIKit 和 AppKit)以及非 Apple 平台(如 Windows、Linux、Wasm 等)构建强大的状态管理和导航 API 的基础。
SwiftNavigation 库构成了可以构建更高级工具的基础,例如 UIKitNavigation 和 SwiftUINavigation 库。 主要提供两个工具:
observe
:以最小的代价观察模型中的更改。UIBinding
:双向绑定,用于将导航和 UI 组件连接到可观察的模型。除了这些工具之外,还有一些补充概念,允许您构建更强大的工具,例如 UITransaction
,它将动画和其他数据与状态更改相关联;以及 UINavigationPath
,它是一种类型擦除的数据堆栈,有助于描述基于堆栈的导航。
所有这些工具都构成了如何为 SwiftUI、UIKit、AppKit 甚至非 Apple 平台构建更强大和更可靠的工具的基础。
SwiftUI 已经配备了非常强大的导航 API,但在某些方面仍存在不足。 特别是从枚举状态驱动导航,以便您可以获得编译时保证,即一次只能激活一个目的地。
例如,假设您有一个功能,可以显示一个用于创建项目的 Sheet,深入到用于编辑项目的视图,并且可以显示一个用于确认删除项目的 Alert。 从技术上讲,可以用 3 个单独的可选项来建模:
@Observable
class FeatureModel {
var addItem: AddItemModel?
var deleteItemAlertIsPresented: Bool
var editItem: EditItemModel?
}
然后在视图中,可以使用 sheet
、navigationDestination
和 alert
视图修饰符来描述导航类型。
.sheet(item: $model.addItem) { addItemModel in
AddItemView(model: addItemModel)
}
.alert("Delete?", isPresented: $model.deleteItemAlertIsPresented) {
Button("Yes", role: .destructive) { /* ... */ }
Button("No", role: .cancel) {}
}
.navigationDestination(item: $model.editItem) { editItemModel in
EditItemModel(model: editItemModel)
}
一开始这很好,但也会给您的功能带来很多不必要的复杂性。 这 3 个可选项意味着技术上有 8 种不同的状态:都可以是 nil
,一个是 非-nil
,两个可以是 非-nil
,或者全部三个都可以是 非-nil
。 但只有 4 种状态有效:要么都是 nil
,要么只有一个是 非-nil
。
通过允许其他这 4 种无效状态,我们可能会意外地告诉 SwiftUI 同时显示 Sheet 和 Alert,但这在 SwiftUI 中是无效的,并且 SwiftUI 甚至会在控制台中打印一条消息,告知您将来它实际上可能会导致您的应用程序崩溃。
幸运的是,Swift 提供了处理这种情况的完美工具:枚举! 它们允许您简洁地定义一种类型,该类型可以是多种情况之一。 因此,我们可以将 3 个可选项重构为具有 3 个 case 的枚举,然后保留一块可选状态:
@Observable
class FeatureModel {
var destination: Destination?
enum Destination {
case addItem(AddItemModel)
case deleteItemAlert
case editItem(EditItemModel)
}
}
这更简洁,并且我们获得了编译时验证,即最多可以同时激活一个目的地。 但是,SwiftUI 没有提供从该模型驱动导航的工具。 这就是 SwiftUINavigation 工具变得有用的地方。
重要提示
要访问下面描述的工具,您必须依赖 SwiftNavigation 包(请参阅 安装)并导入 SwiftUINavigation 库。
我们首先使用 @CasePathable
宏注释 Destination
枚举,这允许人们使用点语法引用枚举的 case,就像对结构和属性所做的那样:
+import SwiftNavigation
+@CasePathable
enum Destination {
// ...
}
现在,可以使用简单的点链语法从 destination
属性的特定 case 派生绑定:
import SwiftUINavigation
// ...
.sheet(item: $model.destination.addItem) { addItemModel in
AddItemView(model: addItemModel)
}
.alert("Delete?", isPresented: Binding($model.destination.deleteItemAlert)) {
Button("Yes", role: .destructive) { /* ... */ }
Button("No", role: .cancel) {}
}
.navigationDestination(item: $model.destination.editItem) { editItemModel in
EditItemView(model: editItemModel)
}
注意
对于 alert,我们使用的是特殊的 Binding
初始化程序,它将 Binding<Void?>
转换为 Binding<Bool>
。
现在,我们有一种简洁的方式来描述一个功能可以导航到的所有目的地,并且我们仍然可以使用 SwiftUI 的导航 API。
重要提示
要访问下面描述的工具,您必须依赖 SwiftNavigation 包并导入 UIKitNavigation 库。
与 SwiftUI 不同,UIKit 没有配备状态驱动的导航工具。 它的导航工具是“即发即弃”的,这意味着您只需调用一个方法来触发导航,但在您功能的状态中没有任何表示。
例如,要从按钮按下显示一个 Sheet,您可以简单地执行:
let button = UIButton(type: .system, primaryAction: UIAction { [weak self] _ in
present(SettingsViewController(), animated: true)
})
这使得开始导航变得容易,但是正如 SwiftUI 教会我们的那样,能够从状态驱动导航是非常强大的。 它允许您将更多的功能逻辑封装在一个隔离且可测试的域中,并且它可以免费解锁深度链接,因为只需要构造一块代表您想要导航到的位置的状态,将其交给 SwiftUI,然后让它处理剩下的事情。
UIKitNavigation 库为 UIKit 带来了一套强大的导航工具,这些工具深受 SwiftUI 的启发。 例如,如果您有一个像上面 SwiftUI 部分讨论的功能模型:
import SwiftNavigation
@Observable
class FeatureModel {
var destination: Destination?
@CasePathable
enum Destination {
case addItem(AddItemModel)
case deleteItemAlert
case editItem(EditItemModel)
}
}
……那么可以使用库中的工具在视图控制器中驱动导航:
import UIKitNavigation
class FeatureViewController: UIViewController {
@UIBindable var model: FeatureModel
func viewDidLoad() {
super.viewDidLoad()
// Set up view hierarchy
present(item: $model.destination.addItem) { addItemModel in
AddItemViewController(model: addItemModel)
}
present(isPresented: UIBinding($model.destination.deleteItemAlert)) {
let alert = UIAlertController(title: "Delete?", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .destructive))
alert.addAction(UIAlertAction(title: "No", style: .cancel))
return alert
}
navigationDestination(item: $model.destination.editItem) { editItemModel in
EditItemViewController(model: editItemModel)
}
}
}
通过使用库的导航工具,我们可以保证模型将与视图保持同步。 当状态变为 非-nil
时,将触发相应的导航形式,并且当呈现的视图被解除时,状态将为 nil
。
SwiftUI 的另一个强大之处在于,它能够在其可观察模型中的状态发生更改时更新其 UI。 并且由于 Swift 的观察工具,这可以隐式且最小地完成:无论在视图的 body
中访问哪些字段都会被自动跟踪,以便在它们更改时视图会更新。
我们的 UIKitNavigation 库附带一个工具,可以将这种能力带到 UIKit,它被称为 observe
:
observe { [weak self] in
guard let self else { return }
countLabel.text = "Count: \(model.count)"
factLabel.isHidden = model.fact == nil
if let fact = model.fact {
factLabel.text = fact
}
activityIndicator.isHidden = !model.isLoadingFact
}
无论在 observe
内部访问哪些字段(例如上面的 count
、fact
和 isLoadingFact
)都会被自动跟踪,以便每当它们发生变化时,都会再次调用 observe
的尾随闭包,从而允许我们使用最新的数据更新 UI。
所有这些工具都建立在 Swift 强大的 Observation 框架之上。 但是,该框架仅适用于较新版本的 Apple 平台:iOS 17+、macOS 14+、tvOS 17+ 和 watchOS 10+。 但是,感谢我们对 Swift 观察工具的反向移植(请参阅 Perception),您可以立即使用我们的工具,一直追溯到 iOS 13 时代的平台。
此库提供的工具还可以构成构建非 Apple 平台(如 Windows、Linux、Wasm 等)的导航工具的基础。 我们目前没有提供任何此类工具,但可以在外部构建它们。
例如,在 Wasm 中,可以使用 observe(isolation:_:)
函数来观察模型的更改并更新 DOM:
import JavaScriptKit
var countLabel = document.createElement("span")
_ = document.body.appendChild(countLabel)
let token = observe {
countLabel.innerText = .string("Count: \(model.count)")
}
并且可以从状态驱动导航,例如 alert:
alert(isPresented: $model.isShowingErrorAlert) {
"Something went wrong"
}
并且您可以构建更高级的工具来呈现和解除浏览器中的 <dialog>
。
此仓库附带了许多示例,用于演示如何使用该库解决常见且复杂的导航问题。 查看 这个 目录以查看所有这些示例,包括:
Swift Navigation 的工具的动机和设计来自 Point-Free 上的许多剧集,这是一个探索函数式编程和 Swift 语言的视频系列,由 Brandon Williams 和 Stephen Celis 主持。
您可以 在此处 观看所有剧集。
如果您想讨论这个库或者对如何使用它来解决特定问题有疑问,可以在许多地方与其他 Point-Free 爱好者进行讨论:
您可以通过将 Swift Navigation 添加为包依赖项来将其添加到 Xcode 项目中。
如果您想在 SwiftPM 项目中使用 Swift Navigation,只需将其添加到 Package.swift
中的 dependencies
子句中即可:
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-navigation", from: "2.0.0")
]
Swift Navigation API 的最新文档可在 此处 获得。
该库是在 MIT 许可证下发布的。 有关详细信息,请参阅 LICENSE。