SwiftUIで使うViewModelのInputsとOutputsを表現したい
ViewModelType
View Model の inputs と outputs を表現する方法として、kickstarter の ViewModelType がお洒落でかっこいいと思っています。
参考) https://github.com/kickstarter/ios-oss/tree/0d86260ce8f5052585affe11f623fef43aea2e3b/Library/ViewModels
protocol HogeViewModelInputs {
func input()
}
protocol HogeViewModelOutputs {
var output: String { get }
}
protocol HogeViewModelType {
var inputs: HogeViewModelInputs { get }
var outputs: HogeViewModelOutputs { get }
}
class HogeViewModel: HogeViewModelType, HogeViewModelInputs, HogeViewModelOutputs {
var inputs: HogeViewModelInputs { self }
var outputs: HogeViewModelOutputs { self }
var output: String = "hoge"
func input() { }
}
SwiftUI の時代になって
SwiftUI で preiew が導入され、ViewModel を抽象化する需要が(私の中で)高まりつつある中、入出力が明確にかける kickstarter の ViewModelType を SwiftUI でも表現したいです。
しかし、SwiftUI は Combine や、ObservableObject、@ObservedObject、@Published、Binding など見たこと無い表現で、一体何が必要なのか分からんになってました。
しかし、先日 EnclosingSelf の存在に気付き、なんとなく SwiftUI と仲良くなれた気がするので、SwiftUI で使える ViewModelType を表現出来るようになった気がします。
protocol ContentViewModelInputs {
var toggle: Bool { get set }
}
protocol ContentViewModelOutputs {
var text: String { get }
}
protocol ContentViewModelType: ObservableObject {
var inputs: ContentViewModelInputs { get set }
var outputs: ContentViewModelOutputs { get }
}
class ContentViewModel: ContentViewModelType, ContentViewModelInputs, ContentViewModelOutputs {
var inputs: ContentViewModelInputs {
get { self }
set { }
}
var outputs: ContentViewModelOutputs { self }
@Published var toggle: Bool = false
@Published var text: String = "preparing"
init() {
$toggle.map({ v -> String in
return "toggled: \(v)"
}).assign(to: &$text)
}
}
struct ContentView<ViewModelType: ContentViewModelType>: View {
@ObservedObject private var viewModel: ViewModelType
init(viewModel: ViewModelType) {
self.viewModel = viewModel
}
var body: some View {
VStack {
Text(viewModel.outputs.text)
Toggle(isOn: $viewModel.inputs.toggle, label: {
Text("Label")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: ContentViewModel())
}
}
以下、実装する上で気付いたポイントなど
- ObservableObject が associatedtype を持つので、 ViewModel を generic 型で定義しておく。
struct ContentView<ViewModelType: ContentViewModelType>: View {
- ViewModelType が ObservableObject を継承する。
protocol ContentViewModelType: ObservableObject {
- @Published は ObservableObject の objectWillChange に更新が通知されればいいので、Input や Outputs では関係無い。
protocol ContentViewModelInputs {
var toggle: Bool { get set }
}
protocol ContentViewModelOutputs {
var text: String { get }
}
さいごに
EnclosingSelf など複雑な仕組みをなんとなく眺めた上で、ViewModelType を考えると意外と普通な結果になった気がします。
Preview をやる上で、ViewModel の mocking や、preview 用のターゲットなど、これから仕組みを整備して行きたい所存です。
Discussion