CombineとSwiftUIを組み合わせて、MVVMとVIPERの学習メモ(VIPER編part1)
こんにちは、iOSエンジニアのTanです。
本記事は前回のMVVM編を続きまして、VIPER編part1になります。
対象ターゲット
SwiftUIを触り始めた。
VIPERがわかるような、わからないような曖昧的な理解。
SwiftUIはVIPERで作ったらどうなるの?
のような方であれば、読んでいただけたら嬉しいです。
はじめ
今回も下記の記事で参考させていただいたので、ソースコードを触りたい方はぜひサイト内からダウンロードしてください!
VIPERって何
VIPERについて触れる前に、Clean Architectureについて触れましょう。
VIPERはClean Architectureの派生と言えるからです。
2012年にUncle Bobが提唱したアーキテクチャー
2003年にEric Evansの提唱したドメイン駆動設計の具体的なアーキテクチャーとして、Hexagonal ArchitectureやOnion 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に自動的に通知します。
DataModelのtripsも同様です。
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を作成
まず、上記と同じようにInteractor、Presenter、Viewを作成します。
説明は割愛します。
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 controllersかactivating seguesが返されますが、SwiftUIはview 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に連載します。
参考した記事
エンジニア募集中!
スペースマーケットでは現在アプリエンジニアを積極採用中です。
アプリで社会課題を解決しながら、あたりまえの世界を作っていきたい方がいらっしゃれば、ぜひお話しさせていただきたいです!
この記事が気に入ったらサポートをしてみませんか?