版本记录
版本号 | 时间 |
---|---|
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()
这个集合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()
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()
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(
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.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
它们为视图提供了读取和设置trip
名称的入口。
然后,在init
方法中添加以下内容:
// 1
setTripName = Binding(
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 =
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()
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架构模式,感兴趣的给个赞或者关注~~~