I am using RxSwift to make a pull to refresh and refreshNextPage.
Currently, here is the viewModel I work so far:
public final class MomentViewModel {
// Property
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let posts = Variable<[Post]>([])
var pageIndex: Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
public init() {
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
self.pageIndex = 0
print("reset page index to 0")
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("refreshRequest", trimOutput: false)
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
}
.debug("nextPageRequest", trimOutput: false)
let request = Observable.merge(refreshRequest, nextPageRequest)
.debug("Request", trimOutput: false)
let response = request.flatMapLatest { page in
RxAPIProvider.shared.getPostList(page: page).materialize()
}
.share(replay: 1)
.elements()
.debug("Response", trimOutput: false)
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
Observable
.merge(request.map{ _ in true },
response.map { _ in false },
error.map { _ in false })
.bind(to: loading)
.disposed(by: disposeBag)
}
}
The refreshTrigger and loadNextPageTrigger is bind to difference target likes:
self.tableView.rx_reachedBottom
.map { _ in () }
.bind(to: self.viewModel.loadNextPageTrigger)
.disposed(by: disposeBag)
self.refreshControl.rx.controlEvent(.valueChanged)
.bind(to: self.viewModel.refreshTrigger)
.disposed(by: disposeBag)
self.rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.map { _ in () }
.bind(to: viewModel.refreshTrigger)
.disposed(by: disposeBag)
Question:
When I scroll the tableView to bottom and trigger the loadNextPageTrigger, everything works fine.
However, if there is no more data in the next coming request, the loadnextPageTrigger will be triggered infinitely.
Any help would be appreciated.
You can download the Demo here.
Main idea is to check if all elements is loaded. I found in source code that you load parts by 20, so this condition should works fine if loadedPosts < 20 then stop loading new part. Here i share with you basic solution that you can refactor as you want (because i'm not good at RxSwift):
In MomentViewModel you should declare property
private var isAllLoaded = false
that set to true if you loaded all values. Then you should check every [Post] that came in response to set correct isAllLoaded:
Observable
.combineLatest(request, response, posts.asObservable()) { request, response, posts in
self.isAllLoaded = response.count < 20 // here the check
return self.pageIndex == 0 ? response : posts + response
}
.sample(response)
.bind(to: posts)
.disposed(by: disposeBag)
And then in nextPageRequest in you should return .empty() observer if all parts have been loaded:
if loading {
return Observable.empty()
} else {
guard !self.isAllLoaded else { return Observable.empty() }
return Observable<Int>.create { [unowned self] observer in
self.pageIndex += 1
print(self.pageIndex)
observer.onNext(self.pageIndex)
observer.onCompleted()
return Disposables.create()
}
}
P.S. The MomentViewModel.swift file to copy/paste.
Related
So i made a simple reactive tableview to learn rxSwift, I got difficulty on accessing the data in my ViewModel. so I created I think a very bad approach on how to get the data from my viewmodel
tableView.rx.itemSelected
.subscribe(onNext:{ [weak self] indexPath in
print("Tapped \(String(describing: self?.hospitalListVM?.result.value[0].id))")
}).disposed(by: disposeBag)
I don't think that 's rx way on handling data. are there any better solution for my problem about accessing data in my viewModel?
my ViewModel
class HospitalListViewModel:APIRequest {
var baseURL:String = "https://"
var method: RequestType = .GET
var result = BehaviorRelay<[RumahSakitModel]>(value: [])
var isLoaded = BehaviorRelay<Bool>(value: false)
func fetchData() {
guard let url = URL(string:baseURL) else { return }
let session = URLSession.shared.dataTask(with: url) { data, _,_ in
guard let data = data else { return }
do {
let hasil = try JSONDecoder().decode([RumahSakitModel].self, from: data)
self.result.accept(hasil)
self.isLoaded.accept(true)
} catch let err {
print(err)
}
}
session.resume()
}
}
How I bind my tablewView
hospitalListVM?
.result
.bind(to: tableView.rx.items(cellIdentifier: HospitalTableCell.identifier, cellType: HospitalTableCell.self)) { _, post, cell in
cell.configureSubView(title: post.nama ?? " ", subTitle: post.jenis ?? " ")}
.disposed(by: disposeBag)
The two standard ways of finding the selected object are here:
let rumahSakitModel1 = tableView.rx.itemSelected
.withLatestFrom(hospitalListVM!.result) { $1[$0.row] }
let rumahSakitModel2 = tableView.rx.modelSelected(RumahSakitModel.self)
With either of the above, you can then do:
rumahSakitModelX
.subscribe(onNext: { model in
print("Tapped \(model.id)")
})
.disposed(by: disposeBag)
I have a ContentService that makes a request for an article. That article response contains an authorId property.
I have a ProfileService that allows me to request a user profile by an userId.
I am trying to request an article from the ContentService, chain on a request once that completes to the ProfileService using the authorId property, I would then like to return a ContentArticleViewModel that contains both the article and profile information.
My ArticleInteractor looks something like this -
final class ArticleInteractor: ArticleInteractorInputProtocol {
let fetchArticleTrigger = PublishSubject<String>()
private lazy var disposeBag = DisposeBag()
weak var output: ArticleInteractorOutputProtocol? {
didSet {
configureSubscriptions()
}
}
private func configureSubscriptions() {
guard let output = output else { return }
fetchArticleTrigger
.bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
.disposed(by: disposeBag)
dependencies.contentSvc.fetchContentByIdResponse
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
}
}
Quite simply fetchArticleTrigger starts a request, I then bind on dependencies.contentSvc.fetchContentByIdResponse and pick up the response.
The method on my ContentService is -
// MARK:- FetchContentById
// #params: id - String
// return: PublishSubject<ContentArticle>
fetchContentByIdTrigger
.flatMapLatest { [unowned self] in self.client.request(.getContentById(id: $0)) }
.map { (resp: Result<ContentArticle>) in
guard case .success(let props) = resp else { return ContentArticle() }
return props
}
.bind(to: fetchContentByIdResponse)
.disposed(by: disposeBag)
I have a very similair setup on my ProfileService -
// MARK:- FetchUserProfileById
// #params: id - String
// return: PublishSubject<User>
fetchUserProfileByIdTrigger
.flatMapLatest { [unowned self] in self.client.request(.getProfileByUserId(id: $0)) }
.map { (resp: Result<User>) in
guard case .success(let props) = resp else { return User() }
return props
}
.bind(to: fetchUserProfileByIdResponse)
.disposed(by: disposeBag)
I imagine I will create a model for the article, something like -
struct ContentArticleViewModel {
var post: ContentArticle
var user: User
}
I was imaging something like this pseudo code within my ArticleInteractor-
dependencies.contentSvc.fetchContentByIdResponse
.flatMapLatest { article in
/* fetch profile using `article.authorId */
}.map { article, profile in
return ContentArticleViewModel(post: article, user: profile)
}
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
But I am completely lost how best to handle this. I have seen a number of articles on chaining requests but am struggling to apply anything successfully.
EDIT
I have something working currently -
private func configureSubscriptions() {
guard let output = output else { return }
fetchArticleTrigger
.bind(to: dependencies.contentSvc.fetchContentByIdTrigger)
.disposed(by: disposeBag)
dependencies.contentSvc.fetchContentByIdResponse
.do(onNext: { [unowned self] article in self.dependencies.profileSvc.fetchUserProfileByIdTrigger.onNext(article.creator.userId)})
.bind(to: fetchArticleResponse)
.disposed(by: disposeBag)
let resp = Observable.combineLatest(fetchArticleResponse, dependencies.profileSvc.fetchUserProfileByIdResponse)
resp
.map { [unowned self] in self.enrichArticleAuthorProps(article: $0, user: $1) }
.bind(to: output.fetchArticleResponse)
.disposed(by: disposeBag)
}
private func enrichArticleAuthorProps(article: ContentArticle, user: User) -> ContentArticle {
var updatedArticle = article
updatedArticle.creator = user
return updatedArticle
}
I am not sure this is correct however.
I'm not sure why you have so much code for such a small job. Below is an example that does what you describe (downloads the article, the downloads the author profile and emits both) with far less code and even it is more code than I would normally use.
protocol ContentService {
func requestArticle(id: String) -> Observable<Article>
func requestProfile(id: String) -> Observable<User>
}
class Example {
let service: ContentService
init(service: ContentService) {
self.service = service
}
func bind(trigger: Observable<String>) -> Observable<(Article, User)> {
let service = self.service
return trigger
.flatMapLatest { service.requestArticle(id: $0) }
.flatMapLatest {
Observable.combineLatest(Observable.just($0), service.requestProfile(id: $0.authorId))
}
}
}
Or maybe you want to display the article while waiting for the author profile to download. In that case something like this:
func bind(trigger: Observable<String>) -> (article: Observable<Article>, author: Observable<User>) {
let service = self.service
let article = trigger
.flatMapLatest { service.requestArticle(id: $0) }
.share(replay: 1)
let author = article
.flatMapLatest {
service.requestProfile(id: $0.authorId)
}
return (article, author)
}
So, I have a button and will make an API request upon tapping it. When the API request returns an error, if my understanding is correct, the sequence will be terminated and no subsequent action will be recorded. How do I handle this properly so that I can still make another API request when tapping the button.
My thoughts are to have two observables that I can subscribe to in ViewController and on button pressed, one of it will print the success response and one of it will print the error. Just not quite sure how I can achieve that.
PS: In Post.swift, I have purposely set id as String type to fail the response. It should have be an Int type.
Post.swift
import Foundation
struct Post: Codable {
let id: String
let title: String
let body: String
let userId: Int
}
APIClient.swift
class APIClient {
static func request<T: Codable> (_ urlConvertible: URLRequestConvertible, decoder: JSONDecoder = JSONDecoder()) -> Observable<T> {
return Observable<T>.create { observer in
URLCache.shared.removeAllCachedResponses()
let request = AF.request(urlConvertible)
.responseDecodable (decoder: decoder) { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
}
PostService.swift
class PostService {
static func getPosts(userId: Int) -> Observable<[Post]> {
return APIClient.request(PostRouter.getPosts(userId: userId))
}
}
ViewModel.swift
class LoginLandingViewModel {
struct Input {
let username: AnyObserver<String>
let nextButtonDidTap: AnyObserver<Void>
}
struct Output {
let apiOutput: Observable<Post>
let invalidUsername: Observable<String>
}
// MARK: - Public properties
let input: Input
let output: Output
// Inputs
private let usernameSubject = BehaviorSubject(value: "")
private let nextButtonDidTapSubject = PublishSubject<Void>()
// MARK: - Init
init() {
let minUsernameLength = 4
let usernameEntered = nextButtonDidTapSubject
.withLatestFrom(usernameSubject.asObservable())
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ -> Observable<Post> in
PostService.getPosts(userId: 1)
.map({ posts -> Post in
return posts[0]
})
}
let invalidUsername = usernameEntered
.filter { text in
text.count < minUsernameLength
}
.map { _ in "Please enter a valid username" }
input = Input(username: usernameSubject.asObserver(),
nextButtonDidTap: nextButtonDidTapSubject.asObserver())
output = Output(apiOutput: apiOutput,
invalidUsername: invalidUsername)
}
deinit {
print("\(self) dellocated")
}
}
ViewController
private func configureBinding() {
loginLandingView.usernameTextField.rx.text.orEmpty
.bind(to: viewModel.input.username)
.disposed(by: disposeBag)
loginLandingView.nextButton.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.bind(to: viewModel.input.nextButtonDidTap)
.disposed(by: disposeBag)
viewModel.output.apiOutput
.subscribe(onNext: { [unowned self] post in
print("Valid username - Navigate with post: \(post)")
})
.disposed(by: disposeBag)
viewModel.output.invalidUsername
.subscribe(onNext: { [unowned self] message in
self.showAlert(with: message)
})
.disposed(by: disposeBag)
}
You can do that by materializing the even sequence:
First step: Make use of .rx extension on URLSession.shared in your network call
func networkCall(...) -> Observable<[Post]> {
var request: URLRequest = URLRequest(url: ...)
request.httpMethod = "..."
request.httpBody = ...
URLSession.shared.rx.response(request)
.map { (response, data) -> [Post] in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
let jsonDictionary = json as? [[String: Any]]
else { throw ... } // Throw some error here
// Decode this dictionary and initialize your array of posts here
...
return posts
}
}
Second step, materializing your observable sequence
viewModel.networkCall(...)
.materialize()
.subscribe(onNext: { event in
switch event {
case .error(let error):
// Do something with error
break
case .next(let posts):
// Do something with posts
break
default: break
}
})
.disposed(by: disposeBag)
This way, your observable sequence will never be terminated even when you throw an error inside your network call, because .error events get transformed into .next events but with a state of .error.
So I have also found the way to achieve what I wanted, which is assigning the success output and error output into two different observable respectively. By using RxSwiftExt, there are two additional operators, elements() and errors() which can be used on an observable that is materialized to get the element.
Here is how I did it,
ViewModel.swift
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ in
PostService.getPosts(userId: 1)
.materialize()
}
.share()
let apiSuccess = apiOutput
.elements()
let apiError = apiOutput
.errors()
.map { "\($0)" }
Then, just subscribe to each of these observables in the ViewController.
As reference: http://adamborek.com/how-to-handle-errors-in-rxswift/
I've been trying to understand rxSwift. I faced with request problem and want to implement this in a good way. Currently, I'm using this code:
enum RequestState<T> {
case loading
case loaded(T)
case error(Error)
}
struct Response<T: Decodable>: Decodable {
let data: T
let error: ResponseError?
}
searchBar.rx.text.asObservable()
.flatMap { self.provider.rx.request(Request(query: $0)) }
.map({ RequestState<Response<Bool>>.loaded($0) })
.asDriver(onErrorRecover: { error in
return Driver.just(.error(error))
})
.startWith(.loading)
.drive(onNext: { state in
switch state {
case .loading: ()
case .loaded(let response): ()
case .error(let error): ()
}
})
.disposed(by: disposeBag)
This works good but not too convenient to work with data and request state. I saw in rxSwift demo project following code.
struct RequestState<T: Decodable> {
let isLoading: Bool
let data: T
let error: ResponseError?
}
let state = viewModel.requestMethod()
state
.map { $0.isLoading }
.drive(self.loaderView.isOffline)
.disposed(by: disposeBag)
state
.map { $0.data }
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
state
.map { $0.error }
.drive(onNext: { error in
showAlert(error)
})
.disposed(by: disposeBag)
And my problem in the following method, I can't understand Rx magic here:
func requestMethod() -> Driver<RequestState> {
// supper code
}
Can someone advise me what I have to do here?
Here's what I ended up with while looking at both of your code samples:
First here is the point of use:
let request = searchBar.rx.text
.unwrap()
.map { URLRequest.search(forQuery: $0) }
let networkRequest = createRequest(forType: MyType.self)
let state = request
.flatMap(networkRequest)
state
.map { $0.isLoading }
.bind(to: loaderView.isOffline)
.disposed(by: bag)
state
.map { $0.data }
.unwrap()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
state
.map { $0.error }
.unwrap()
.subscribe(onNext: showAlert)
.disposed(by: bag)
Here is the support code for the above:
enum RequestState<T> {
case loading
case loaded(T)
case error(Error)
var isLoading: Bool {
guard case .loading = self else { return false }
return true
}
var data: T? {
guard case let .loaded(t) = self else { return nil }
return t
}
var error: Error? {
guard case let .error(e) = self else { return nil }
return e
}
}
You will see that the above RequestState enum is a bit of an amalgamation of both RequestState types you showed in your example. The enum makes it easy to create the object while the computed properties make it easy to extract the information.
func createRequest<T>(forType type: T.Type, session: URLSession = URLSession.shared) -> (URLRequest) -> Observable<RequestState<T>> where T: Decodable {
return { request in
return Observable.create { observer in
observer.onNext(.loading)
let disposable = session.rx.data(request: request)
.subscribe { event in
switch event {
case let .error(error):
observer.onNext(.error(error))
case let .next(data):
do {
let item = try JSONDecoder().decode(type, from: data)
observer.onNext(.loaded(item))
}
catch {
observer.onNext(.error(error))
}
case .completed:
observer.onCompleted()
}
}
return Disposables.create([disposable])
}
}
}
The above is a factory function. You use it to create a function that knows how to make network requests for the appropriate type. Recall in the code where it's being used I had let networkRequest = createRequest(forType: MyType.self). This line produces a function networkRequest that takes a URLRequest and returns an Observable that has been specialized for the type in question.
When the Observable from networkRequest is subscribed to, it will immediately push out a .loading case, then make the request. Then it will use the response to either push out a .loaded(T) or an .error(Error) depending on the results.
Personally, I'm more inclined to use something like the ActivityIndicator system from the examples in the RxSwift repository instead.
I can't manage to get this solution to work:
https://github.com/liuznsn/RxMoyaPaginationNetworking
Maybe someone can tell me where is the mistake. The loading variable never goes to false. I guess the issue is in the request observable, but I can't find out why.
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
self.offset += 1
return self.loadData(offset: self.offset)
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { events -> Observable<[T1]> in
request
.do(onError: { error in
self.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map { _ in true },
response.map { $0.count == 0 },
error.map { _ in false }
)
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
Thank you Daniel for help, here completed solution
Call example:
goodsModel = GoodsNetworkModel()
goodsModel.elements.asObservable().bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: StoreCell.self)) { (ip, item: Goods, cell: StoreCell) in
cell.configure(goods: item)
}.addDisposableTo(rx_disposeBag)
tableView.rx.itemSelected.bind() { [unowned self] ip in
self.didSelectRow(ip: ip.row)
}.addDisposableTo(rx_disposeBag)
rx.sentMessage(#selector(UIViewController.viewDidAppear(_:)))
.map { _ in () }
.bind(to: goodsModel.refreshTrigger)
.addDisposableTo(rx_disposeBag)
tableView.rx_reachedBottom
.map{ _ in ()}
.bind(to: goodsModel.loadNextPageTrigger)
.addDisposableTo(rx_disposeBag)
Model code:
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.offset += 1
observer.onNext(self.offset)
observer.onCompleted()
return Disposables.create()
}
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { offset -> Observable<[T1]> in
self.loadData(offset: offset)
.do(onError: { [weak self] error in
self?.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map{_ in true},
response.map { $0.count == 0 },
error.map { _ in false })
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
The problem is here:
let refreshRequest: Observable<[T1]> = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
refreshRequest doesn't emit a value until after loadData returns. The way your code is structured, emitting a signal on the refreshTrigger will start the network request, and then set loading to true after the network request completes.
It would be better to have refreshRequest and nextPageRequest return an Observable of what page to load and then merge them and call flatMap with the network call on the merged result.