RxSwift MVVM Validate Form on Button Submit then Make API Request - ios

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.
Validate the phoneNumberTextField values when submitButton is tapped
Stop the Alamofire Request from being submitted if phoneNumberTextField is invalid and throw a client side error
Show a display indicator when loading takes place. This is the least important right now
A few things to note.
There is nothing tracking the phone number text at the moment
I do not want to disable the submit button until the form is valid as seen in examples all over.
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)
}

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.)

Related

RxSwift, invoking a network call

I am new to Rx and trying to one network call.
i mange to do that as follow:
struct Genre: Codable {
var genres: [Genres]?
}
struct Genres: Codable {
var id: Int?
var name: String?
}
final class GenreViewModel {
let disposeBag = DisposeBag()
var networking: Networking!
let getGeners = PublishRelay<[Genres]>()
var items: PublishRelay<[Genres]>?
var itemsDriver: Driver<[Genres]>?
let isLoading: Driver<Bool>
init(networking: Networking = Networking()) {
self.networking = networking
let shouldLoadItems = Observable.merge(
).debug("merge.debug(🚘)")
.startWith(())
let gg = shouldLoadItems.flatMap {
networking.preformNetwokTask(
endPoint: TheMoviedbApi.genre,
type: Genre.self)
.debug("🚘 network call")
}
isLoading = Observable.merge(
shouldLoadItems.map { true },
gg.map { _ in false }
)
.asDriver(onErrorJustReturn: false).debug("🚘 is loading")
itemsDriver = gg
.map { $0.genres! }
.asDriver(onErrorJustReturn: []).debug("🚘drive")
}
i am trying to figure a way of doing it without "let shouldLoadItems".
something like that:
final class GenreViewModel {
let disposeBag = DisposeBag()
var networking: Networking!
let getGeners = PublishRelay<[Genres]>()
var items: PublishRelay<[Genres]>?
var itemsDriver: Driver<[Genres]>?
let isLoading: Driver<Bool>
init(networking: Networking = Networking()) {
self.networking = networking
let geners = getGeners
.flatMap { geners in
networking.preformNetwokTask(
endPoint: TheMoviedbApi.genre,
type: Genre.self)
.debug("🚘 network call not in use")
}.startWith(())
.share()
isLoading = Observable.merge(
shouldLoadItems.map { true },
gg.map { _ in false }
)
.asDriver(onErrorJustReturn: false).debug("🚘 is loading")
itemsDriver = geners
.map { $0.genres! }
.asDriver(onErrorJustReturn: []).debug("🚘drive")
}
The vc:
func bindRx() {
viewModel.isLoading
.drive(hud.rx.animation)
.disposed(by: disposeBag)
viewModel.itemsDriver?.drive(collectionView.rx.items(cellIdentifier: GenreCollectionViewCell.reuseIdentifier, cellType: GenreCollectionViewCell.self)) { (row,item,cell) in
cell.config(item: item)
}.disposed(by: disposeBag)
}
Yet let geners not called.
what am i missing out?
I expect all the questions in the comments was a bit of a pain, but the thing that was missing in this question was the "reactive" part of "functional reactive programming".
In FRP, a network request doesn't happen in isolation. It's usually triggered by an event producer and feeds an event consumer. In order for the request to happen at all, an event consumer must exist.
A view model, on its own and not connected to anything, does nothing. In order for your networking event to happen, something must be listening for the result. Your view model is like the plumbing in your house. The water doesn't flow unless someone opens a tap.
struct GenreViewModel {
let items: Observable<[Genres]>
let error: Observable<Error>
let isLoading: Observable<Bool>
init(networking: Networking) {
let result = networking.preformNetwokTask(endPoint: TheMoviedbApi.genre, type: Genre.self)
.compactMap(\.genres)
.materialize()
.share(replay: 1)
items = result
.compactMap { $0.element }
error = result
.compactMap { $0.error }
isLoading = result
.map { _ in false }
.startWith(true)
}
}
final class GenreViewController: UIViewController {
var tableView: UITableView!
var activityIndicatorView: UIActivityIndicatorView!
var viewModel: GenreViewModel!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, genres, cell in
cell.textLabel?.text = genres.name
}
.disposed(by: disposeBag)
viewModel.error
.bind(onNext: presentScene(animated: true) {
UIAlertController(title: "Error", message: $0.localizedDescription, preferredStyle: .alert)
.scene { $0.connectOK() }
})
.disposed(by: disposeBag)
viewModel.isLoading
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
}
}
In the above code, the network request will start as soon as the bind call is executed. If an error emits, it is routed to the error Observable and will display a UIAlertController with the error message (It uses the CLE-Architecture-Tools to do this.) If it succeeds, the genres objects' names will be displayed in the table view.

