架构之路 (五) —— VIPER架构模式(一)

版本记录

版本号 时间
V1.0 2020.04.27 星期一

前言

前面写了那么多篇主要着眼于局部问题的解决,包括特定功能的实现、通用工具类的封装、视频和语音多媒体的底层和实现以及动画酷炫的实现方式等等。接下来这几篇我们就一起看一下关于iOS系统架构以及独立做一个APP的架构设计的相关问题。感兴趣的可以看上面几篇。
1. 架构之路 (一) —— iOS原生系统架构(一)
2. 架构之路 (二) —— APP架构分析(一)
3. 架构之路 (三) —— APP架构之网络层分析(一)
4. 架构之路 (四) —— APP架构之工程实践中网络层的搭建(二)

开始

首先看下主要内容:

在本教程中,您将了解如何在SwiftUICombine中使用VIPER体系结构模式,同时构建一个允许用户创建公路旅行的iOS应用程序,来自翻译。

下面看下写作环境

Swift 5, iOS 13, Xcode 11

接着就是正文了。

VIPER架构模式是MVCMVVM的另一种选择。虽然SwiftUICombine框架创建了一个强大的组合,可以快速构建复杂的ui和在应用程序中移动数据,但它们也面临着各自的挑战和对架构的看法。

人们普遍认为所有的应用逻辑都应该进入SwiftUI视图,但事实并非如此。

VIPER为这种情况提供了一种替代方案,可以与SwiftUICombine结合使用,帮助构建具有清晰架构的应用程序,该架构有效地分离了所需的不同功能和职责,如用户界面、业务逻辑、数据存储和网络。这样就更容易进行测试、维护和扩展。

在本教程中,您将使用VIPER体系结构模式构建一个应用程序。这款应用也被方便地称为VIPER

它将允许用户通过向一条路线添加路径点来构建公路旅行。在此过程中,您还将了解您的iOS项目中的SwiftUICombine

打开启动项目。这包括一些代码,让你开始:

  • 当你构建其他视图时,ContentView会启动它们。
  • Functional views组中有一些帮助视图:一个用于包装MapKit map视图,这是一个特殊的split image视图,由TripListCell使用。你会把这些加到屏幕上。
  • Entities组中,您将看到与数据模型相关的类。TripWaypoint稍后将作为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是一种类似MVCMVVM的体系结构模式,但是它通过单一职责进一步分离了代码。苹果风格的MVC促使开发者将所有的逻辑放到一个UIViewController子类中。像之前的MVVM一样,VIPER试图解决这个问题。

VIPER中的每个字母代表体系结构的一个组件:视图、交互程序、演示程序、实体和路由器(View, Interactor, Presenter, Entity and Router)

  • 视图View是用户界面。这与SwiftUIView相对应。
  • 交互器Interactor是一个在演示者presenter和数据之间进行中介的类。它从演示者presenter那里获得方向。
  • 演示者Presenter是架构的“交通警察”,在视图view和交互器interactor之间指挥数据,执行用户操作并调用路由器在视图之间移动用户。
  • 实体Entity表示应用程序数据。
  • 路由器Router处理屏幕之间的导航。这与SwiftUI不同,在SwiftUI中,视图显示任何新视图。

这种分离来自“Uncle”Bob Martin的Clean Architecture paradigm。

当您查看图表时,您可以看到数据在视图view和实体entities之间流动的完整路径。

SwiftUI有自己独特的做事方式。如果你将VIPER职责映射到域对象将会不同,如果你将它与UIKit应用的教程相比较。

1. Comparing Architectures

人们经常用MVCMVVM来讨论VIPER,但它与那些模式不同。

  • MVC (Model-View-Controller)是2010年iOS应用程序架构中最常使用的模式。使用这种方法,你在storyboard中定义ViewController是一个关联的UIViewController子类。控制器Controller修改视图,接受用户输入并直接与模型交互。控制器Controller因视图逻辑和业务逻辑而膨胀。
  • MVVM是一种流行的体系结构,在View Model中它将视图逻辑与业务逻辑分离开来。视图模型与模型Model交互。

最大的区别是,视图模型View Model与视图控制器不同,它只有对视图和模型的单向引用。MVVM非常适合SwiftUI

VIPER更进一步,将视图逻辑与数据模型逻辑分离。只有演示者presenter与视图对话,只有interactormodel (entity)对话。演示者presenter和交互者interactor相互协调。演示者presenter关心的是显示和用户操作,而交互者interactor`关心的是操纵数据。


Defining an Entity

VIPER是这种架构的一个有趣的缩写,但它的顺序不是禁止的。

在屏幕上显示内容的最快方法是从实体entity开始。entity是项目的数据对象。在本例中,主要的entityTrip,它包含一个路点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集合,创建一个链接,当数据模型改变时,保持presentertrips更新。

最后,此subscription存储在cancellables中,以便您可以在以后清理它。


Building a View

现在需要构建第一个视图Viewtrip 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

到目前为止,您已经看到了从entityinteractor的数据流,通过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()的操作。这将操作转发给interactorinteractor操作数据模型。

返回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

现在是时候添加VIPERRouter部分了。

路由器Router允许用户从旅行列表视图trip list view导航到旅行详细信息视图trip detail viewtrip detail视图将显示路线点列表以及路线地图。

用户将能够从此屏幕编辑路线点列表和旅行名称。

1. Setting Up the Trip Detail Screens

在显示细节屏幕之前,您需要创建它。

按照前面的例子,创建两个新的Swift文件:TripDetailPresenter.swiftTripDetailInteractor.swift,以及一个名为TripDetailView.swiftSwiftUI视图。

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,该视图由一个interactorpresenter填充。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)
}

它使用来自presenterNavigationLink,将单元格设置为其内容并将其放入列表中。

构建并运行,现在,当用户点击单元格时,它将把它们路由到“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的旅行名分配给presentertripName属性。这使值保持同步。

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:)中,您将waypointsinteractor映射到MKPointAnnotation对象,以便它们可以作为地图上的大头针显示。然后将directions复制到routes数组。

要使用presenter,创建一个名为TripMapView.swiftSwiftUI 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 routespreviews结构构建的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,它使用ForEachpresenter为每个路点创建一个单元格。该列表定义了一个onMoveonDelete操作,该操作启用那些编辑操作并回调到presenter

构建并运行,您现在可以自定义一次旅行!确保保存任何更改。


Making Modules

使用VIPER,您可以将presenter, interactor, view, router和相关代码分组到模块中。

传统上,模块会在单个契约中公开presenter, interactor and router的接口。这对SwiftUI没有太大意义,因为它是向前的view。除非您希望将每个模块打包为自己的framework,否则可以将模块概念化为组。

TripListView.swift, TripListPresenter.swift, TripListInteractor.swiftTripListRouter.swift并将它们放在一个名为TripListModule的组中。

对细节类detail classes执行相同的操作:TripDetailView.swift, TripDetailPresenter.swift, TripDetailInteractor.swift, TripMapViewPresenter.swift, TripMapView.swift, and TripDetailRouter.swift

将它们添加到一个名为TripDetailModule的新组中。

模块是保持代码整洁和分离的好方法。作为一个好的经验法则,一个模块应该是一个概念性的屏幕/特性,routers在模块之间传递用户。

后记

本篇主要介绍了VIPER架构模式,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(架构之路 (五) —— VIPER架构模式(一))