🐙

SwiftUIで使うViewModelのInputsとOutputsを表現したい

2020/10/20に公開

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