How to assign a value to a variable in a class from JSON or pass this value to the next function?

Hi everyone!
At the moment, I am taking a course at the Harvard computer science CS50.
My homework is almost ready, but has some incompleteness.
I cannot assign a value from a function to a variable in the class or pass
this value to the next function.
import UIKit
class PokemonViewController: UIViewController {
var url: String!
var name: String!
#IBOutlet var pokemonImage: UIImageView!
#IBOutlet var nameLabel: UILabel!
#IBOutlet var numberLabel: UILabel!
#IBOutlet var type1Label: UILabel!
#IBOutlet var type2Label: UILabel!
#IBOutlet var catchButton: UIButton!
#IBOutlet var descriptionLabel: UILabel!
// MARK: - additional properties
var currentDescURL: String!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadPokemon()
showPokemonDescription()
}
//MARK: - pokemon loading
func loadPokemon() {
guard let pokemonURL = URL(string: url) else { return }
URLSession.shared.dataTask(with: pokemonURL) { (data, _, error) in
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(PokemonResult.self, from: data)
DispatchQueue.main.async {
self.navigationItem.title = self.capitalize(text: result.name)
self.nameLabel.text = self.capitalize(text: result.name)
self.numberLabel.text = String(format: "#%03d", result.id)
for typeEntry in result.types {
if typeEntry.slot == 1 {
self.type1Label.text = typeEntry.type.name
}
else if typeEntry.slot == 2 {
self.type2Label.text = typeEntry.type.name
}
}
// Create Image and Update ImageView
guard let imageURL = URL(string: result.sprites.front_default) else { return }
if let data = try? Data(contentsOf: imageURL) {
self.pokemonImage.image = UIImage(data: data)
}
self.currentDescURL = result.species.url
print(self.currentDescURL)
}
} catch let error { print(error) }
}.resume()
}
// MARK: - Get the URL of a specific Pokémon
func showPokemonDescription() {
guard let pokemonDescriptionURL = URL(string: currentDescURL) else { return }
URLSession.shared.dataTask(with: pokemonDescriptionURL) { (data, _, error) in
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(PokemonDescription.self, from: data)
DispatchQueue.main.async {
// Check and get first pokemon description in English
for index in 0..<result.flavor_text_entries.count {
if result.flavor_text_entries[index].language.name == "en" {
self.descriptionLabel.text = result.flavor_text_entries[index].flavor_text
}
}
}
} catch let error { print(error) }
}.resume()
}
}
The first function loadPokemon() inside itself gets value from JSON and prints the value to the console -> print(self.currentDescURL). Moreover, if you display this value in viewWillAppear, then "nil" will be displayed in the console. I understand that the loadPokemon() function processes the values in the stream that occur at the very end. Perhaps because of this, the variable currentDescURL cannot get the value from the loadPokemon() function and the showPokemonDescription() function cannot use this value since currentDescURL is nil.
I ask you to explain to me what my mistake is and to help finish the assignment.
Move the call for method showPokemonDescription from viewWillAppear to loadPokemon after the currentDescURL property is set.
class PokemonViewController: UIViewController {
//...
var currentDescURL: String!
//...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadPokemon()
getPreferences()
// <- remove the call from here
}
//...
func loadPokemon() {
//...
self.currentDescURL = result.species.url
self.showPokemonDescription() // <- move the call here
}
//...
func showPokemonDescription() {
//...
}
}

