見出し画像

CombineとSwiftUIを組み合わせて、MVVMとVIPERの学習メモ(VIPER編part1)

こんにちは、iOSエンジニアのTanです。
本記事は前回のMVVM編を続きまして、VIPER編part1になります。

対象ターゲット

  • SwiftUIを触り始めた。

  • VIPERがわかるような、わからないような曖昧的な理解。

  • SwiftUIVIPERで作ったらどうなるの?

のような方であれば、読んでいただけたら嬉しいです。

はじめ

今回も下記の記事で参考させていただいたので、ソースコードを触りたい方はぜひサイト内からダウンロードしてください!

VIPERって何

VIPERについて触れる前に、Clean Architectureについて触れましょう。
VIPERClean Architectureの派生と言えるからです。

  • 2012年にUncle Bobが提唱したアーキテクチャー

  • 2003年にEric Evansの提唱したドメイン駆動設計の具体的なアーキテクチャーとして、Hexagonal ArchitectureOnion Architectureが提唱された。そのコンセプトを統合するために生まれたのがClean Architecture

  • ソフトウェアの中で、変更の多い場所のUI、DB、デバイスとの接続などから、本来変更の少ないはずであるビジネスロジックをEntityとして切り出すことで、技術的な目まぐるしい変更からビジネスロジックを守ろう、というのが目的

上記のClean Architectureについての説明とスクショはこの記事の一部を参考させていただきました。

そして、VIPERはView, Interactor, Presenter, Entity and Routerの各単語の先頭文字から取ったものです。

  • ViewはUser Interface, 本記事ではSwiftUIを指します。

  • Interactorはデータの取得依頼、取得したデータをPresenterに通知します。

  • Presenterは通知されたデータを正確にViewに渡します。ユーザアクションによってRouterに通知します。

  • Entityはデータのこと。Entityはドメインとほぼ同義で、こちらを参考してください。

  • Routerはどの画面に遷移する定義されています。

手を動かしましょう

Interactorを作成

TripListInteractor.swiftを作成します。

class TripListInteractor {
  let model: DataModel

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


Presenterを作成

TripListPresenter.swiftを作成します。

import SwiftUI
import Combine

class TripListPresenter: ObservableObject {
  private let interactor: TripListInteractor
    @Published var trips: [Trip] = []
    private var cancellables = Set<AnyCancellable>()


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

        interactor.model.$trips
          .assign(to: \.trips, on: self)
          .store(in: &cancellables)
  }
}

trips@Publishedをつけることで、tripsが変わったらViewに自動的に通知します。
DataModeltripsも同様です。

Viewを作成

SwiftUI ViewでTripListView.swiftを作成します。

プロパティを追加します。

@ObservedObject var presenter: TripListPresenter

TripListView_Previews.previewsを修正します。

let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)

bodyを修正します

List {
  ForEach (presenter.trips, id: \.id) { item in
    TripListCell(trip: item)
      .frame(height: 240)
  }
}

これで、一覧画面が作成できます。

詳細ページを開くために、Routerを作成

まず、上記と同じようにInteractorPresenterViewを作成します。
説明は割愛します。

Interactor

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
  }
}

Presenter

import SwiftUI
import Combine

class TripDetailPresenter: ObservableObject {
  private let interactor: TripDetailInteractor

  private var cancellables = Set<AnyCancellable>()

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

View

import SwiftUI

struct TripDetailView: View {
    @ObservedObject var presenter: TripDetailPresenter
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
    }
}

struct TripDetailView_Previews: PreviewProvider {
    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)
        }
    }
}

Routing
TripListRouter.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)
  }
}

UIKitの場合はview controllersactivating seguesが返されますが、SwiftUIview classが返されます。

そして、TripListPresenter.swiftにプロパティとメソッドを追加します。

private let router = TripListRouter()
func linkBuilder<Content: View>(
    for trip: Trip,
    @ViewBuilder content: () -> Content
  ) -> some View {
    NavigationLink(
      destination: router.makeDetailView(
        for: trip,
        model: interactor.model)) {
          content()
    }
}

最後に、TripListView.swiftのForEachの中身をlinkBuilderを追加すればOKです。

    var body: some View {
        List {
          ForEach (presenter.trips, id: \.id) { item in
              self.presenter.linkBuilder(for: item) {
                TripListCell(trip: item)
                  .frame(height: 240)
              }
          }
          .onDelete(perform: presenter.deleteTrip)
        }
        .navigationBarTitle("Roadtrips", displayMode: .inline)
        .navigationBarItems(trailing: presenter.makeAddNewButton())
    }


まとめ

一覧画面の実装から、詳細画面に遷移するまでやってきたのですが、いかがでしょうか?VIPERは少しわかりにくいかもしれませんが、コードを触ったらスッキリになると思います。
詳細画面の実装はpart2に連載します。

参考した記事

エンジニア募集中!

スペースマーケットでは現在アプリエンジニアを積極採用中です。
アプリで社会課題を解決しながら、あたりまえの世界を作っていきたい方がいらっしゃれば、ぜひお話しさせていただきたいです!

この記事が気に入ったらサポートをしてみませんか?