前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >架构之路 (五) —— VIPER架构模式(一)

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

原创
作者头像
conanma
修改2021-09-06 10:08:42
17.3K0
修改2021-09-06 10:08:42
举报
文章被收录于专栏:正则正则

开始

首先看下主要内容:

在本教程中,您将了解如何在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 ImagesPixabay 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 MartinClean 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文件。

添加以下代码到文件:

代码语言:javascript
复制
class TripListInteractor {
  let model: DataModel

  init (model: DataModel) {
    self.model = model
  }
}

这将创建interactor类并为它分配一个DataModel,稍后您将使用它。


Setting Up the Presenter

现在,创建一个名为TripListPresenter.swift的新Swift文件。这是为presenter类准备的。演示者presenter关心的是向UI提供数据和协调用户操作。

将此代码添加到文件中:

代码语言:javascript
复制
import SwiftUI
import Combine

class TripListPresenter: ObservableObject {
  private let interactor: TripListInteractor

  init(interactor: TripListInteractor) {
    self.interactor = interactor
  }
}

这将创建一个presenter类,它引用了interactor

由于演示者presenter的工作是用数据填充视图,所以您希望从数据模型中公开旅程trips列表。

添加一个新变量到类:

代码语言:javascript
复制
@Published var trips: [Trip] = []

这是用户将在视图中看到的旅行列表。通过使用@Published属性包装器声明它,视图将能够监听属性的变化并自动更新自身。

下一步是将此列表与来自interactor的数据模型同步。首先,添加以下helper属性:

代码语言:javascript
复制
private var cancellables = Set<AnyCancellable>()

这个集合set用于存储Combine subscriptions,因此它们的生存期与类的生存期绑定在一起。这样,任何subscriptions将保持活跃,只要presenter

init(interactor:)的末尾添加以下代码:

代码语言:javascript
复制
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

代码语言:javascript
复制
@ObservedObject var presenter: TripListPresenter

这将presenter链接到视图。接下来,通过更改TripListView_Previews.preview的主体来修复预览:

代码语言:javascript
复制
let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)

现在,替换TripListView.body的内容:

代码语言:javascript
复制
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类中添加以下内容:

代码语言:javascript
复制
func addNewTrip() {
  model.pushNewTrip()
}

这封装了模型的pushNewTrip(),它在trips列表的顶部创建了一个新的Trip

然后,在TripListPresenter.swift,把这个加到类里:

代码语言:javascript
复制
func makeAddNewButton() -> some View {
  Button(action: addNewTrip) {
    Image(systemName: "plus")
  }
}

func addNewTrip() {
  interactor.addNewTrip()
}

这将创建一个带有system + image的按钮,其中包含一个调用addNewTrip()的操作。这将操作转发给interactorinteractor操作数据模型。

返回TripListView.swift,并在List右括号后添加以下内容:

代码语言:javascript
复制
.navigationBarTitle("Roadtrips", displayMode: .inline)
.navigationBarItems(trailing: presenter.makeAddNewButton())

这将按钮和标题添加到导航栏。现在在TripListView_Previews中修改return,如下所示:

代码语言:javascript
复制
return NavigationView {
  TripListView(presenter: presenter)
}

这允许您在预览模式下查看导航栏。

恢复实时预览以查看按钮。

3. Seeing It In Action

现在是返回并将TripListView连接到应用程序其余部分的好时机。

打开ContentView.swift,在view主体中,将VStack替换为:

代码语言:javascript
复制
TripListView(presenter:
  TripListPresenter(interactor:
    TripListInteractor(model: model)))

这将创建视图及其presenter and interactor。现在构建并运行。

点击+按钮将向列表添加一个New Trip

4. Deleting a Trip

创建旅行的用户可能还希望能够删除它们,以防出错或旅行结束。既然已经创建了数据路径,向屏幕添加额外的操作就很简单了。

TripListInteractor,添加:

代码语言:javascript
复制
func deleteTrip(_ index: IndexSet) {
  model.trips.remove(atOffsets: index)
}

这将从数据模型中的trips集合中删除项。因为它是一个@Published属性,所以UI将自动更新,因为它订阅了更改。

TripListPresenter,添加:

代码语言:javascript
复制
func deleteTrip(_ index: IndexSet) {
  interactor.deleteTrip(index)
}

这将delete命令转发给interactor

最后,在TripListView中,在ForEach的结束括号后面添加以下内容:

代码语言:javascript
复制
.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的内容设置为:

代码语言:javascript
复制
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中,将其内容设置为:

代码语言:javascript
复制
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中,添加以下属性:

代码语言:javascript
复制
@ObservedObject var presenter: TripDetailPresenter

这将在视图中添加对presenter的引用。

再次获得预览,改变stub为:

代码语言:javascript
复制
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文件。

将其内容设置为:

代码语言:javascript
复制
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添加为属性:

代码语言:javascript
复制
private let router = TripListRouter()

现在,您已经创建了路由器作为presenter的一部分。

接下来,添加这个方法:

代码语言:javascript
复制
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的内容改为:

代码语言:javascript
复制
self.presenter.linkBuilder(for: item) {
  TripListCell(trip: item)
    .frame(height: 240)
}

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

构建并运行,现在,当用户点击单元格时,它将把它们路由到“Hello World”TripDetailView

3. Finishing Up the Detail View

您仍然需要填写一些旅行细节,以便用户可以看到路线并编辑路线点。

首先添加一个旅行标题:

TripDetailInteractor中,添加以下属性:

代码语言:javascript
复制
var tripName: String { trip.name }
var tripNamePublisher: Published<String>.Publisher { trip.$name }

这只公开了旅行名称的String版本,以及当该名称更改时的的Publisher

此外,加上以下内容:

代码语言:javascript
复制
func setTripName(_ name: String) {
  trip.name = name
}

func save() {
  model.save()
}

第一种方法允许presenter更改旅行名称,第二种方法将模型保存到持久层。

现在,转到TripDetailPresenter。添加以下属性:

代码语言:javascript
复制
@Published var tripName: String = "No name"
let setTripName: Binding<String>

它们为视图提供了读取和设置trip名称的入口。

然后,在init方法中添加以下内容:

代码语言:javascript
复制
// 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的旅行名分配给presentertripName属性。这使值保持同步。

trip名称分隔成这样的属性允许您同步该值,而不需要创建一个无限循环的更新。

接下来,添加:

代码语言:javascript
复制
func save() {
  interactor.save()
}

这增加了一个保存功能,这样用户可以保存任何编辑过的细节。

最后,转到TripDetailView,将body替换为:

代码语言:javascript
复制
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,并添加以下属性:

代码语言:javascript
复制
@Published var totalDistance: Measurement<UnitLength> =
  Measurement(value: 0, unit: .meters)
@Published var waypoints: [Waypoint] = []
@Published var directions: [MKRoute] = []

它们提供了关于一次旅行中的路径点的以下信息:作为Measurement的总距离、路径点列表和连接这些路径点的方向列表。

然后,在init(trip:model:mapInfoProvider:)的末尾添加后续订阅:

代码语言:javascript
复制
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,添加以下属性:

代码语言:javascript
复制
@Published var distanceLabel: String = "Calculating..."
@Published var waypoints: [Waypoint] = []

视图将使用这些属性。通过在init(interactor:)的末尾添加以下内容,将它们连接起来以跟踪数据更改:

代码语言:javascript
复制
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文件。将其内容设置为:

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

要使用presenter,创建一个名为TripMapView.swiftSwiftUI View。将其内容设置为:

代码语言:javascript
复制
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

代码语言:javascript
复制
func makeMapView() -> some View {
   TripMapView(presenter: TripMapViewPresenter(interactor: interactor))
}

这将生成一个地图视图,并为其提供presenter

接下来,打开TripDetailView.swift

将以下内容添加到TextField下面的VStack

代码语言:javascript
复制
presenter.makeMapView()
Text(presenter.distanceLabel)

构建和运行,以查看屏幕上的地图:

6. Editing Waypoints

最后一个功能是添加路点编辑功能,这样您就可以进行自己的旅行了!您可以在trip detail视图中重新排列列表。但是要创建一个新的waypoint,您需要一个新视图,以便用户输入名称。

为了得到一个新的视图,你需要一个Router。创建一个名为TripDetailRouter.swift的新Swift文件。

添加此代码到新文件:

代码语言:javascript
复制
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,并添加以下方法:

代码语言:javascript
复制
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中,添加以下属性:

代码语言:javascript
复制
private let router: TripDetailRouter

这将保持router。通过将这个添加到init(interactor:)的顶部来创建它:

代码语言:javascript
复制
self.router = TripDetailRouter(mapProvider: interactor.mapInfoProvider)

这将创建与waypoint编辑器一起使用的router。接下来,添加这些方法:

代码语言:javascript
复制
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中向用户显示:

代码语言:javascript
复制
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架构模式,感兴趣的给个赞或者关注~~~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开始
  • What is VIPER?
    • 1. Comparing Architectures
    • Defining an Entity
    • Adding an Interactor
    • Setting Up the Presenter
    • Building a View
      • 1. Creating a View with a Presenter
        • 2. Modifying the Model from the View
          • 3. Seeing It In Action
            • 4. Deleting a Trip
            • Routing to the Detail View
              • 1. Setting Up the Trip Detail Screens
                • 2. Routing
                  • 3. Finishing Up the Detail View
                    • 4. Using a Second Presenter for the Map
                      • 5. Considering the Map View
                        • 6. Editing Waypoints
                        • Making Modules
                        • 后记
                        相关产品与服务
                        数据保险箱
                        数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档