How to implement firebase auth into MVVM-C RxSwift

I'm trying to implement a MVVM-C rx swift app.
I have a VM for my sign up view controller with the username and password as behaviour subjects. I also have a firebase handler injected into the VM. What is the best way to pass the sign up result back to the VC?
my VM code:
class CreateVM {
let firebase: FirebaseHandler
let email: String
var password = BehaviorSubject<String>(value: "")
var confirmPassword = BehaviorSubject<String>(value: "")
var shouldHideButton: Observable<Bool> {
return Observable.combineLatest(password.asObservable(), confirmPassword.asObservable()) { pass, confPass in
!(pass.count >= 5 && pass == confPass)
}
}
init(firebase: FirebaseHandler, email: String) {
self.firebase = firebase
self.email = email
}
func submit() {
let pass = try! password.value()
firebase.createWithEmail(email: email, password: pass) { (result) in
switch result {
case .success(let uid):
print(uid, "created")
//handle successful creation
case .failure(let err):
print("failed with error:", err)
//handler error
}
}
}
}
My VC code:
class CreateVC: UIViewController, Storyboarded {
#IBOutlet weak var createButton: Rounded!
#IBOutlet weak var passwordEntry: UITextField!
#IBOutlet weak var confirmPasswordEntry: UITextField!
weak var coordinator: AuthCoordinator?
var displayName: String!
var viewModel: CreateVM!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindUI()
}
func bindUI() {
passwordEntry.rx.text.orEmpty.bind(to: viewModel.password).disposed(by: disposeBag)
confirmPasswordEntry.rx.text.orEmpty.bind(to: viewModel.confirmPassword).disposed(by: disposeBag)
viewModel.shouldHideButton.bind(to: createButton.rx.isHidden).disposed(by: disposeBag)
createButton.rx.tap.bind { [unowned self] _ in
self.viewModel.submit()
}.disposed(by: disposeBag)
}
}
I do my view models as a function, and a lot depends on exactly what you want to do with the result, but here is some sample code that might help you:
struct CreateInput {
let password: Observable<String>
let confirm: Observable<String>
let submit: Observable<Void>
}
struct CreateOutput {
let displayName: String
let shouldHideButton: Observable<Bool>
let signUpResult: Observable<Result<Int, Error>>
}
func createVM(firebase: FirebaseHandler, email: String) -> (CreateInput) -> CreateOutput {
return { input in
let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
let signUpResult = input.submit
.withLatestFrom(credentials)
.flatMapLatest {
firebase.create(email: $0.email, password: $0.password)
}
return CreateOutput(
displayName: email,
shouldHideButton: shouldHideButton,
signUpResult: signUpResult
)
}
}
extension FirebaseHandler {
func create(email: String, password: String) -> Observable<Result<Int, Error>> {
Observable.create { observer in
self.createWithEmail(email: email, password: password) { (result) in
observer.onNext(result)
observer.onCompleted()
}
return Disposables.create()
}
}
}
final class CreateViewController: UIViewController {
#IBOutlet weak var displayNameLabel: UILabel!
#IBOutlet weak var createButton: UIButton!
#IBOutlet weak var passwordEntry: UITextField!
#IBOutlet weak var confirmPasswordEntry: UITextField!
var bindUI: (CreateInput) -> CreateOutput = { _ in fatalError() } // assign `createVM(firebase: myFirebaseHandler, email: "myEmail")` to this before it loads.
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = CreateInput(
password: passwordEntry.rx.text.orEmpty.asObservable(),
confirm: confirmPasswordEntry.rx.text.orEmpty.asObservable(),
submit: createButton.rx.tap.asObservable()
)
let output = bindUI(input)
displayNameLabel.text = output.displayName
output.shouldHideButton
.bind(to: createButton.rx.isHidden)
.disposed(by: disposeBag)
output.signUpResult
.bind { result in
switch result {
case .success(let uid):
print("uid:", uid)
case .failure(let error):
print("error:", error.localizedDescription)
}
}
.disposed(by: disposeBag)
}
}
If higher order functions make you nervous, then you could wrap it in a type:
struct CreateVM {
struct Input {
let password: Observable<String>
let confirm: Observable<String>
let submit: Observable<Void>
}
struct Output {
let displayName: String
let shouldHideButton: Observable<Bool>
let signUpResult: Observable<Result<Int, Error>>
}
let firebase: FirebaseHandler
let email: String
func bind(_ input: Input) -> Output {
let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
let signUpResult = input.submit
.withLatestFrom(credentials)
.flatMapLatest { [unowned firebase] in
firebase.create(email: $0.email, password: $0.password)
}
return Output(
displayName: email,
shouldHideButton: shouldHideButton,
signUpResult: signUpResult
)
}
}
Then your view controller would have a property:
var viewModel: CreateVM!
and build the output with:
let output = viewModel.bind(input)

