Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connect enabledIf after init #170

Open
AlexisQapa opened this issue Nov 27, 2018 · 4 comments
Open

Connect enabledIf after init #170

AlexisQapa opened this issue Nov 27, 2018 · 4 comments

Comments

@AlexisQapa
Copy link

Hello,

I've been looking into this lib and it's working nice. The only thing I can't manage is to bind the enabledIf observable after the action init. This would be useful to inject actions into viewModels. Right now providing the work factory and the enabledIf at the same time is very inconvenient.

Here is an example of my viewModel :
`final class ExampleViewModel {

private let bag = DisposeBag()
let input: Input
let output: Output

init() {

    let isFormValid = PublishSubject<Bool>()
    let formValue = PublishSubject<MyFormValue>()
    let tap = PublishSubject<Void>()
    let isEnabled = PublishSubject<Bool>()
    let isExecuting = PublishSubject<Bool>()
    let error = PublishSubject<Error>()
    let success = PublishSubject<Void>()

    self.input = Input(isValid: isFormValid.asObserver(),
                       formValue: formValue.asObserver(),
                       tap: tap.asObserver())
    self.output = Output(isEnabled: isEnabled.asDriverAssertError(),
                         isExecuting: isExecuting.asDriverAssertError(),
                         error: error.asDriverAssertError(),
                         success: success.asDriverAssertError())

    let saveAction =  Action(enabledIf: isFormValid.asObservable(), workFactory: save)

    saveAction.enabled
        .asDriverIgnoreError()
        .drive(isEnabled)
        .disposed(by: bag)

    saveAction.executing
        .asDriverIgnoreError()
        .drive(isExecuting)
        .disposed(by: bag)

    saveAction.executionObservables
        .switchLatest()
        .filterMap { (result) -> FilterMap<Error> in
            guard case .failure(let underlyingError) = result else { return .ignore }
            return .map(underlyingError)
        }
        .asDriverAssertError()
        .drive(error)
        .disposed(by: bag)

    saveAction.executionObservables
        .switchLatest()
        .filterMap { (result) -> FilterMap<Void> in
            guard case .success = result else { return .ignore }
            return .map(())
        }
        .asDriverIgnoreError()
        .drive(success)
        .disposed(by: bag)

    tap
        .withLatestFrom(formValue)
        .subscribe(onNext: { (value) in
            saveAction.execute(value)
        })
        .disposed(by: bag)
}

func save(formData: MyFormValue) -> Observable<Either<Void, Error>> {
    return Observable<Either<Void, Error>>.create { observer in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let result: Either<Void, Error> = Bool.random() ? .success : .failure(MyError)
            observer.onNext(result)
            observer.onCompleted()
        }
        return Disposables.create()
    }
}

}

extension ExampleViewModel {
struct Input {
let isValid: AnyObserver
let formValue: AnyObserver
let tap: AnyObserver
}

struct Output {
    let isEnabled: Driver<Bool>
    let isExecuting: Driver<Bool>
    let error: Driver<Error>
    let success: Driver<Void>
}

}`

Being able to bind enabledIf after the init would allow me to drop most of the subjects.

@ashfurrow
Copy link
Member

Hmm, I haven't run into that issue. Instead of passing actions into my view models, I usually have the view models generate them from observables that are passed in as initializer parameters. Maybe I'm missing something from your question – anyone else have suggestions?

@Davarg
Copy link

Davarg commented Nov 29, 2018

Hello guys. Usually I use Action in this manner, maybe this will be helpful

var login = BehaviorRelay(value: Optional<String>(""))
var password = BehaviorRelay(value: Optional<String>(""))

override init() {
        super.init()
        
        let isEnabled = Observable<Bool>.combineLatest(self.login.asObservable(), self.password.asObservable()) { (login, password) in
            if String.stringIsBlank(login)
                || String.stringIsBlank(password) {
                return false
            } else {
                return true
            }
        }
        
        self.loginAction = Action(enabledIf: isEnabled,
                                  workFactory: { [weak self] in
                                    if let wCredManager = self?.credManager {
                                        return wCredManager.loginObservable(login: self?.login.value ?? "",
                                                                            password: self?.password.value ?? "",
                                                                            server: self?.settingsManager?.host ?? "",
                                                                            port: self?.settingsManager?.port ?? "")
                                            .flatMap({ _ in
                                                Observable<Void>.empty()
                                            })
                                    } else {
                                        return Observable.error(ActionError.notEnabled)
                                    }
        })
    }

@AlexisQapa
Copy link
Author

If you init the action in the viewModel you are forced to inject all the action dependencies into the viewModel.

Being able to init the action and injecting it into the ViewModels allow you to move out all the dependencies out of the viewModel. It is then only responsible to connect ui to actions.

In my app I've build a generic form which gather inputs and then call the save action. Depending of the flow I'd like to replace the action by another one. I'd rather inject an action then a work factory and its dependencies.

@ashfurrow
Copy link
Member

Gotcha. I hear you – but that’s not how I’ve done mvvm. My view models have all the business logic, and the view controller hooks up the view model’s actions to its UI. Hope that context helps find a solution – let us know how it goes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants