I am using RxSwift for the first time.
It will load Post List from my server.
The first time only need to run loadPostList
but
Just by initializing the model, nextPage is called.
Please let me know what I'm doing wrong.
class PostNetworkModel {
var nextPage:BehaviorSubject<Void> = BehaviorSubject(value: ())
var reFresh:BehaviorSubject<Void> = BehaviorSubject(value: ())
var raiseError:BehaviorSubject<Void> = BehaviorSubject(value: ())
var isLoadingComplete = false
private var pageNo = BehaviorRelay(value: 1)
var postList : BehaviorRelay<[Post]> = BehaviorRelay(value: [])
let disposeBag = DisposeBag()
init() {
loadPostList()
.bind(to: postList)
.disposed(by: disposeBag)
postList
.asObservable()
.subscribe({ ( _) in
self.isLoadingComplete = true
})
.disposed(by: disposeBag)
nextPage
.asObservable()
.subscribe(onNext: { [weak self] ( _) in
print("nextPage")
if (self?.isLoadingComplete)!{
self?.isLoadingComplete = false
self?.pageNo.accept(self!.pageNo.value + 1)
}
})
.disposed(by: disposeBag)
}
}
BehaviorSubject accepts an initial value as constructor parameter because its specification is as follows:
Observers can subscribe to the subject to receive the last (or initial) value and all subsequent notifications.
Meaning nextPage will trigger a next even on subscription. You'll probably want to change all the BehaviorSuject to PublishSubject for this class
class PostNetworkModel {
var nextPage:PublishSubject<Void> = PublishSubject()
var reFresh:PublishSubject<Void> = PublishSubject()
var raiseError:PublishSubject<Void> = PublishSubject()
var isLoadingComplete = false
private var pageNo = BehaviorRelay(value: 1)
var postList : PublishRelay<[Post]> = PublishRelay()
let disposeBag = DisposeBag()
init() {
loadPostList()
.bind(to: postList)
.disposed(by: disposeBag)
postList
.asObservable()
.subscribe({ ( _) in
self.isLoadingComplete = true
})
.disposed(by: disposeBag)
nextPage
.asObservable()
.subscribe(onNext: { [weak self] ( _) in
print("nextPage")
if (self?.isLoadingComplete)!{
self?.isLoadingComplete = false
self?.pageNo.accept(self!.pageNo.value + 1)
}
})
.disposed(by: disposeBag)
}
}
Note than I kept pageNo as a BehaviorRelay, as it makes sense for this observable to have an initial value.
Related
I'm trying to get the amount value from the service in the View Model, and then bind it in the ViewController to an amountLabel.
This is my ViewModel:
class AmountViewModel {
private let accountService: AccountManagerProtocol
let _amount = BehaviorRelay<Int>(value: 0)
let amount: Observable<Int>
private let disposeBag = DisposeBag()
init(accountService: AccountManagerProtocol = AccountManager()) {
self.accountService = accountService
amount = _amount.asObservable()
getAmount()
}
func getAmount(){
accountService.getAccount()
.map{ $0.amount ?? 0 }
.bind(to: _amount)
.disposed(by: disposeBag)
}
}
This is my ViewController,
I did something like this, to obtain the amount of the viewModel, but I feel that it is not the best way, I would like to obtain the value of amount and be able to bind it to amountLabel in a simpler way.
private extension AmountViewController {
private func bindViewModel() {
amountView.titleLabel.text = viewModel.title
//Get Amount
viewModel.amount
.observe(on: MainScheduler.instance)
.withUnretained(self)
.subscribe(onNext: { owner, amount in
if let amountString = amount.currencyToString() {
owner.inputAmountView.amountLabel.text = "BALANCE: \(amountString)"
}
})
.disposed(by: disposeBag)
}
Here is the most obvious simplification:
class AmountViewModel {
let amount: Observable<Int>
init(accountService: AccountManagerProtocol = AccountManager()) {
amount = accountService.getAccount()
.map { $0.amount ?? 0 }
}
}
private extension AmountViewController {
private func bindViewModel() {
viewModel.amount
.compactMap { $0.currencyToString().map { "BALANCE: \($0)"} }
.observe(on: MainScheduler.instance)
.bind(to: inputAmountView.amountLabel.rx.text)
.disposed(by: disposeBag)
}
}
But I think I would move the code in the compactMap closure into the view model...
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.
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
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.
I am using RxSwift 4.0 and build tableView content using DTTableViewManager
in Presenter, I have model variable
1
lazy var mostRecent: TableTitleHeaderContainer = {
let container = TableTitleHeaderContainer(isHidden: true, title: "Title 1")
return container
}()
lazy var lastRecent: TableTitleHeaderContainer = {
let container = TableTitleHeaderContainer(isHidden: false, title: "Title 2")
return container
}()
code of model
2
class TableTitleHeaderContainer {
var subject: PublishSubject<Void> = PublishSubject<Void>()
var isHidden: Bool
var title: String
var disposeBag = DisposeBag()
init(isHidden: Bool, title: String) {
self.isHidden = false
self.title = title
}
}
and view configuration with model
3
extension TableTitleHeaderView: ModelTransfer {
func update(with model: TableTitleHeaderContainer) {
clearButton.isHidden = model.isHidden
clearButton.rx.tap
.bind(to: model.subject).disposed(by: disposeBag)
titleLabel.text = model.title
}
}
And I want to listen tap on button in Presenter
4
mostRecent.subject.asObserver().subscribe(onNext: { [weak self] (_) in
print("Clear mostRecent")
}).disposed(by: disposeBag)
lastRecent().subject.asObserver().subscribe(onNext: { [weak self] (_) in
print("Clear lastRecent")
}).disposed(by: disposeBag)
But after configure view with model subscribe in Presenter does not call?
What is the trouble?