RxSwift Tap Event not triggered

I'm implementing the mvvm using RxSwif.
Here is what happens:
Validate Fields (write 7 characters in both textFields).
Tap the login button.
API is called which is fine.
Tap the button again.
API is not called.
I noticed that the "validObservable" changes every time that I write something in textFields which is fine.
ViewController:
var viewModel: LoginViewModelType!
let disposeBag = DisposeBag()
#IBOutlet weak var dniTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var logInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
logInButton.rx
.tap
.bind(to: viewModel.inputs.logInButtonDidTap)
.disposed(by: disposeBag)
dniTextField.rx
.text
.bind(to: viewModel.inputs.dniChanged)
.disposed(by: disposeBag)
passwordTextField.rx
.text
.bind(to: viewModel.inputs.passwordChanged)
.disposed(by: disposeBag)
/*viewModel.outputs.isLoginButtonEnabled
.drive(onNext: { [weak self] isEnabled in
guard let `self` = self else { return }
self.logInButton.isEnabled = isEnabled
})
.disposed(by: disposeBag)
*/
viewModel.outputs.logIn
.drive(onNext: { [weak self] user in
guard let `self` = self else { return }
self.performSegue(withIdentifier: "showMainController", sender: user)
})
.disposed(by: disposeBag)
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel = LoginViewModel()
}
ViewModel:
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
// ---------------------
// MARK: - Inputs
// ---------------------
var dniChanged: BehaviorRelay<String?>
var passwordChanged: BehaviorRelay<String?>
var logInButtonDidTap: PublishSubject<Void> = PublishSubject<Void>()
// ---------------------
// MARK: - Outpuds
// ---------------------
var logIn: Driver<User>
var isLoginButtonEnabled: Driver<Bool>
private let disposeBag = DisposeBag()
public init () {
dniChanged = BehaviorRelay<String?>(value: "")
passwordChanged = BehaviorRelay<String?>(value: "")
let dniObservable = dniChanged.asDriver().filterNil().asObservable()
let passwordObservable = passwordChanged.asDriver().filterNil().asObservable()
let dniValidation = dniObservable.map { $0.count > 4 }
let passwordValidation = passwordObservable.map { $0.count > 3 }
let validObservable = Observable.combineLatest(dniValidation, passwordValidation) { return $0 && $1 }.filter { $0 }
isLoginButtonEnabled = validObservable.asDriver(onErrorDriveWith: .empty())
let loginSuccessObservable = Observable.combineLatest(dniObservable, passwordObservable, validObservable) { (dni, password, valid) -> LogInRequest in
return LogInRequest(dni: dni, password: password)
}
logIn = logInButtonDidTap.withLatestFrom(loginSuccessObservable).flatMapLatest({ request -> Observable<User> in
return API.shared.post(endpoint: EndPoints.Authorize, type: User.self, body: request)
}).asDriver(onErrorDriveWith: .empty())
}
I'd say that .empty() is causing the observable to complete, and the subscription to be disposed of as a consequence.
I'd put some .debug() instructions to make sure what gets disposed and when.
I got this problem because the button size is zero

