4年半ぶりに復帰!
生成AIやら、SwiftUIやら、
世界が変わってししまいましたが、
なんとかiOSエンジニアとして生き残っています。
iOS開発でMVVMをやってる時に、
- ViewControllerやViewModel内のバインディングがぐちゃぐちゃになった
- どのプロパティがどこにバインドされてるかわからん
- Outputのはずなのに、他のOutputにInputとしてそのまま使われてる
など、バインド処理がぐちゃぐちゃで鬱になったので、役割ごとに切り分けることで、すっきりさせる設計を色々試しました。
で、どのように分けたか
- Input
- Viewからの入力
- ボタンのタップ
- テキストの入力
- ライフサイクル
- Viewからの入力
- Output
- ViewModelからViewへ通知
- テキストの更新
- 色の更新
- 画像の更新
- ViewModelからViewへ通知
- Navigation
- ViewModelからControllerへ通知
- 次の画面へ進むトリガー
- 前の画面へ進むトリガー
- エラーダイアログ表示トリガー
- ViewModelからControllerへ通知
骨組み
全コードを記載すると、全体が見渡しにくいので、骨組みだけ記載します。
ViewModel
/// ViewModel
final class SampleViewModel {
/// 入力
struct Input {
// 画面から受け取る入力を定義
}
/// 出力
struct Output {
// 画面へ出力する内容を定義
}
/// 画面遷移
struct Navigation {
// 画面遷移を定義
}
init() {}
/// Inputをバインディング
func bindViewModel(input: Input) -> (Output, Navigation) {
// inputとModelを利用して、OutputとNavigationを紐づける
let output = Output()
let navigation = Navigation()
return (output, navigation)
}
}
ViewController
/// ViewController
final class SampleViewController<ViewModel: SampleViewModel>: UIViewController {
/// ViewModel
private let viewModel: ViewModel
/// Input
private var input = ViewModel.Input
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
self.input = .init()
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
/// ViewModelとバインド
private func bindViewModel() {
bindIntput(input)
let (output, navigation) = viewModel.bind(input)
bindOutput(output)
bindNavigaion(navigation)
}
/// 画面の操作をバインド
private func bindInput(_ input: ViewModel.Input) {
}
/// ViewModelの出力をバインド
private func bindOutput(_ output: ViewModel.Output) {
}
/// 画面遷移をバインド
private func bindNavigation(_ navigation: ViewModel.Navigation) {
}
}
今後
-
実際の実装レベルのサンプルを公開
- UseCaseとRepositryも含める
-
画面の中に画面がある、カスタムViewの入れ子状態の時、View間でやりとりするとぐちゃぐちゃになるので、Model層で効率的に連携する方法を模索
-
生成AIでサクッと機能を追加するテンプレ呪文を考えたい
参考