Search code examples
swiftmvvmrx-swift

Injecting additional props into a viewmodel using generics


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?


Solution

  • 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
        }
    }