anhlq-ios / Surveys

MIT License
1 stars 0 forks source link

Some issues and discussions related to RxSwift and VIPER #6

Closed suho closed 3 years ago

suho commented 3 years ago

class LoginPresenter {

var email = BehaviorRelay<String>("")

}


In VIPER, the View displays what it is told to by the Presenter and relays user input back to the Presenter. Not edit the state of the Presenter. Also, from your code, the LoginView also holds `var isShowLoading: BehaviorRelay<Bool>`, it should use `Driver` instead.

View -> Presenter: there are 3 user inputs (observables: emailTextField.rx.text, passText.rx.text, loginButton.rx.tap), we just need send it backs to Presenter. Presenter will decide the logic.
Presenter -> View: Presenter emit 2 outputs to View (drivers: isLoading: Driver<Bool>; isEnableLogin: Driver<Bool>).

- You are applying RxSwift, but only for View and Presenter, could you tell me the reason why you didn't apply it for Interactor or NetworkService?
- About VIPER, let's imagine, the project is bigger, and there are multiple screens that use the same API request (for example, fetch user data and show it), we will have duplicate code in the Interactor of each module, how can we deal with that with VIPER architecture?
- And for example, if I want to send back some data from the `SurveyDetail` screen back to the `SurveyList` screen, how can we do it with VIPER?
anhlq-ios commented 3 years ago

LoginPresenter should not public BehaviorRelay, PublishRelay to view, because when you public mutable property in Presenter, it means the View can edit the state of Presenter In VIPER, the View displays what it is told to by the Presenter and relays user input back to the Presenter. Not edit the state of the Presenter. View -> Presenter: there are 3 user inputs (observables: emailTextField.rx.text, passText.rx.text, loginButton.rx.tap), we just need send it backs to Presenter. Presenter will decide the logic.

Sure we can implement the relationship between view and presenter by Input/Output. But I think it's okay to go with my solution. It separates the binding flow from view -> presenter and reverse. That way I could easily mock view or presenter to test the relationship between them.

Also, from your code, the LoginView also holds var isShowLoading: BehaviorRelay<Bool>, it should use Driver instead.

I did use is as Driver where the view subscribed to it, again they are the same concept, I just want to make the view to control its variables more careful: isShowLoading.asDriver().drive(loadingActivity.rx.isAnimating).disposed(by: disposeBag)

You are applying RxSwift, but only for View and Presenter, could you tell me the reason why you didn't apply it for Interactor or NetworkService?

Sure we could use Traits like Single to implement the Interactor and NetworkService.

About VIPER, let's imagine, the project is bigger, and there are multiple screens that use the same API request (for example, fetch user data and show it), we will have duplicate code in the Interactor of each module, how can we deal with that with VIPER architecture?

In VIPER, Interactors represent the business components, and Presenters are unique for each Screen. As one view could have multiple business requirements, a presenter could interact with many interactors as it needs. So we could reuse Interactor and pass it as a separated dependency to different presenters, but I don't recommend this solution. But ideally is NO, VIPER speaks for itself that each screen should have one and only one for each component. This way we take advance of the architect, decoupling each screen, well-organized code base, easier to testing, maintaining, scale up. In case of need, one Interactor could have multiple services associate with it, in order to talk to different data source/services.

And for example, if I want to send back some data from the SurveyDetail screen back to the SurveyList screen, how can we do it with VIPER?

We could use delegate in this case, specify a protocol in SurveyDetail and use it to communicate with the previous screen, for example:

protocol DetailListener: AnyObject {
    func popBack()
}

class DetailPresenter {
    weak var listener: DetailListener?

    init(listener: DetailListener) {
        self.listener = listener
    }
}

And in the SurveyListPresenter we conform the protocol like this, and pass itself to the as dependency

protocol DetailListener: AnyObject {
    func popBack()
}

class ListPresenter: DetailListener {
    func popBack() {
        //do something
    }
}
suho commented 3 years ago

In VIPER, Interactors represent the business components, and Presenters are unique for each Screen. As one view could have multiple business requirements, a presenter could interact with many interactors as it needs. So we could reuse Interactor and pass it as a separated dependency to different presenters, but I don't recommend this solution.

And from the VIPER doc:

An Interactor represents a single use case in the app. It contains the business logic to manipulate model objects (Entities) to carry out a specific task. The work done in an Interactor should be independent of any UI. The same Interactor could be used in an iOS app or an OS X app.

An Interactor only represents a single use case, if VIPER speaks for itself that each screen should have one and only one for each component, then how we share Interactor and use it for iOS app / iPad app or even macOS app? What do you think about this?

anhlq-ios commented 3 years ago

An Interactor only represents a single use case, if VIPER speaks for itself that each screen should have one and only one for each component, then how we share Interactor and use it for iOS app / iPad app or even macOS app? What do you think about this?

If they have quite similar functionality, we could share the interfaces of the Interactor from an iOS app to another. They may have the same interface but the implementation would be specific for individuals. For examples:

protocol LoginInteractableListener: AnyObject {
    func onLoginSuccess()
    func onLoginError(_ error: Error)
}

protocol LoginInteractable: Interactable {
    var presenter: LoginInteractableListener? { get set }
    func login(email: String, password: String)
    func loginAutomated()
}