MVVM with RxSwift

I'm trying to understand mvvm + RxSwift but I got some questions.
I'm currently using this approach which I'm not sure if is the right or can be better. How can I do to like grouping the methods, I mean, maybe something like doFirst(loading = true).doNext(getData).doLast(loading = false).catch(apiError) then subscribe to this event? It's possible?
ViewController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel = UsersViewModel(apiService: apiService)
configureBindings()
}
func configureBindings() {
tableView.delegate = nil
tableView.dataSource = nil
viewModel.isLoading.bind(to: loadingView.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.models
.bind(to: tableView.rx.items(cellIdentifier: "userCell", cellType: UserCell.self)) {(_, _, cell) in
print("Binding the cell items")
}.disposed(by: disposeBag)
tableView.rx.modelSelected(User.self).subscribe(onNext: { value in
print(value)
}).disposed(by: disposeBag)
viewModel.error.filterNil().subscribe(onNext: { (err) in
self.tableView.backgroundView = EmptyView(title: "No Users", description: "No users found")
print("Showing empty view...")
print(err)
}).disposed(by: disposeBag)
}
}
Then in my UsersViewModel:
class UsersViewModel {
var models: Observable<[User]> {
return modelsVariable.asObservable()
}
var isLoading: Observable<Bool> {
return isLoadingVariable.asObservable()
}
var error: Observable<ApiError?> {
return errorVariable.asObservable()
}
private var modelsVariable = BehaviorRelay<[User]>(value: [])
private var isLoadingVariable = BehaviorRelay<Bool>(value: false)
private var errorVariable = BehaviorRelay<ApiError?>(value: nil)
// MARK: - Data Manager
var apiService: API
required init(apiService: API) {
self.apiService = apiService
isLoadingVariable.accept(true)
apiService.GET(EndPoints.USER_LIST, type: Several<User>.self)
.subscribe(onNext: { (model) in
self.isLoadingVariable.accept(false)
self.modelsVariable.accept(model.items)
}, onError: { (err) in
self.isLoadingVariable.accept(false)
self.errorVariable.accept(err as? ApiError)
})
}
}
My 'GET' function just returns a Observable<Several<User>>.
Several:
struct Several {
var items: [User]
}
Is there any improvements that I can do?
It's a little hard to understand what you're asking, but if you're concerned about the imperative nature of your init method, and want to wrap your API call into a continuous Observable sequence that can be repeated, you could do something like this:
class UsersViewModel {
//...
var fetchUsersObserver: AnyObserver<Void> {
return fetchUsersSubject.asObserver()
}
//...
private let fetchUsersSubject = PublishSubject<Void>()
private let disposeBag = DisposeBag()
//...
required init(apiService: API) {
self.apiService = apiService
bindFetchUsers()
}
private func bindFetchUsers() {
fetchUsersSubject
.asObservable()
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(true) })
.flatMap(self.fetchUsers)
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(false) })
.bind(to: modelsVariable)
.disposed(by: disposeBag)
}
private func fetchUsers() -> Observable<[User]> {
return apiService
.GET(EndPoints.USER_LIST, type: Several<User>.self)
.map { $0.items }
.catchError { [weak self] error in
self?.errorVariable.accept(error as? ApiError)
return .just([])
}
}
}
Then, you need only bind a control to this AnyObserver, or send it an event manually:
func configureBindings() {
// from a control, such as UIButton
someButton
.rx
.tap
.bind(to: viewModel.fetchUsersObserver)
.disposed(by: disposeBag)
// manually
viewModel.fetchUsersObserver.onNext(())
}
Footnote 1: I typically like to make my view models structs so that I don't have to worry about all the [weak self] statements.
Footnote 2: Notice how the fetchUsers() function catches any errors thrown and does not let the error propagate to the outer Observable sequence. This is important because if this outer Observable emits an error event, it can never emit another next event.

Resources