版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2020.04.27 星期一 |
前言
前面写了那么多篇主要着眼于局部问题的解决,包括特定功能的实现、通用工具类的封装、视频和语音多媒体的底层和实现以及动画酷炫的实现方式等等。接下来这几篇我们就一起看一下关于iOS系统架构以及独立做一个APP的架构设计的相关问题。感兴趣的可以看上面几篇。
1. 架构之路 (一) —— iOS原生系统架构(一)
2. 架构之路 (二) —— APP架构分析(一)
3. 架构之路 (三) —— APP架构之网络层分析(一)
4. 架构之路 (四) —— APP架构之工程实践中网络层的搭建(二)
开始
首先看下主要内容:
在本教程中,您将了解如何在
SwiftUI和Combine中使用VIPER体系结构模式,同时构建一个允许用户创建公路旅行的iOS应用程序,来自翻译。
下面看下写作环境
Swift 5, iOS 13, Xcode 11
接着就是正文了。
VIPER架构模式是MVC或MVVM的另一种选择。虽然SwiftUI和Combine框架创建了一个强大的组合,可以快速构建复杂的ui和在应用程序中移动数据,但它们也面临着各自的挑战和对架构的看法。
人们普遍认为所有的应用逻辑都应该进入SwiftUI视图,但事实并非如此。
VIPER为这种情况提供了一种替代方案,可以与SwiftUI和Combine结合使用,帮助构建具有清晰架构的应用程序,该架构有效地分离了所需的不同功能和职责,如用户界面、业务逻辑、数据存储和网络。这样就更容易进行测试、维护和扩展。
在本教程中,您将使用VIPER体系结构模式构建一个应用程序。这款应用也被方便地称为VIPER。
它将允许用户通过向一条路线添加路径点来构建公路旅行。在此过程中,您还将了解您的iOS项目中的SwiftUI和Combine。

打开启动项目。这包括一些代码,让你开始:
- 当你构建其他视图时,
ContentView会启动它们。 - 在
Functional views组中有一些帮助视图:一个用于包装MapKit map视图,这是一个特殊的split image视图,由TripListCell使用。你会把这些加到屏幕上。 - 在
Entities组中,您将看到与数据模型相关的类。Trip和Waypoint稍后将作为VIPER架构的Entities。因此,它们只保存数据,不包含任何功能逻辑。 - 在
Data Sources组中,有用于保存或加载数据的辅助函数。 - 如果您喜欢在
WaypointModule组中查看前面的内容。它有一个Waypoint编辑屏幕的VIPER实现。它包含在starter中,因此您可以在本教程结束时完成应用程序。
这个示例使用的是Pixabay,这是一个获得许可的照片共享站点。要将图像拉入应用程序,您需要创建一个免费帐户并获得一个API密钥。
按照以下说明创建一个帐户:https://pixabay.com/accounts/register/。然后,将您的API密钥复制到ImageDataProvider.swift中找到的apiKey变量中。你可以在Search Images的Pixabay API docs中找到它。
如果您现在构建并运行,您将不会看到任何有趣的东西。

然而,在本教程结束时,您将拥有一个功能齐全的道路旅行计划应用程序。
What is VIPER?
VIPER是一种类似MVC或MVVM的体系结构模式,但是它通过单一职责进一步分离了代码。苹果风格的MVC促使开发者将所有的逻辑放到一个UIViewController子类中。像之前的MVVM一样,VIPER试图解决这个问题。
VIPER中的每个字母代表体系结构的一个组件:视图、交互程序、演示程序、实体和路由器(View, Interactor, Presenter, Entity and Router)。
- 视图View是用户界面。这与
SwiftUI的View相对应。 - 交互器Interactor是一个在演示者presenter和数据之间进行中介的类。它从演示者presenter那里获得方向。
- 演示者Presenter是架构的“交通警察”,在视图
view和交互器interactor之间指挥数据,执行用户操作并调用路由器在视图之间移动用户。 - 实体Entity表示应用程序数据。
- 路由器Router处理屏幕之间的导航。这与
SwiftUI不同,在SwiftUI中,视图显示任何新视图。
这种分离来自“Uncle”Bob Martin的Clean Architecture paradigm。

