iOSアプリ「ニフティ不動産」へのSwiftUI導入に向けて、Viewに画面遷移処理が入り込んでしまう課題への解決方針を検討しました
はじめに
こんにちは!ニフティライフスタイルでモバイルアプリの開発をしているt-toshasegawです。
過去の記事のiOSアプリ「ニフティ不動産 賃貸版」の画面をSwiftUIで書けるか試してみたでも紹介した通り、モバイルアプリ開発チームではiOSアプリ「ニフティ不動産 賃貸版」へのSwiftUI導入を目指していました。
そこで今回はiOSアプリ「ニフティ不動産 賃貸版」のアーキテクチャに沿ったSwiftUI導入に向けて発生した課題への対応方針をご紹介させて頂きます。
目次
導入にあたっての課題
現在iOSアプリ「ニフティ不動産 賃貸版」ではVIPERアーキテクチャを採用しており、画面遷移処理はViewから独立させRouterで行なっています。
SwiftUIで画面遷移を行うにはNavigationLink
を使用するなどの方法がありますが、これを使用するとViewに画面遷移処理が入り込んでしまうという問題に直面します。
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("SecondViewへ画面遷移")
}
}
}
}
対応方針
上記の問題を解決するために、画面は従来通りViewControllerを使用しそこにUIHostingController
を介したSwiftUIで作成したViewを置くという方針にすることにしました。そうすることで画面UIをSwiftUIで実装し、画面遷移処理はこれまで通りRouterに移譲することが出来るようになります。
また、SwiftUIからUIViewController経由でのデータ参照はObservableObject
を使用し、SwiftUIからViewControllerへのイベント受け渡しはdelegate
を使用することにしました。
コード例
ViewControllerへSwiftUIで作成したViewを置く
まずはViewControllerにSwiftUIで作成したViewを置くための実装です。
ViewControllerにextensionでaddSwiftUIChild
を追加します。そうすることで各ViewControllerからaddSwiftUIChild
を呼ぶだけで完結出来るようにしています。
extension UIViewController {
func addSwiftUIChild<Content: SwiftUI.View>(_ childView: Content) {
// SwiftUIをUIKitで使用できるように変換
let hostingVC = UIHostingController(rootView: childView)
// ViewControllerにSwiftUIを置く
addChild(hostingVC)
view.addSubview(hostingVC.view)
hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
// 制約をつける
NSLayoutConstraint.activate([
hostingVC.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
hostingVC.didMove(toParent: self)
}
}
SwiftUIからViewController経由でデータを参照出来るようにする
次にSwiftUIからViewController経由でデータを参照出来るようにするための実装です。
SwiftUIにDataSource
というObservableObject
を継承させたクラスを作成し、そこに@Published
を付与したプロパティを持たせます。さらにSwiftUIに@ObservedObject
を付与したdataSource
プロパティ定義することでDataSource
クラスを監視させます。
ViewController側ではpresenter
から送られてきた値をそのままdataSource.texts
にassign
することでデータが更新されます。すると、SwiftUI側で検知されViewが更新されるといった流れになります。
※ViewControllerはpresenterの値をCombineを使用して監視しています
class ViewController: UIViewController {
var presenter: Presenter!
private var cancellables: Set<AnyCancellable> = []
private var dataSource: ContentView.DataSource = .init()
override func viewDidLoad() {
super.viewDidLoad()
addSwiftUIChild(ContentView(dataSource: dataSource))
// dataSource.textsを更新
presenter.$texts
.assign(to: \.dataSource.texts, on: self)
.store(in: &cancellables)
}
}
struct ContentView: View {
// ObservableObjectを継承する
class DataSource: ObservableObject {
@Published var texts: [String] = []
}
// DataSourceを監視
@ObservedObject var dataSource: DataSource
var body: some View {
// dataSource.textsの変更を検知してViewを更新
List(dataSource.texts, id: \.self) { text in
HStack {
Text(text)
Spacer()
}
.contentShape(Rectangle())
}
.listStyle(.plain)
}
}
SwiftUIからViewControllerにイベントを送る
最後にSwiftUIからViewControllerにタップや長押しなどのイベントを送るための実装です。
新たにContentViewDelegate
を作成し、SwiftUI側からdelegateでメソッドを呼びます。ViewControllerをContentViewDelegate
に準拠させイベントが送られたらViewController側で処理をすることが出来るようにしています。
class ViewController: UIViewController {
var presenter: Presenter!
private var cancellables: Set<AnyCancellable> = []
private var dataSource: ContentView.DataSource = .init()
override func viewDidLoad() {
super.viewDidLoad()
// delegate: selfを指定しContentViewDelegateに準拠させる
addSwiftUIChild(ContentView(delegate: self, dataSource: dataSource))
presenter.$texts
.assign(to: \.dataSource.texts, on: self)
.store(in: &cancellables)
}
}
extension ViewController: ContentViewDelegate {
// タップイベントを検知する
func didSelect(_ contentView: ContentView, didSelect text: String) {
// presenterに処理を送る
}
}
// protocolにdelegateメソッドを作成
protocol ContentViewDelegate: AnyObject {
func didSelect(
_ contentView: ContentView,
didSelect text: String
)
}
struct ContentView: View {
class DataSource: ObservableObject {
@Published var texts: [String] = []
}
@ObservedObject var dataSource: DataSource
weak var delegate: ContentViewDelegate?
var body: some View {
List(dataSource.texts, id: \.self) { text in
HStack {
Text(text)
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
// delegateでタップイベントを送る
delegate?.didSelect(self, didSelect: text)
}
}
.listStyle(.plain)
}
}
さいごに
複雑な実装はせずに画面遷移処理をこれまで通りRouterに移譲することが出来ました。
iOSアプリ「ニフティ不動産 賃貸版」では今後SwiftUIを導入しやすい画面から徐々に置き換えていく計画です。
また今回はViewControllerも従来通り使用するという対応方針にしましたが、今後の展望としてSwiftUIオンリーでの導入検討も行いたいなと思いました。
参考
今回の記事の内容については、下記クックパッド様の記事を参考にさせて頂きました。
掲載内容は、記事執筆時点の情報をもとにしています。