-
- 2.1. Approach 1
- 2.2. Approach 2
- 2.3. Which approach is best?
大型、复杂的应用程序依赖于健壮而灵活的应用程序架构。在本文中,我们将向您展示如何实现这个目标,以及如何使用SwiftUI处理navigation和deep-linking。
随着越来越多的人使用SwiftUI构建产品应用,我们最近开始尝试、讨论和比较不同的应用架构。在之前的一篇博客文章中,我们比较了三种截然不同的应用架构方法:
- Model-View
- Redux
- and Model-View-ViewModel (MVVM)
在另一篇博客文章中,我们展示了如何在SwiftUI中应用协调器模式,这种模式在UIKit开发人员中非常流行。
在本文中,让我们看看在navigation and deep linking方面从Coordinator模式获得了什么。
除了这篇文章,我们还开放了XUI库,它允许你在SwiftUI中构建MVVM-C应用程序架构。它提供了对我们将在本文中介绍的所有代码组件的访问,以及大量有用的Combine和SwiftUI扩展。
协调器模式的好处
Coordinator模式从现有视图和视图模型中提取转场逻辑以及视图模型的创建和组织。它使我们的视图模型耦合更少。 因此,视图可以在不同的视图上下文中重用,例如,我们可以很容易地将它们从TabView移动到NavigationView,而不需要改变该视图的内部代码。 这极大地提高了视图和视图模型的灵活性和可维护性。
改进
通过我们之前介绍的实现协调器模式的方法,我们已经实现了其中的大部分好处。 但这种脱钩还没有完全完成。现在我们想让这个概念变得更加强大,通过允许视图模型被协议抽象。我们将允许直接访问协调器层次结构的不同组件来实现深度链接,并在处理多个ObservableObject实例时提供常见问题的解决方案。
1. Protocol Abstraction
在我们的项目中,我们希望将业务逻辑从视图中解耦。 这就是我们使用视图模型的原因。然而,一个视图(MyView)总是引用一个视图模型(MyViewModel),所以如果我们想在不同的上下文中使用同一个视图,我们必须复制视图以更改视图模型类型(MyViewModel→MyOtherViewModel)。
所以在给定视图和特定视图模型之间仍然有一个紧密的耦合我们需要摆脱。通常,我们会为此编写一个协议,以屏蔽视图模型的具体类型。不幸的是,以下代码无法编译:
protocol SomeViewModel: ObservableObject {
// ...
}
struct SomeView: View {
@ObservedObject var viewModel: SomeViewModel
}
在SwiftUI中使用协议作为视图模型有两个问题:
- ObservableObject有一个相关联的类型ObjectWillChangePublisher,我们可以在它的声明中看到:
protocol ObservableObject: AnyObject {
associatedtype ObjectWillChangePublisher: Publisher = ObservableObjectPublisher
where Self.ObjectWillChangePublisher.Failure == Never
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
由于SomeViewModel符合ObservableObject,它继承了相关的类型要求。 因此,它只能用作通用约束,而不能用作具体类型。
- ObservedObject(用于ObservableObject属性的属性包装器)有一个通用的约束,要求强制我们指定一个具体的类或结构,一般不允许协议。 这只是一个语言限制。
好吧,那我们能做些什么?
1.1. Option 1: Generic Constraint
首先想到的是泛型:
struct SomeView<ViewModel: SomeViewModel>: View {
@ObservedObject var viewModel: ViewModel
}
嗯,没那么简单。这仍然会迫使我们在创建SomeView时知道SomeViewModel的具体类型。 然而,在我们的协调器方法中,视图只知道视图模型遵循的协议,而不知道具体的类型。
我们可以通过创建视图的方法来扩展视图模型协议,但为每一个视图编写代码并不实际。
extension SomeViewModel {
var someView: some View {
SomeView(viewModel: self)
}
}
在这个扩展中,编译器知道视图模型的具体类型,因此,我们可以像平常一样创建视图。
1.2. Option 2: The Store Property Wrapper
我们可以创建自己的属性包装器,而不是使用ObservedObject属性包装器。在新的XUI库中,我们定义了Store属性包装器,可以类似于ObservedObject使用:
protocol SomeViewModel: AnyObservableObject {
// ...
}
struct SomeView {
@Store var viewModel: SomeViewModel
}
这正是我们想要的,不是吗?
你可能已经注意到,我们的视图模型协议不再要求与ObservableObject一致,而是要求与AnyObservableObject一致。
protocol AnyObservableObject: AnyObject {
var objectWillChange: ObservableObjectPublisher { get }
}
AnyObservableObject是我们创建的没有关联类型要求的协议。相反,它需要与ObservableObject类具有相同名称和具体类型的属性。问题1解决了!✅
现在,剩下的唯一问题是,我们必须为包装在ObservedObject中的属性指定具体类型。为此目的,让我们再来看看Store属性包装器:
它允许我们使用协议作为泛型类型(例如,你可以创建一个Store
在本文中,我们不想深入探讨Store属性包装器的实现细节,但如果您感兴趣,请继续阅读我们的XUI库。
注意:我们也不能使AnyObservableObject符合identifiableobject,因为协议不能符合协议,这就是为什么我们要重新创建不同的视图修饰符来使用
.sheet(model: $coordinator.detailViewModel) { ... }
instead of
.sheet(item: $coordinator.detailViewModel) { ... }
2. Deep Links
有时候,我们希望在特定的状态下打开我们的应用程序,例如,当
- 用户点击通知
- 应用程序是通过URL打开的
- 用户希望使用Handoff功能在不同设备之间切换同一个应用程序。
在所有这些场景中,我们都希望在启动时执行多个转换,以便导航到特定的UI状态以显示所请求的信息。
通常,深度链接是使用整个应用程序中复杂的、难以跟踪的命令链实现的。 您团队中的新开发人员(甚至几个月后的您自己)将很难理解何时执行该链中的特定方法,以及是否真的需要它。
但通常情况下,用户可以通过手动一步一步地通过应用程序到达任何深度链接的目标视图,所以为什么我们不直接通过在协调器对象和视图模型上触发相同的操作来模拟这些步骤呢?
我们想向您介绍两种方法。
让我们假设如下:我们有一个用于食谱的应用程序(我们在上一篇文章中介绍过的应用程序),它显示两个带有食谱列表的选项卡。一个选项卡只显示素食食谱,而另一个选项卡只显示非素食食谱。对于每个菜谱,您可以在详细视图中打开准备说明,然后从那里,您可以显示一个带有菜谱评级的模态表。我们现在想要这个应用程序支持打开一个特定的食谱的URL评级。
2.1. Approach 1
在这个例子中,我们假设我们已经知道应用应该显示哪个菜谱配方的评级。(将深链接URL映射到特定配方的逻辑取决于您使用的URL方案,与本文无关。)
class DefaultHomeCoordinator: HomeCoordinator, ObservableObject {
// ...
func openRatings(for recipe: Recipe) {
tab = recipe.isVegetarian ? .veggie : .meat
let recipeListCoordinator: RecipeListCoordinator = recipe.isVegetarian ? veggieCoordinator : meatCoordinator
recipeListCoordinator.open(recipe)
let recipeViewModel: RecipeViewModel? = recipeListCoordinator.detailViewModel
recipeViewModel?.openRatings()
}
}
太好了,我们找到了解决方案!但不要这么快,因为我们也创建了一些关于应用程序的整体结构的限制:
在某些情况下,我们可能只知道想要触发哪些动作,而不知道每个协调器对象或视图模型在内部做什么。 根据要显示给用户的模型对象,协调器可能有许多不同的子视图模型或协调器对象。如果我们可以只搜索符合某种类型和/或满足某种要求的任何对象,事情就会简单得多。
2.2. Approach 2
为了能够在协调器/视图模型层次结构中搜索对象,我们需要使该层次结构对开发人员可见。我们通过创建一个简单的协议DeepLinkable来做到这一点。
protocol DeepLinkable {
var children: [DeepLinkable] { get }
}
有了这个协议,我们现在可以很容易地看到,我们的应用程序的结构非常类似于树层次结构,其中协调器对象充当分支,子协调器充当更小的分支,视图模型充当它们的叶子。每个协调器现在都可以实现children属性来提供对其子程序的访问。 (提示:当使用协议抽象时,你可以在协议上写一个扩展。默认实现简单地不返回子节点。)
对于我们的食谱应用程序,它的层次结构是这样的:

现在,我们实现了一个宽度优先搜索,以方便提取层次结构中的特定对象。度优先搜索在树中逐层查找协调器。与每个视图模型或协调器对象相关联的数字对应于遍历每个协调器对象或视图模型的顺序。
让我们通过翻译上面的代码示例来演示如何实际使用搜索,以便使用DeepLinkable上定义的firstReceiver方法。
class DefaultHomeCoordinator: HomeCoordinator, ObservableObject {
// ...
func openRatings(for recipe: Recipe) {
tab = recipe.isVegetarian ? .veggie : .meat
let recipeListCoordinator = firstReceiver(as: RecipeListCoordinator.self, where: {
$0.filter(recipe)
})
recipeListCoordinator.open(recipe)
let recipeViewModel = firstReceiver(as: RecipeViewModel.self, where: {
$0.recipe.id == recipe.id
})
recipeViewModel.openRatings()
}
}
如您所见,我们不再需要了解应用程序的整个结构,而是可以通过简单地了解所需对象的类型来实现快捷方式。
警告:深度链接仍然会对应用程序的部分结构做出假设,一旦在稍后的开发或维护期间引入更改,就可能会中断。在这些情况下,您要么需要适应深链接,要么使用方法1。
2.3. Which approach is best?
如您所见,在SwiftUI中有不同的方法来实现与协调器的深度链接。 使用协调器接口来获取独立的子协调器和视图模型,我们可以创建安全的深度链接。然而,当一个属性被重命名或结构被最小程度地操纵时,我们将需要适应这些深链接。
通过将协调者层次结构理解为一个图表,我们可以以牺牲安全为代价,使我们的深层联系变得更加灵活。自动化测试允许最大的灵活性,同时确保深层链接仍然正常工作。
3. Conclusion
在本文中,我们已经看到了协调器给我们的应用程序带来了很多好处,以及我们如何使用协议抽象和实现深度链接来让我们的协调器变得更加强大。我们在新的开源框架XUI中提供了许多组件(甚至更多!)。XUI简化了MVVM和MVVM- c架构的处理,并为swifttui和Combine提供了一些有用的扩展。