当您查看图表时,您可以看到数据在视图view和实体entities之间流动的完整路径。
SwiftUI有自己独特的做事方式。如果你将VIPER职责映射到域对象将会不同,如果你将它与UIKit应用的教程相比较。
1. Comparing Architectures
人们经常用MVC和MVVM来讨论VIPER,但它与那些模式不同。
-
MVC (Model-View-Controller)是2010年iOS应用程序架构中最常使用的模式。使用这种方法,你在
storyboard中定义View,Controller是一个关联的UIViewController子类。控制器Controller修改视图,接受用户输入并直接与模型交互。控制器Controller因视图逻辑和业务逻辑而膨胀。 -
MVVM是一种流行的体系结构,在
View Model中它将视图逻辑与业务逻辑分离开来。视图模型与模型Model交互。
最大的区别是,视图模型View Model与视图控制器不同,它只有对视图和模型的单向引用。MVVM非常适合SwiftUI。
VIPER更进一步,将视图逻辑与数据模型逻辑分离。只有演示者presenter与视图对话,只有interactor与model (entity)对话。演示者presenter和交互者interactor相互协调。演示者presenter关心的是显示和用户操作,而交互者interactor`关心的是操纵数据。

Defining an Entity
VIPER是这种架构的一个有趣的缩写,但它的顺序不是禁止的。
在屏幕上显示内容的最快方法是从实体entity开始。entity是项目的数据对象。在本例中,主要的entity是Trip,它包含一个路点Waypoints列表,路点是旅程中的各个站点。
这个应用程序包含一个DataModel类,它包含一个旅行列表。该模型使用一个JSON文件来实现本地持久性,但是您可以使用一个远程后端来代替它,而不必修改任何ui级代码。这就是干净体系结构的优点之一:当您更改一个部分(比如持久层)时,它与代码的其他部分是隔离的。
Adding an Interactor
创建一个名为TripListInteractor.swift的新Swift文件。
添加以下代码到文件:
class TripListInteractor {
let model: DataModel
init (model: DataModel) {
self.model = model
}
}
这将创建interactor类并为它分配一个DataModel,稍后您将使用它。
Setting Up the Presenter
现在,创建一个名为TripListPresenter.swift的新Swift文件。这是为presenter类准备的。演示者presenter关心的是向UI提供数据和协调用户操作。
将此代码添加到文件中:
import SwiftUI
import Combine
class TripListPresenter: ObservableObject {
private let interactor: TripListInteractor
init(interactor: TripListInteractor) {
self.interactor = interactor
}
}
这将创建一个presenter类,它引用了interactor。
由于演示者presenter的工作是用数据填充视图,所以您希望从数据模型中公开旅程trips列表。
添加一个新变量到类:
@Published var trips: [Trip] = []
这是用户将在视图中看到的旅行列表。通过使用@Published属性包装器声明它,视图将能够监听属性的变化并自动更新自身。
下一步是将此列表与来自interactor的数据模型同步。首先,添加以下helper属性:
private var cancellables = Set<AnyCancellable>()
这个集合set用于存储Combine subscriptions,因此它们的生存期与类的生存期绑定在一起。这样,任何subscriptions将保持活跃,只要presenter。
在init(interactor:)的末尾添加以下代码:
interactor.model.$trips
.assign(to: \.trips, on: self)
.store(in: &cancellables)
interactor.model.$trips创建一个发布者publisher,用于跟踪对数据模型的trips集合的更改。它的值被分配给这个类自己的trips集合,创建一个链接,当数据模型改变时,保持presenter的trips更新。
最后,此subscription存储在cancellables中,以便您可以在以后清理它。
Building a View
现在需要构建第一个视图View:trip list视图。
1. Creating a View with a Presenter
从SwiftUI视图模板中创建一个新文件,并将其命名为TripListView.swift。
添加以下属性到TripListView:
@ObservedObject var presenter: TripListPresenter
这将presenter链接到视图。接下来,通过更改TripListView_Previews.preview的主体来修复预览:
let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)
现在,替换TripListView.body的内容:
List {
ForEach (presenter.trips, id: \.id) { item in
TripListCell(trip: item)
.frame(height: 240)
}
}
这将创建一个列表List,其中列举演示者presenter的行程trips,并为每个行程生成一个预先提供的TripListCell。

2. Modifying the Model from the View
到目前为止,您已经看到了从entity到interactor的数据流,通过presenter来填充视图view。当将用户操作发送回数据模型时,VIPER模式甚至更有用。
为此,您将添加一个按钮来创建一个新的旅程。
首先,在TripListInteractor.swift类中添加以下内容:
func addNewTrip() {
model.pushNewTrip()
}
这封装了模型的pushNewTrip(),它在trips列表的顶部创建了一个新的Trip。
然后,在TripListPresenter.swift,把这个加到类里:
func makeAddNewButton() -> some View {
Button(action: addNewTrip) {
Image(systemName: "plus")
}
}
func addNewTrip() {
interactor.addNewTrip()
}
这将创建一个带有system + image的按钮,其中包含一个调用addNewTrip()的操作。这将操作转发给interactor,interactor操作数据模型。
返回TripListView.swift,并在List右括号后添加以下内容:
.navigationBarTitle("Roadtrips", displayMode: .inline)
.navigationBarItems(trailing: presenter.makeAddNewButton())
这将按钮和标题添加到导航栏。现在在TripListView_Previews中修改return,如下所示:
return NavigationView {
TripListView(presenter: presenter)
}
这允许您在预览模式下查看导航栏。
恢复实时预览以查看按钮。

3. Seeing It In Action
现在是返回并将TripListView连接到应用程序其余部分的好时机。
打开ContentView.swift,在view主体中,将VStack替换为:
TripListView(presenter:
TripListPresenter(interactor:
TripListInteractor(model: model)))
这将创建视图及其presenter and interactor。现在构建并运行。
点击+按钮将向列表添加一个New Trip。

4. Deleting a Trip
创建旅行的用户可能还希望能够删除它们,以防出错或旅行结束。既然已经创建了数据路径,向屏幕添加额外的操作就很简单了。
在TripListInteractor,添加:
func deleteTrip(_ index: IndexSet) {
model.trips.remove(atOffsets: index)
}
这将从数据模型中的trips集合中删除项。因为它是一个@Published属性,所以UI将自动更新,因为它订阅了更改。
在TripListPresenter,添加:
func deleteTrip(_ index: IndexSet) {
interactor.deleteTrip(index)
}
这将delete命令转发给interactor。
最后,在TripListView中,在ForEach的结束括号后面添加以下内容:
.onDelete(perform: presenter.deleteTrip)
将. ondelete添加到SwiftUI List中的一个项目中,将自动启用滑动操作来删除行为。然后,动作被发送给presenter,整个链条就断开了。
构建并运行,现在您就可以移除旅行了!

Routing to the Detail View
现在是时候添加VIPER的Router部分了。
路由器Router允许用户从旅行列表视图trip list view导航到旅行详细信息视图trip detail view。trip detail视图将显示路线点列表以及路线地图。
用户将能够从此屏幕编辑路线点列表和旅行名称。

1. Setting Up the Trip Detail Screens
在显示细节屏幕之前,您需要创建它。
按照前面的例子,创建两个新的Swift文件:TripDetailPresenter.swift和TripDetailInteractor.swift,以及一个名为TripDetailView.swift的SwiftUI视图。
将TripDetailInteractor的内容设置为:
import Combine
import MapKit
class TripDetailInteractor {
private let trip: Trip
private let model: DataModel
let mapInfoProvider: MapDataProvider
private var cancellables = Set<AnyCancellable>()
init (trip: Trip, model: DataModel, mapInfoProvider: MapDataProvider) {
self.trip = trip
self.mapInfoProvider = mapInfoProvider
self.model = model
}
}
这将为trip detail屏幕的interactor创建一个新类。它与两个数据源交互:一个单独的旅行Trip和来自MapKit的地图信息。还有一个可取消订阅的集合,您稍后将添加它。
然后,在TripDetailPresenter中,将其内容设置为:
import SwiftUI
import Combine
class TripDetailPresenter: ObservableObject {
private let interactor: TripDetailInteractor
private var cancellables = Set<AnyCancellable>()
init(interactor: TripDetailInteractor) {
self.interactor = interactor
}
}
这将创建一个存根presenter,其中包含一个针对interactor和可取消集的引用。您将在稍后对此进行构建。
在TripDetailView中,添加以下属性:
@ObservedObject var presenter: TripDetailPresenter
这将在视图中添加对presenter的引用。
再次获得预览,改变stub为:
static var previews: some View {
let model = DataModel.sample
let trip = model.trips[1]
let mapProvider = RealMapDataProvider()
let presenter = TripDetailPresenter(interactor:
TripDetailInteractor(
trip: trip,
model: model,
mapInfoProvider: mapProvider))
return NavigationView {
TripDetailView(presenter: presenter)
}
}
现在视图将构建,但是预览仍然是“Hello, World!”

2. Routing
在构建细节视图之前,您需要通过trip列表中的router将其链接到应用程序的其余部分。
创建一个名为TripListRouter.swift的新Swift文件。
将其内容设置为:
import SwiftUI
class TripListRouter {
func makeDetailView(for trip: Trip, model: DataModel) -> some View {
let presenter = TripDetailPresenter(interactor:
TripDetailInteractor(
trip: trip,
model: model,
mapInfoProvider: RealMapDataProvider()))
return TripDetailView(presenter: presenter)
}
}
这个类输出一个新的TripDetailView,该视图由一个interactor和presenter填充。router处理从一个屏幕到另一个屏幕的转换,设置下一个视图所需的类。
在命令式UI范例中——换句话说,在UIKit中——路由router将负责显示视图控制器或激活segue。
SwiftUI将所有目标视图声明为当前视图的一部分,并根据视图状态显示它们。要将VIPER映射到SwiftUI,视图现在负责显示/隐藏视图,路由router是一个目标视图生成器,presenter在它们之间进行协调。
在TripListPresenter.swift,将路由router添加为属性:
private let router = TripListRouter()
现在,您已经创建了路由器作为presenter的一部分。
接下来,添加这个方法:
func linkBuilder<Content: View>(
for trip: Trip,
@ViewBuilder content: () -> Content
) -> some View {
NavigationLink(
destination: router.makeDetailView(
for: trip,
model: interactor.model)) {
content()
}
}
这将创建一个指向路由器提供的详细视图的NavigationLink。当您将其放置在NavigationView中时,该链接将成为一个按钮,将destination推送到导航堆栈上。
content块可以是任何一个SwiftUI视图。但在本例中,TripListView将提供一个TripListCell。
切换到TripListView.swift,将ForEach的内容改为:
self.presenter.linkBuilder(for: item) {
TripListCell(trip: item)
.frame(height: 240)
}
它使用来自presenter的NavigationLink,将单元格设置为其内容并将其放入列表中。
构建并运行,现在,当用户点击单元格时,它将把它们路由到“Hello World”TripDetailView。

3. Finishing Up the Detail View
您仍然需要填写一些旅行细节,以便用户可以看到路线并编辑路线点。
首先添加一个旅行标题:
在TripDetailInteractor中,添加以下属性:
var tripName: String { trip.name }
var tripNamePublisher: Published<String>.Publisher { trip.$name }
这只公开了旅行名称的String版本,以及当该名称更改时的的Publisher。
此外,加上以下内容:
func setTripName(_ name: String) {
trip.name = name
}
func save() {
model.save()
}
第一种方法允许presenter更改旅行名称,第二种方法将模型保存到持久层。
现在,转到TripDetailPresenter。添加以下属性:
@Published var tripName: String = "No name"
let setTripName: Binding<String>
它们为视图提供了读取和设置trip名称的入口。
然后,在init方法中添加以下内容:
// 1
setTripName = Binding<String>(
get: { interactor.tripName },
set: { interactor.setTripName($0) }
)
// 2
interactor.tripNamePublisher
.assign(to: \.tripName, on: self)
.store(in: &cancellables)
这段代码:
- 1) 创建一个
binding来设置旅行名称。TextField将在视图中使用它来读写值。 - 2) 将
interactor’s publisher的旅行名分配给presenter的tripName属性。这使值保持同步。
将trip名称分隔成这样的属性允许您同步该值,而不需要创建一个无限循环的更新。
接下来,添加:
func save() {
interactor.save()
}
这增加了一个保存功能,这样用户可以保存任何编辑过的细节。
最后,转到TripDetailView,将body替换为:
var body: some View {
VStack {
TextField("Trip Name", text: presenter.setTripName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding([.horizontal])
}
.navigationBarTitle(Text(presenter.tripName), displayMode: .inline)
.navigationBarItems(trailing: Button("Save", action: presenter.save))
}
VStack现在保存一个用于编辑旅行名的TextField。导航栏修饰符使用presenter发布的tripName来定义标题,因此当用户键入时,它就会更新,而保存按钮则会保存任何更改。
构建并运行,现在,您可以编辑trip标题。

编辑旅行名称后保存,重新启动应用程序后将显示更改。

4. Using a Second Presenter for the Map
向屏幕添加额外的widgets将遵循相同的模式:
- 向
interactor添加功能。 - 通过
presenter连接功能。 - 将
widgets添加到视图。
转到TripDetailInteractor,并添加以下属性:
@Published var totalDistance: Measurement<UnitLength> =
Measurement(value: 0, unit: .meters)
@Published var waypoints: [Waypoint] = []
@Published var directions: [MKRoute] = []
它们提供了关于一次旅行中的路径点的以下信息:作为Measurement的总距离、路径点列表和连接这些路径点的方向列表。
然后,在init(trip:model:mapInfoProvider:)的末尾添加后续订阅:
trip.$waypoints
.assign(to: \.waypoints, on: self)
.store(in: &cancellables)
trip.$waypoints
.flatMap { mapInfoProvider.totalDistance(for: $0) }
.map { Measurement(value: $0, unit: UnitLength.meters) }
.assign(to: \.totalDistance, on: self)
.store(in: &cancellables)
trip.$waypoints
.setFailureType(to: Error.self)
.flatMap { mapInfoProvider.directions(for: $0) }
.catch { _ in Empty<[MKRoute], Never>() }
.assign(to: \.directions, on: self)
.store(in: &cancellables)
它根据旅行路线点的变化执行三个独立的操作。
第一个只是interactor的路点列表的一个副本。第二个使用mapInfoProvider来计算所有路径点的总距离。第三种方法使用相同的数据provider来获得路点之间的方向。
然后,presenter使用这些值向用户提供信息。
转到TripDetailPresenter,添加以下属性:
@Published var distanceLabel: String = "Calculating..."
@Published var waypoints: [Waypoint] = []
视图将使用这些属性。通过在init(interactor:)的末尾添加以下内容,将它们连接起来以跟踪数据更改:
interactor.$totalDistance
.map { "Total Distance: " + MeasurementFormatter().string(from: $0) }
.replaceNil(with: "Calculating...")
.assign(to: \.distanceLabel, on: self)
.store(in: &cancellables)
interactor.$waypoints
.assign(to: \.waypoints, on: self)
.store(in: &cancellables)
第一个订阅获取与interactor的原始距离,并将其格式化以便在视图中显示,第二个复制路点。
5. Considering the Map View
在转向细节视图之前,考虑一下地图视图。这个widget比其他的更复杂。
除了绘制地理特征,该应用还会覆盖每个点的大头针pins和它们之间的路线。
这需要它自己的一组presentation逻辑。您可以使用TripDetailPresenter,或者在本例中,创建一个单独的TripMapViewPresenter。它将重用TripDetailInteractor,因为它共享相同的数据模型,并且是只读read-only视图。
创建一个名为TripMapViewPresenter.swift的新Swift文件。将其内容设置为:
import MapKit
import Combine
class TripMapViewPresenter: ObservableObject {
@Published var pins: [MKAnnotation] = []
@Published var routes: [MKRoute] = []
let interactor: TripDetailInteractor
private var cancellables = Set<AnyCancellable>()
init(interactor: TripDetailInteractor) {
self.interactor = interactor
interactor.$waypoints
.map {
$0.map {
let annotation = MKPointAnnotation()
annotation.coordinate = $0.location
return annotation
}
}
.assign(to: \.pins, on: self)
.store(in: &cancellables)
interactor.$directions
.assign(to: \.routes, on: self)
.store(in: &cancellables)
}
}
在这里,地图presenter公开两个数组来保存annotations and routes。在init(interactor:)中,您将waypoints从interactor映射到MKPointAnnotation对象,以便它们可以作为地图上的大头针显示。然后将directions复制到routes数组。
要使用presenter,创建一个名为TripMapView.swift的SwiftUI View。将其内容设置为:
import SwiftUI
struct TripMapView: View {
@ObservedObject var presenter: TripMapViewPresenter
var body: some View {
MapView(pins: presenter.pins, routes: presenter.routes)
}
}
#if DEBUG
struct TripMapView_Previews: PreviewProvider {
static var previews: some View {
let model = DataModel.sample
let trip = model.trips[0]
let interactor = TripDetailInteractor(
trip: trip,
model: model,
mapInfoProvider: RealMapDataProvider())
let presenter = TripMapViewPresenter(interactor: interactor)
return VStack {
TripMapView(presenter: presenter)
}
}
}
#endif
它使用了辅助MapView,并从presenter那里为它提供了pins and routes。previews结构构建的VIPER的应用程序需要预览只是地图。使用实时预览(Live Preview)查看地图正确:

要将地图添加到应用程序,首先将以下方法添加到TripDetailPresenter:
func makeMapView() -> some View {
TripMapView(presenter: TripMapViewPresenter(interactor: interactor))
}
这将生成一个地图视图,并为其提供presenter。
接下来,打开TripDetailView.swift。
将以下内容添加到TextField下面的VStack:
presenter.makeMapView()
Text(presenter.distanceLabel)
构建和运行,以查看屏幕上的地图:

6. Editing Waypoints
最后一个功能是添加路点编辑功能,这样您就可以进行自己的旅行了!您可以在trip detail视图中重新排列列表。但是要创建一个新的waypoint,您需要一个新视图,以便用户输入名称。
为了得到一个新的视图,你需要一个Router。创建一个名为TripDetailRouter.swift的新Swift文件。
添加此代码到新文件:
import SwiftUI
class TripDetailRouter {
private let mapProvider: MapDataProvider
init(mapProvider: MapDataProvider) {
self.mapProvider = mapProvider
}
func makeWaypointView(for waypoint: Waypoint) -> some View {
let presenter = WaypointViewPresenter(
waypoint: waypoint,
interactor: WaypointViewInteractor(
waypoint: waypoint,
mapInfoProvider: mapProvider))
return WaypointView(presenter: presenter)
}
}
这就创建了一个WaypointView,它已经设置好,可以运行了。
有了router之后,转到TripDetailInteractor.swift,并添加以下方法:
func addWaypoint() {
trip.addWaypoint()
}
func moveWaypoint(fromOffsets: IndexSet, toOffset: Int) {
trip.waypoints.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
func deleteWaypoint(atOffsets: IndexSet) {
trip.waypoints.remove(atOffsets: atOffsets)
}
func updateWaypoints() {
trip.waypoints = trip.waypoints
}
这些方法是自我描述的。它们添加、移动、删除和更新waypoints。
接下来,通过TripDetailPresenter将它们暴露给视图。在TripDetailPresenter中,添加以下属性:
private let router: TripDetailRouter
这将保持router。通过将这个添加到init(interactor:)的顶部来创建它:
self.router = TripDetailRouter(mapProvider: interactor.mapInfoProvider)
这将创建与waypoint编辑器一起使用的router。接下来,添加这些方法:
func addWaypoint() {
interactor.addWaypoint()
}
func didMoveWaypoint(fromOffsets: IndexSet, toOffset: Int) {
interactor.moveWaypoint(fromOffsets: fromOffsets, toOffset: toOffset)
}
func didDeleteWaypoint(_ atOffsets: IndexSet) {
interactor.deleteWaypoint(atOffsets: atOffsets)
}
func cell(for waypoint: Waypoint) -> some View {
let destination = router.makeWaypointView(for: waypoint)
.onDisappear(perform: interactor.updateWaypoints)
return NavigationLink(destination: destination) {
Text(waypoint.name)
}
}
前三个是waypoint操作的一部分。最后一个方法调用router来获取waypoint的一个waypoint视图,并将其放到一个NavigationLink中。
最后,将以下内容添加到Text下面的VStack中,从而在TripDetailView中向用户显示:
HStack {
Spacer()
EditButton()
Button(action: presenter.addWaypoint) {
Text("Add")
}
}.padding([.horizontal])
List {
ForEach(presenter.waypoints, content: presenter.cell)
.onMove(perform: presenter.didMoveWaypoint(fromOffsets:toOffset:))
.onDelete(perform: presenter.didDeleteWaypoint(_:))
}
这将向视图添加以下控件:
- 将列表置于编辑模式的
EditButton,以便用户可以移动或删除路径点。 - 使用
presenter向列表添加新路径点的add按钮。 - 一个列表
List,它使用ForEach与presenter为每个路点创建一个单元格。该列表定义了一个onMove和onDelete操作,该操作启用那些编辑操作并回调到presenter。
构建并运行,您现在可以自定义一次旅行!确保保存任何更改。


Making Modules
使用VIPER,您可以将presenter, interactor, view, router和相关代码分组到模块中。
传统上,模块会在单个契约中公开presenter, interactor and router的接口。这对SwiftUI没有太大意义,因为它是向前的view。除非您希望将每个模块打包为自己的framework,否则可以将模块概念化为组。
TripListView.swift, TripListPresenter.swift, TripListInteractor.swift和TripListRouter.swift并将它们放在一个名为TripListModule的组中。
对细节类detail classes执行相同的操作:TripDetailView.swift, TripDetailPresenter.swift, TripDetailInteractor.swift, TripMapViewPresenter.swift, TripMapView.swift, and TripDetailRouter.swift。
将它们添加到一个名为TripDetailModule的新组中。
模块是保持代码整洁和分离的好方法。作为一个好的经验法则,一个模块应该是一个概念性的屏幕/特性,routers在模块之间传递用户。
后记
本篇主要介绍了VIPER架构模式,感兴趣的给个赞或者关注~~~

