Following this blog post I have create an MVVM pattern in my app.
I have the following -
protocol ViewModelType {
associatedtype Dependency
associatedtype Bindings
init(dependency: Dependency, bindings: Bindings)
}
enum Attachable<VM: ViewModelType> {
case detached(VM.Dependency)
case attached(VM.Dependency, VM)
mutating func bind(_ bindings: VM.Bindings) -> VM {
switch self {
case let .detached(dependency):
let vm = VM(dependency: dependency, bindings: bindings)
self = .attached(dependency, vm)
return vm
case let .attached(dependency, _):
let vm = VM(dependency: dependency, bindings: bindings)
self = .attached(dependency, vm)
return vm
}
}
}
ViewController
final class StartViewController: BaseViewController<StartView> {
var viewModel: Attachable<StartViewModel>!
var bindings: StartViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear))
.mapToVoid()
.asDriverOnErrorJustComplete()
return StartViewModel.Bindings(
checkAuthState: viewWillAppear
)
}
}
extension StartViewController: ViewModelAttaching {
func bind(viewModel: StartViewModel) -> StartViewModel {
return viewModel
}
}
ViewModel
final class StartViewModel: ViewModelType {
typealias Dependency = HasAuthService
let authCheck: Driver<Void>
struct Bindings {
let checkAuthState: Driver<Void>
}
private let disposeBag = DisposeBag()
init(dependency: Dependency, bindings: Bindings) {
authCheck = bindings.checkAuthState
.flatMap {
return dependency.authSvc.checkSession()
.mapToVoid()
.asDriver(onErrorJustReturn: ())
}
}
}
This allows me to setup my views as follows -
let viewController = StartViewController()
let avm: Attachable<StartViewModel> = .detached(dependencies)
let viewModel = viewController.attach(wrapper: avm)
navigationController.setViewControllers([viewController], animated: false)
This works great, is very testable and I happy with how it works. I however have a scenario in which I need to pass some additional props into my ViewModel
.
For example, a userId
in the case of a profile scene.
This pattern keeps the init
method away so I need a way to inject additional props via .detached(dependencies)
Something like .detached(dependencies, userId)
however I am unsure how to do this in a generic fashion.
I have tried this - notice the addition of Props
and props
protocol ViewModelType {
associatedtype Dependency
associatedtype Bindings
associatedtype Props
init(dependency: Dependency, bindings: Bindings, props: Props?)
}
enum Attachable<VM: ViewModelType> {
case detached(VM.Dependency, VM.Props)
case attached(VM.Dependency, VM, VM.Props)
mutating func bind(_ bindings: VM.Bindings) -> VM {
switch self {
case let .detached(dependency, props):
let vm = VM(dependency: dependency, bindings: bindings, props: props)
self = .attached(dependency, vm, props)
return vm
case let .attached(dependency, _, props):
let vm = VM(dependency: dependency, bindings: bindings, props: props)
self = .attached(dependency, vm, props)
return vm
}
}
}
This however requires me to update my ViewModel
with the following -
**ViewModel**
```swift
typealias Props = <Some Prop Type>
init(dependency: Dependency, bindings: Bindings, props: Props?)
This works great, except in the case my I don't need to inject any props. I'm then stuck needing to specify a type for something that is not used.
I have tried this -
typealias Props = Void?
init(dependency: Dependency, bindings: Bindings, props: Props?)
and then setting up my view with -
let avm: Attachable<StartViewModel> = .detached(dependencies, nil)
But having to add typealias Props = Void?
in all my models that don't accept anything feels off
How can I achieve this in the most scalable, generic way?
Rather than introducing Props
, why don't you hide the additional information behind Dependency
?
So for example in your case you could have:
final class ProfileViewModel: ViewModelType {
let authCheck: Driver<Void>
struct Dependency {
let authSvc: HasAuthService
let userId: String
}
struct Bindings {
let checkAuthState: Driver<Void>
}
private let disposeBag = DisposeBag()
init(dependency: Dependency, bindings: Bindings) {
authCheck = bindings.checkAuthState
.flatMap {
return dependency.authSvc.checkSession()
.mapToVoid()
.asDriver(onErrorJustReturn: ())
}
// use dependency.userId
}
}