版本记录
版本号 | 时间 |
---|---|
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
。
![](https://img.haomeiwen.com/i3691932/0ba1be8c1ea54683.png)
打开启动项目。这包括一些代码,让你开始:
- 当你构建其他视图时,
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中找到它。
如果您现在构建并运行,您将不会看到任何有趣的东西。
![](https://img.haomeiwen.com/i3691932/f11f1c7763037140.png)
然而,在本教程结束时,您将拥有一个功能齐全的道路旅行计划应用程序。
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。
![](https://img.haomeiwen.com/i3691932/e33bb4ac8220d435.png)
当您查看图表时,您可以看到数据在视图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`关心的是操纵数据。
![](https://img.haomeiwen.com/i3691932/06fd05141e70d42d.png)
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
。
![](https://img.haomeiwen.com/i3691932/b8e112253de35dc3.png)
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)
}
这允许您在预览模式下查看导航栏。
恢复实时预览以查看按钮。
![](https://img.haomeiwen.com/i3691932/c0789a559a1c8651.png)
3. Seeing It In Action
现在是返回并将TripListView
连接到应用程序其余部分的好时机。
打开ContentView.swift
,在view
主体中,将VStack
替换为:
TripListView(presenter:
TripListPresenter(interactor:
TripListInteractor(model: model)))
这将创建视图及其presenter and interactor
。现在构建并运行。
点击+
按钮将向列表添加一个New Trip
。
![](https://img.haomeiwen.com/i3691932/ba38f812690b735a.png)
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
,整个链条就断开了。
构建并运行,现在您就可以移除旅行了!
![](https://img.haomeiwen.com/i3691932/84b6103e8bfcb8ed.png)
Routing to the Detail View
现在是时候添加VIPER
的Router
部分了。
路由器Router
允许用户从旅行列表视图trip list view
导航到旅行详细信息视图trip detail view
。trip detail
视图将显示路线点列表以及路线地图。
用户将能够从此屏幕编辑路线点列表和旅行名称。
![](https://img.haomeiwen.com/i3691932/608c493b6409d991.png)
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!”
![](https://img.haomeiwen.com/i3691932/0455c700218962cb.png)
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
。
![](https://img.haomeiwen.com/i3691932/55e8043a0fa5e9e1.png)
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
标题。
![](https://img.haomeiwen.com/i3691932/a8bbc16215136da6.png)
编辑旅行名称后保存,重新启动应用程序后将显示更改。
![](https://img.haomeiwen.com/i3691932/9ebab777bb0f064f.png)
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)
查看地图正确:
![](https://img.haomeiwen.com/i3691932/1e4047ce65bd6bc5.png)
要将地图添加到应用程序,首先将以下方法添加到TripDetailPresenter
:
func makeMapView() -> some View {
TripMapView(presenter: TripMapViewPresenter(interactor: interactor))
}
这将生成一个地图视图,并为其提供presenter
。
接下来,打开TripDetailView.swift
。
将以下内容添加到TextField
下面的VStack
:
presenter.makeMapView()
Text(presenter.distanceLabel)
构建和运行,以查看屏幕上的地图:
![](https://img.haomeiwen.com/i3691932/1c28c0ecdf4fd340.png)
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
。
构建并运行,您现在可以自定义一次旅行!确保保存任何更改。
![](https://img.haomeiwen.com/i3691932/6c3c7c19c26933a5.png)
![](https://img.haomeiwen.com/i3691932/01eed370e310325a.png)
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架构模式,感兴趣的给个赞或者关注~~~
![](https://img.haomeiwen.com/i3691932/626c27e986520b54.png)
网友评论