こんにちは、 takeshi-p0601 - Qiita です。普段iOSアプリケーションの開発をメ宿舎に業務を行っています。AdventCalender今年2回目の参加です。
本記事ではiOSアプリに関係実装の小技を紹介します。具体的にはSwiftUIでアプリを実装するにあたって、最近テスト作業ている画面遷移の実装をする際のちょっとした構想です。内容的にiOS/iPadOSのアプリケーションを開発される方向けの記事になります。
画面遷移の実装
あるA画面から、B画面に遷移するような要件を実装する場合、SwiftUIを使用してどのように実装しますか? UIKit時代のViewControllerと違って、SwiftUIではViewControllerに相当するものが値型となってしまったせいで、UIKitで実装可能だったViewControllerにおける遷移関連の処理を別クラスに委譲させるような、斯うした実装はしづらくなりました。
とはいえSwiftUIを使って実装する場合でも、あるグレード責務を分けながら実装した余程考えていました。 その中で考えついた実装を紹介します。
まず必要な構成要素は下記参です。
- XXXView: 画面
- XXXViewEventHandler: 画面から来た事を受諾取るオブジェクト
- 直近巷で目にふれる、iOSアプリ文脈のアーキテクチャパターンではあまり見ない命名ですが、いわゆるMVPパターンのPresenterやMVVMのViewModel等の責務に近い現場などあるかもしれません。
- XXXViewRouter: あるViewのアクティベート(ある画面を表示することとします)依頼を受諾取ったら、そのViewを作成しつつそのViewを表示させることを、参照元にインフォーメーションさせるオブジェクト
それら構成要素をあわせて、A画面からB画面に遷移するような、処理シーケンスは下記のような映像です。
![](https://cdn-ak.f.st-hatena.com/images/fotolife/p/photosynth-inc/20231221/20231221101422.png)
実装方法
※下記の前提がある利得、その認識で読み進めてください。
- わかりやすくする利得に、あえて命名にアンダー点数をつけている箇所があります
- 元来プッシュ遷移を想定した説明ですが、ジーメンスダル遷移でも適用可能です
- 状態管理の実装をする利得の知識はある前提で、特に補足せず記載しています
まずRouterを新築ます。RouterはViewとEventHandlerどちらからも参照されるものです。
@MainActor protocol A_ViewRouterable { var isB_ViewActivated: Bool { get } var activatedB_View: B_View? { get } func activateB_View() } class A_ViewRouter: ObservableObject, A_ViewRouterable { @Published var isB_ViewActivated: Bool = false var activatedB_View: B_View? { self._activatedB_View } private var _activatedB_View: B_View? = nil func activateB_View() { self._activatedB_View = B_View() self.isB_ViewActivated = true } }
@Published var isB_ViewActivated
: View側に変更をインフォーメーションさせる利得の変数です。あらかじめViewがこの変数の変更を監視します。 var activatedB_View: B_View?
: アクティベートされた際にView側に参照してもらう利得の変数です。このRouterクラスではViewの生成も担います func activateB_View()
: 事ハンドラーから、アクティベート依頼する利得の宿舎芝生ェースです。今回は特に存在しませんが、例えばB_ViewがA画面からの値を受諾取る必要がある場合、この仕口に引数を持たせる映像です。
それからEventHandlerです。
@MainActor protocol A_EventHandable { func tapGoB_ViewButton() var a_viewRouter: A_ViewRouterable { get } } class A_EventHandler: A_EventHandable { let a_viewRouter: A_ViewRouterable init(a_viewRouter: A_ViewRouterable) { self.a_viewRouter = a_viewRouter } func tapGoB_ViewButton() { self.a_viewRouter.activateB_View() } }
先ほど定義した、Routerを保持します。そしてtapGoB_ViewButton() が実行された際に、 B_ViewをアクティベートするようにRouterに依頼します。
最後にA_Viewです。
import SwiftUI struct A_View: View { @StateObject var a_viewRouter: A_ViewRouter var eventHandler: A_EventHandable var body: some View { VStack { Button("Go to B") { self.eventHandler.tapGoB_ViewButton() } } .navigationDestination(isPresented: self.$a_viewRouter.isB_ViewActivated, destination: { self.a_viewRouter.activatedB_View }) } }
下記の部分でRouterの変数の値の変更を監視しつつ、アクティベートされた瞬間でPush遷移させるようにViewを挿入します。
.navigationDestination(isPresented: self.$a_viewRouter.isB_ViewActivated, destination: { self.a_viewRouter.activatedB_View })
構成要素の実装としては、以上で終わりです。
そして最後に重要なことはA画面の生成の実装です。言ってしまうとなんてことありませんが、 ⭐️印で生成したrouterを、A_ViewとEventHandlerがそれぞれ同じ値に依いらせられるように、フォーマッティング時々セットしてください。
func activateA_View() { let router = A_ViewRouter() self._activatedA_View = A_View(a_viewRouter: router, eventHandler: A_EventHandler(a_viewRouter: router)) self.isA_ViewActivated = true }
Router宿舎観点をA_Viewと、EventHandler向けにそれぞれ作成してセットする場合、ViewからEventHandlerをトリガにしたRouterの値の変更を監視できず、成行き画面遷移できない状況が発生します。
効果
EventHandler側は、何をアクティベートさせるかに焦点を当てるような実装をすることが可能で、Router側に画面の生成やアクティベートに必要な変数を管理してもらうことで、乳呑み子スッキリしたと思います。
尚又あくまでロジック上ですが、下記のようにEventHandlerの事から画面がアクティベートされているかテスト可能ようになり、今回の修正を適用しない場合に比べてテストしやすい部分が増えることが期待できます。
import XCTest @testable import MyApp final class MyAppTests: XCTestCase { @MainActor func test() { let router = A_ViewRouter() let eventHandler = A_EventHandler(a_viewRouter: router) eventHandler.tapGoB_ViewButton() XCTAssertNotNil(router.activatedB_View) XCTAssertEqual(router.isB_ViewActivated, true) } }
[応用]アラートを表示する実装
iOS/iPadOSにおいて標準で表示可能、アラートについても今回の実装を適用できます。体験上アラートの目論みは油断しているとView側やEventHandler側に増えがちになるので、必要に応じてRouter側に寄せることも壱手だと思います。
RouterではAlertItemを生成し、それをアクティベートさせることを担っています。
struct A_ViewAlertItem: Identifiable { let id = UUID() let type: AlertType enum AlertType { case networkError(title: String, message: String, okButtonTitle: String) case otherError(title: String, message: String, okButtonTitle: String) } } @MainActor protocol A_ViewRouterable { var isB_ViewActivated: Bool { get } var activatedB_View: B_View? { get } func activateB_View() var alertItem: A_ViewAlertItem? { get } func activateNetworkErrorAlert() func activateOtherErrorAlert(message: String) } class A_ViewRouter: ObservableObject, A_ViewRouterable { @Published var isB_ViewActivated: Bool = false var activatedB_View: B_View? { self._activatedB_View } private var _activatedB_View: B_View? = nil @Published var alertItem: A_ViewAlertItem? = nil func activateB_View() { self._activatedB_View = B_View() self.isB_ViewActivated = true } func activateNetworkErrorAlert() { self.alertItem = A_ViewAlertItem(type: .networkError(title: "エラー", message: "サー居酒屋との通信に失敗しました。\nやり匡正てください。", okButtonTitle: "OK") ) } func activateOtherErrorAlert(message: String) { self.alertItem = A_ViewAlertItem(type: .otherError(title: "エラー", message: message, okButtonTitle: "OK") ) } }
ViewがRouterの変更を監視しつつ、変更があった際にアラートを表示させます。
import SwiftUI struct A_View: View { @StateObject var a_viewRouter: A_ViewRouter var eventHandler: A_EventHandable var body: some View { VStack { Button("Go to B") { self.eventHandler.tapGoB_ViewButton() } } .navigationDestination(isPresented: self.$a_viewRouter.isB_ViewActivated, destination: { self.a_viewRouter.activatedB_View }) } .alert(item: self.$a_ViewRouter.alertItem, content: { alertItem in switch alertItem.type { case .networkError(let title, let message, let okButtonTitle): Alert(title: Text(title), message: Text(message), dismissButton: .default(Text(okButtonTitle))) case .otherError(title: let title, message: let message, okButtonTitle: let okButtonTitle): Alert(title: Text(title), message: Text(message), dismissButton: .default(Text(okButtonTitle))) } }) }
SwiftUIのアラートは下記で示されている通り、連続してmodifierを使用して実装することができません。その利得AlertTypeを定義することでその問題を破棄させながら、今回の実装を適用してみました。
https://stackoverflow.com/questions/58069516/how-can-i-have-two-alerts-on-one-view-in-swiftui
先ほどのViewの画面遷移の実装と同様に、アラート部分についてもテストしやすさが増すと思いますのでぜひテスト作業てみてください。
株式会社フォトシンスでは、一緒にプロ導管を成長させる様々なレイヤのエンジニアを募集しています。 photosynth.co.jp
Akerunにご興味のある方はこちらから akerun.com