Reputation: 3749
I'm new to RxSwift and attempting to do as the title states with an MVVM input output approach.
I can't figure out the best approach to do the following.
A few things to note.
Here is my view controller
import UIKit
import RxSwift
import RxCocoa
class SplashViewController: BaseViewController {
// MARK: – View Variables
@IBOutlet weak var phoneNumberTextField: UITextField!
@IBOutlet weak var phoneNumberBackgroundView: UIView!
@IBOutlet weak var submitButton: BaseButton!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var separatorView: UIView!
@IBOutlet weak var countryCodeButton: UIButton!
@IBOutlet weak var parentVerticalStackView: UIStackView!
// MARK: – View Model & RxSwift Setup
private let disposeBag = DisposeBag()
private let viewModel: SplashMVVM = SplashMVVM()
// MARK: – View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// RxSwift handling
setupViewModelBinding()
setupCallbacks()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: true)
}
// MARK: – RxSwift Handling
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside)
.bind(to: viewModel.input.submit)
.disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.success.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
self.pushVerifyPhoneNumberViewController()
})
.disposed(by: disposeBag)
viewModel.output.error.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
})
.disposed(by: disposeBag)
}
// MARK: – Navigation
func pushVerifyPhoneNumberViewController() {
let viewController = VerifyPhoneNumberViewController.fromStoryboard("Authentication")
self.navigationController?.pushViewController(viewController, animated: true)
}
}
Here is my view model.
import Foundation
import RxSwift
import RxCocoa
import Alamofire
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
struct Input {
let submit: AnyObserver<Void>
}
struct Output {
let success: Observable<VerifyMobilePhone?>
let error: Observable<Error?>
}
init() {
input = Input(submit: submitSubject.asObserver())
let request = Alamofire.request(VerifyMobileRouter.post("+16306996540")).responseDecodableRx(VerifyMobilePhone.self)
let requestData = submitSubject.flatMapLatest {
request
}
let success = requestData.map { $0.value ?? nil }
let error = requestData.map { $0.error ?? nil }
output = Output(
success: success,
error: error
)
}
}
Here is what I came up with.
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
var phoneNumberSubject = PublishSubject<String>()
struct Input {
let phoneNumber: AnyObserver<String>
let submit: AnyObserver<Void>
}
struct Output {
let validationError: Observable<String>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
init() {
input = Input(phoneNumber: phoneNumberSubject.asObserver(), submit: submitSubject.asObserver())
let request = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
$0.isValidPhoneNumber(region: "US")
}.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
}.share()
let validationError = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
!$0.isValidPhoneNumber(region: "US")
}.map { _ in
"This phone number is invalid"
}
let success = request.filter { $0.isSuccess }.map { $0.value! }
let error = request.filter { $0.isFailure }.map { $0.error! }
output = Output(
validationError: validationError,
success: success,
error: error
)
}
}
View controller changes…
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside).bind(to: viewModel.input.submit).disposed(by: disposeBag)
phoneNumberTextField.rx.text.orEmpty.bind(to: viewModel.input.phoneNumber).disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.validationError.bind { string in
SwiftMessages.show(.error, message: string)
}.disposed(by: disposeBag)
viewModel.output.success.bind { verifyMobilePhone in
self.pushVerifyPhoneNumberViewController()
}.disposed(by: disposeBag)
viewModel.output.error.bind { error in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
}.disposed(by: disposeBag)
}
Upvotes: 1
Views: 2638
Reputation: 33967
You are close, you're just missing the phone number text as input into your view model.
struct SplashInput {
let phoneNumber: Observable<String>
let submit: Observable<Void>
}
struct SplashOutput {
let invalidInput: Observable<Void>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
extension SplashOutput {
init(_ input: SplashInput) {
let request: Observable<Event<VerifyMobilePhone>> = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber }
.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
.materialize()
}
.share()
invalidInput = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber == false }
success = request
.map { $0.element }
.filter { $0 != nil }
.map { $0! }
error = request
.map { $0.error }
.filter { $0 != nil }
.map { $0! }
}
}
Your SplashViewController
would have:
override func viewDidLoad() {
super.viewDidLoad()
let input = SplashInput(
phoneNumber: phoneNumberTextField.rx.text.orEmpty.asObservable(),
submit: submitButton.rx.tap.asObservable()
)
let viewModel = SplashOutput(input)
viewModel.invalidInput
.bind {
SwiftMessages.show(.invalid, message: "You entered an invalid number. Please try again.")
}
.disposed(by: bag)
viewModel.success
.bind { [unowned self] verifyMobilePhone in
self.pushVerifyPhoneNumberViewController(verifyMobilePhone)
}
.disposed(by: bag)
viewModel.error
.bind { error in
SwiftMessages.show(.error(error), message: "There was an error. Please try again.")
}
}
(I took some liberties with what you already have written, but the above should make sense.)
Upvotes: 1