はじめに
こんにちは。リブセンスで転職ナビiOSアプリの開発に携わっている須川です。
今回は転職ナビアプリを例にして、ViewControllerにSwiftUIで作ったViewを組み込む方法を紹介します。
また、SwiftUIの**PropertyWrappers
**を使って、表示中のデータに変更があった場合に実装コストをかけずに画面を更新する方法を実現したいと思います。
SwiftUIを組み込む画面について
転職ナビアプリには求人を検索する時の検索条件や検索結果の件数を表示する箇所があるのですが、既存の実装は.xib
ファイルで作成されています。
今回はxibで作ったViewをSwiftUIで置き換える形でSwiftUIを導入したいと思います。
(画像の赤枠部分が対象のViewです)
Viewの要件
要件は以下の内容です。
- 右端の青いアイコンをタップすると検索条件の設定画面が開く
- 検索条件を設定して設定画面を閉じると、新しい検索条件が反映される
- 検索条件の設定項目
- 職種(複数選択可)
- 希望年収
- 希望勤務地(複数選択可)
- 検索条件の設定項目
- 求人検索が実行され、検索結果の件数が反映される
SwiftUIで画面作成
ViewModelの作成
まずはViewの要件を満たすViewModelを作成します。
ViewModelはObservableObject
プロトコルに準拠させて、値の更新があった場合に自動的に新しい値がViewに反映されるようにします。
プロパティは@Published
を付けて宣言することで、値の更新が発生した時にオブザーブしているViewに変更が通知されます。
class SearchConditionHeaderModel: ObservableObject {
@Published private(set) var workCount = "0"
@Published private(set) var occupation = "希望職種"
@Published private(set) var salary = "希望年収"
@Published private(set) var location = "希望勤務地"
var buttonAction: (() -> Void)?
func update(workCount: String, occupation: String, salary: String, location: String) {
self.workCount = workCount
self.occupation = occupation
self.salary = salary
self.location = location
}
}
Viewの作成
次にSwiftUIで下記の画面を作成します。
ViewModelのプロパティの更新が通知されるように、@ObservedObject
を付けてViewModelを宣言します。
@ObservedObject var viewModel: SearchConditionHeaderModel
画面右のPreviewはデフォルトだとiPhoneの画面サイズで表示されますが、↓のようにmodifierを追加するとコンテンツのサイズで表示されて余白も追加できます。
struct SearchConditionHeaderView_Previews: PreviewProvider {
static var previews: some View {
SearchConditionHeaderView(viewModel: SearchConditionHeaderModel())
// コンテンツのサイズでpreviewを表示
.previewLayout(PreviewLayout.sizeThatFits)
// 余白
.padding()
}
}
ViewControllerからSwiftUIを呼び出す
呼び出し側のViewControllerのレイアウトはStoryboard
で作成されています。
画像の赤枠部分に先ほどのSwiftUIで作ったViewを表示したいので、既存のxib
で作ったViewを削除して、この部分にContainerView
を追加します。
追加したContainerView
はIBOutlet
でViewControllerに紐付けておきます。
次に、最初に作成したViewModelをViewControllerで保持しておきます。
private var searchConditionModel = SearchConditionHeaderModel()
下記の関数をViewControllerのviewDidLoad()
で呼び出します。
private func setupConditionHeaderView() {
// 1. SwiftUIのViewはUIHostingControllerを継承したクラスにラップ
// SwiftUIに↑のViewModelを渡す
let hostingVC = SearchConditionHeaderHostingViewController(rootView: SearchConditionHeaderView(viewModel: searchConditionModel))
hostingVC.view.backgroundColor = UIColor(red: 240 / 255, green: 238 / 255, blue: 235 / 255, alpha: 1)
// 2. hostingVCをchild ViewControllerとしてセット
addChild(hostingVC)
// 3. ContainerViewのsubViewにhostingVCのviewを追加
conditionHeaderView.addSubview(hostingVC.view)
// 4. 親ViewControllerに子ViewControllerが追加されたことをコール
hostingVC.didMove(toParent: self)
// 5. constraintを付けてviewを固定
hostingVC.view.translatesAutoresizingMaskIntoConstraints = false
let bindings = ["view": hostingVC.view]
conditionHeaderView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0),
metrics: nil,
views: bindings as [String: Any]))
conditionHeaderView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0),
metrics: nil,
views: bindings as [String: Any]))
// 6. SwiftUIのViewのボタンがタップされた時のアクション
searchConditionModel.buttonAction = { [weak self] in
self?.showMatchingCondition()
}
}
}
コードについて解説します。
- ViewControllerからSwiftUIを呼び出す場合は、UIHostingControllerが必要になります。今回はUIHostingControllerの
viewWillAppear()
でNavigationControllerの挙動を制御したかったのでサブクラスを使っていますが、特にoverrideしたい箇所がない場合はUIHostingControllerでSwiftUIをラップすればOKです。また、SwiftUIの初期化にviewModelが必要なため、ViewControllerで呼び出したviewModelを渡しています。 - SwiftUIをラップしたUIHostingControllerを呼び出し側のViewControllerのchildViewControllerとして追加します。
- IBOutletで接続したContainerViewのsubViewにhostingVCのview (SwiftUI) を追加します。
- ContainerViewController(この場合はUIHostingControllerのサブクラス)に子ViewControllerを追加した場合は、子ViewControllerに
.didMove(toParent:)
メソッドで追加の完了を通知する必要があるようです。こちらに詳しい解説がありました。 - Viewの制約をつけるか表示するポジションを指定しないと表示がずれてしまうため、今回は制約を付けています。
- 最後に、SwiftUIでボタンがタップされた時のアクションを定義します。今回はボタンタップでSwiftUIを保持しているViewControllerから検索条件の設定画面に遷移したいので、ViewControllerで遷移処理を実行します。SwiftUIでボタンがタップされたことをViewControllerに通知する必要があるので、ViewModelのクロージャ経由で通知を行っています。
SwiftUIを表示
アプリを起動するとSwiftUIで実装したViewが表示されます。
値の更新
あとは検索条件が更新されたタイミングでViewModelに定義したupdate()
メソッドを実行すれば、検索条件をSwiftUIにバインドできます。
func update(workCount: String, occupation: String, salary: String, location: String) {
self.workCount = workCount
self.occupation = occupation
self.salary = salary
self.location = location
}
Before After
フォントやテキストカラーなどは微妙に変更していますが、xibで作ったViewと同等のものをSwiftUIで置き換えることができました。
Before | After |
---|---|
xib | SwiftUI |
UIKitベースのプロジェクトにSwiftUIを追加した所感
今回は実験的にSwiftUIを追加してみましたが、SwiftUIでViewを作っている時にPreviewの表示に時間が掛かり、レイアウトを確認する作業が大変でした。
Previewを更新する度に差分ビルドが走って、ビルドが完了してから数十秒後に更新が完了するという状態だったので、本格的にSwiftUIを導入する場合はPreview速度の向上を模索する必要がありそうです。
また、状況に応じてどのPropertyWrappersを使えば良いか迷ったので、もう少しPropertyWrappersの理解が必要だと感じました。
思いの外簡単に導入できたので、今後新しい画面を追加するときはSwiftUIを使っていきたいと思います!