NavigationStack for SwiftUI
NavigationStack 是一个用于在视图之间导航的 SwiftUI 自定义解决方案。这是一个比 SwiftUI 自带的导航更灵活的替代方案。
优点
与 SwiftUI 的 NavigationView
/ NavigationLink
和 .sheet
修饰符相比
- 使用不同的过渡动画(不仅仅是水平的推送/弹出或垂直的出现/消失)
- 使用各种内置过渡动画之一
- 或者创建您自己的自定义过渡动画
- 如果您愿意,甚至可以在没有任何过渡动画的情况下进行导航
- 在返回时,可以立即定义返回过渡动画,而不是在向前导航时预先定义
- 可以一次导航多个屏幕,而不仅仅是上一个屏幕
- 在 iOS 13 上也可以使用全屏出现过渡
过渡示例
使用 SwiftUI 的默认过渡动画
或使用一些默认视图动画进行过渡
或者编写您自己的自定义过渡动画
安装
CocoaPods
通过 CocoaPods 包管理器安装,请在 Podfile
中添加
pod 'NavStack'
Carthage
通过 Carthage 安装,请在 Cartfile
中添加
github "indieSoftware/NavigationStack"
SPM
通过 SwiftPackageManager 包管理器安装,请添加仓库
https://github.com/indieSoftware/NavigationStack.git
使用方法
-
将库导入到视图的源文件中。
-
将
NavigationModel
作为环境对象包含到视图中。 -
使用
NavigationStackView
作为视图的根堆栈视,并给它一个唯一的名称以便引用。 -
使用
NavigationModel
对象执行任何过渡操作,例如推送或弹出行,提供NavigationStackView
的标识符以定义应切换内容的哪个NavigationStackView
。import NavigationStack // 1 struct MyRootView: View { @EnvironmentObject var navigationModel: NavigationModel // 2 var body: some View { NavigationStackView("MyRootView") { // 3 Button(action: { navigationModel.pushContent("MyRootView") { // 4 MyDetailView() } }, label: { Text("Push MyDetailView") }) } } }
-
由于需要引用
NavigationModel
实例,您必须在视图层次结构中将一个作为环境对象附加,例如在SceneDelegate
中。func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) let myRootView = MyRootView() .environmentObject(NavigationModel()) // 5 window.rootViewController = UIHostingController(rootView: myRootView) self.window = window window.makeKeyAndVisible() } }
这就是您需要的全部内容!
附加信息
《NavigationStackView》的标识符
每个应该提供切换到不同视图的可能性的视图,都需要包含一个唯一的标识符的《NavigationStackView》。
当您导航到的视图现在想要导航回特定视图时,只需告诉《NavigationModel》要导航回哪一个。
import NavigationStack
struct MyDetailView: View {
@EnvironmentObject var navigationModel: NavigationModel
var body: some View {
Button(action: {
navigationModel.popContent("MyRootView")
}, label: {
Text("Pop to root")
})
}
}
使用《NavigationStackView》的参考标识符,您还可以一次导航回多个屏幕,只需提供更深层级中一个屏幕的ID。
为了避免为《NavigationStackView》ID使用硬编码字符串,只需在每个视图中定义一个静态常量,然后可以进行引用。
struct ContentView1: View {
static let id = String(describing: Self.self)
@EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationStackView(ContentView1.id) {
Button(action: {
navigationModel.pushContent(ContentView1.id) {
ContentView2()
}
}, label: {
Text("Push ContentView2")
})
}
}
}
方便导航方法
您还可以使用其他模型方法中可能更适合您当前情况的方法,例如使用hideTopViewWithReverseAnimation()
只需过渡回上一个屏幕,并使用在切换到屏幕时的反向动画。
有一些方便的方法可以表达更合适的特定切换,例如pushContent
、popContent
、presentContent
和dismissContent
。请参阅NavigationModel的文档。
转换动画
方便导航方法都依赖于带动画参数的《showView(_ identifier:, animation:, alternativeView:)》和《hideView(_ identifier:, animation:)》方法。方便方法使用预定义的导航动画(例如《NavigationAnimation》的push
、pop
、present
和dismiss
)。但是,您还可以通过提供不同的动画曲线和转换类型参数来创建自己的过渡动画。然后您可以使用显示和隐藏方法来使用自己的动画创建转换。
let myAnimation = NavigationAnimation(
animation: .easeOut,
defaultViewTransition: .static,
alternativeViewTransition: .brightness()
)
navigationModel.hideView("MyRootView", animation: myAnimation)
使用 "defaultView",指的是源视图,即用于导航的视图;而 "alternativeView" 指的是目标视图,即用于导航到的视图。通过为这两个视图提供不同的过渡动画,可以定义一个用于离开视图的过渡动画和用于出现视图的不同过渡动画。
重要:对于在过渡期间不应该进行动画的视图,例如在另一个视图进行动画时保持静态可见,您必须提供一个 .static
过渡,而不是 SwiftUI 的 .identity
。
该库提供的所有过渡动画的列表可以在库的文档中找到,具体请参考 AnyTransition 扩展。
自定义过渡
要创建自己的过渡动画,请简单地创建一个自定义的 ViewModifier
,并根据需要扩展 AnyTransition
以创建便利方法。
public struct BrightnessModifier: ViewModifier {
public let amount: Double
public func body(content: Content) -> some View {
content.brightness(amount)
}
}
public extension AnyTransition {
static func brightness(_ amount: Double = 1) -> AnyTransition {
.modifier(active: BrightnessModifier(amount: amount), identity: BrightnessModifier(amount: 0))
}
}
重要:请注意,SwiftUI 只有在值发生变化时才会执行过渡动画,例如,在提供亮度为 0 的亮度过渡时,您将看不到任何动画,并且会跳过该视图的过渡。这是因为身份也有亮度 0 的值,因此等价于身份和活动状态。
有关更多信息,请参阅库的 API 文档:https://indiesoftware.github.io/NavigationStack
示例代码
该项目包含一个示例目标(NavigationStackExample
),它展示了如何使用该库。只需克隆仓库,打开工作区并将其切换到同名方案。示例应用程序展示了如何推送多个视图,如何直接回到根视图,如何模拟模态呈现以及展示了现有的不同过渡动画。示例视图经过良好的注释,并位于 Sources/NavigationStackExample/ExampleViews
组中。
您还可以运行示例的 UI 测试以自动运行特定的或所有过渡。
示例项目包含一些实验,每个实验都描述了一个问题或尝试解决的问题。它们相当于解决一些 SwiftUI 稍微不同的行为的方法指南。对于库用户来说这些不重要,但对那些想要理解和改进库的人来说可能很重要。因此,请随意跳过这些实验。但是,要运行实验,只需在方案中将实验名称作为启动参数传递,例如,要运行 Experiment1.swift
,请将方案的信息传递为 "Experiment1"。
已知限制
手动全屏
NavigationStackView
只是一个另一种堆叠视图。您可以在这个堆叠视图中放入任何东西,然而,您应该确保内容充满整个屏幕。否则,您可能会引发子视图过渡,即使在没有这么做的情况下,如果屏幕的过渡不是全屏的,那么外观可能会显得有些尴尬。
无子视图过渡
技术上,您不是必须将 NavigationStackView
添加到视图主体的根级别。即使在将自定义视图添加到自定义视图的根的情况下,您也可以将此自定义视图作为子视图添加到更大的视图中。只要视图是全屏的,这实际上没有问题。
然而,当要导航到的视图不是全屏时,用户可能会从一个已经过渡到不同视图的屏幕中过渡。这可能会导致问题,因为lib使用线性堆栈来保持导航过渡。如果尝试从堆栈的中间分支,这不会按预期工作。
示例中有 SubviewExample
和 SubviewExampleTests
来展示这个问题。
无真正的模态
SwiftUI的sheet
修饰符实际上触发真实的模态过渡。这个库并没有这样做,相反,NavigationStackView
依赖于单页架构,这意味着始终只有一个视图可见,但其内容将在过渡时被替换。我不确定这是否真的是一个限制,但这是需要注意的一点。
故障排除
致命错误:没有找到类型为 NavigationModel 的 ObservableObject。可能缺少 NavigationModel 的祖先 View.environmentObject(_:)。
将导航模型添加到视图层次结构中,例如 MyRootView().environmentObject(NavigationModel())
。