The following code works:
let provider = RxMoyaProvider<MyAPI>( stubClosure: MoyaProvider.delayedStub(3))
provider
.request(.studentSearch(query: ""))
.retry(3)
.observeOn(MainScheduler.instance)
.asObservable()
.mapJSON()
.map { respJSON in
guard let studentsJsonArray = JSON(respJSON)["students"].array else {
throw APIError.wrongJSONParsing
}
return studentsJsonArray.map {
guard let students = Student.fromJSON($0) else {
fatalError("Invalid Student Object")
}
return students
} as [Student]
}
.subscribe(onNext: {
print($0)
}, onCompleted: {
print($0) // This one is being called.
})
.disposed(by: rx.disposeBag)
The method onCompleted is being called in the above code But not in the following one.
I am trying to do it with refresh trigger like the one in the UITableView for refreshing the content. I want to load the contents on start so I use startWith(()) in the following code in my ViewModel
let results: Driver<[Student]>
var refreshTrigger = PublishSubject<Void>()
results = refreshTrigger
.startWith(())
.do(onNext: {
execute.value = true
})
.flatMapLatest {
provider
.request(.studentSearch(query: ""))
.retry(3)
.observeOn(MainScheduler.instance)
.asObservable()
}
.mapJSON()
.map { respJSON in
guard let studentsJsonArray = JSON(respJSON)["students"].array else {
throw APIError.wrongJSONParsing
}
return studentsJsonArray.map {
guard let students = Student.fromJSON($0) else {
fatalError("Invalid Student Object")
}
return students
}
}
.do(onNext: {
items.value = $0
execute.value = false
noResults.value = items.value.isEmpty
}, onCompleted: {
print($0)
})
.asDriver(onErrorJustReturn: [])
and in the controller I call following in viewdidload.
viewModel
.results
.asObservable()
.map { StudentGroup(header: "Follower", items: $0) }
.subscribe(onNext: {
print($0)
}, onCompleted: {
print($0) // This is not being called.
})
.disposed(by: rx.disposeBag)
Here onCompleted is not being called. I don't know the reason why?. Please help me out.
flatMapLatest will not send an onCompleted event unless its input (the PublishSubject in your case) completes.
The Moya requests send their own onCompleted events alright, but those are filtered out when flatMapLatest merges the results.
In other words, there is a long-lived subscription from the PublishSubject to the data source which doesn't complete, and it's not supposed to, either. (edited)
(Otherwise you'd lose the refresh functionality after the first load.)
I got help from Zsolt Varadi from RxSwift Slack channel.
here is the solution
results = refreshTrigger
.startWith(())
.do(onNext: {
execute.value = true
})
.flatMapLatest {
provider
.request(.studentSearch(query: ""))
.retry(3)
.observeOn(MainScheduler.instance)
.asObservable()
.mapJSON()
.map { respJSON in
guard let studentsJsonArray = JSON(respJSON)["students"].array else {
throw APIError.wrongJSONParsing
}
return studentsJsonArray.map {
guard let students = Student.fromJSON($0) else {
fatalError("Invalid Student Object")
}
return students
} as [Student]
}
.map { StudentGroup(header: "Follower", items: $0) }
.toArray()
.catchErrorJustReturn([])
.do(onNext: {
studentGroups.value = $0
execute.value = false
guard let isEmpty = studentGroups.value.first?.items.isEmpty else {
return
}
noResults.value = isEmpty
})
}
.asDriver(onErrorJustReturn: [])
Related
I've got a function that will return an observable that will emit an enum based on what stage it is currently on. The enum basically goes like this:
enum ViewModelAction {
case inputEnabled(isEnabled: Bool)
case loadingIndicatorShown(isShown: Bool)
case errorMessageShownIfAvailable(error: Error?)
case loadUIData(with entity: Entity)
case startSession(for entity: Entity)
case endSession(for entity: Entity)
case loadEndUIData(with entity: Entity)
}
Now what I want the function do is like so:
Function start with input params -> go to 2
Do:
Emit disable input from UI
Emit show loading indicator
Call API to create session based on input params -> API returns session data -> go to 3
Do:
If success -> go to 4
If error:
Emit enable input from UI
Emit hide loading indicator
Emit show error with the error -> Complete stream
Do:
Loop and call API to poll the entity based on the session with the status of READY:
If success with READY status -> Emit load UI data with entity -> go to 5
If success with non READY status -> loop again / back to 4
If error -> Emit show error with the error -> Loop again / back to 4
Do:
Call API to start session using the entity -> API returns session data -> go to 6
Do:
If success:
Emit enable input from UI
Emit hide loading indicator
Emit start session for entity -> Complete stream
If error:
Emit enable input from UI
Emit hide loading indicator
Emit show error with the error -> Complete stream
So in a way, for a successful run, it will emit this exact sequence:
Observable.merge(
.just(ViewModelAction.inputEnabled(isEnabled: false)),
.just(ViewModelAction.loadingIndicatorShown(isShown: true)),
.just(ViewModelAction.loadUIData(with: entity)),
.just(ViewModelAction.inputEnabled(isEnabled: true)),
.just(ViewModelAction.loadingIndicatorShown(isShown: false)),
.just(ViewModelAction.startSession(for: entity)),
.complete()
)
I have actually done this, but I was thinking that it is not that clear and very complicated.
protocol SessionHandlerProtocol {
func create(for userId: String) -> Observable<SessionData>
func start(session: SessionData, entity: Entity) -> Observable<Void>
}
protocol EntityPollerProtocol {
var entity: Observable<Entity?> { get }
func startPolling(for session: SessionData) //Will poll and emit to entity observable
finc stopPolling()
}
class StartSessionStrategy {
private let sessionHandler: SessionHandlerProtocol
private let entityPoller: EntityPollerProtocol
init(sessionHandler: SessionHandlerProtocol,
entityPoller: EntityPollerProtocol) {
self.sessionHandler = sessionHandler
self.entityPoller = entityPoller
}
func handleSession(with userId: String) -> Observable<ViewModelAction> {
let initialUIObservable =
Observable.from(ViewModelAction.inputEnabled(isEnabled: false),
ViewModelAction.loadingIndicatorShown(isShown: true))
let sharedCreateSession =
sessionHandler.create(for: userID).materialize.share()
let createSessionError =
sharedCreateSession
.flatMapLatest {
switch $0 {
case .error(let error):
return Observable.merge(
.just(ViewModelAction.inputEnabled(isEnabled: true)),
.just(ViewModelAction.loadingIndicatorShown(isShown: false)),
.just(ViewModelAction.errorMessageShownIfAvailable(error: error)),
.complete()
)
default:
return .never()
}
let createSessionSuccess =
sharedCreateSession
.flatMapLatest {
switch $0 {
case .next(let element):
return .just(element)
default:
return .never()
}
let sharedEntityPoller =
createSessionSuccess
.do(onNext: { [weak self] in self?.entityPoller.startPolling(for: $0) })
.withLatestFrom(entityPoller.entity) { return ($0, $1) }
.materialize()
.share()
let entityPollerError =
sharedEntityPoller
.flatMapLatest {
switch $0 {
case .error(let error):
return .just(ViewModelAction.errorMessageShownIfAvailable(error: error))
default:
return .never()
}
let entityPollerSuccessWithReadyStatus =
sharedEntityPoller
.filter { (_, entity) entity.status = .ready }
.flatMapLatest {
switch $0 {
case .next(let element):
return .just(element)
default:
return .never()
}
.do(onNext: { [weak self] _ in self?.stopPolling() })
let doOnEntityPollerSuccessWithReadyStatus =
entityPollerSuccessWithReadyStatus
.map { return ViewModelAction.loadUIData(with: $0.1) }
let sharedStartSession =
entityPollerSuccessWithReadyStatus
.flatMapLatest { [weak self] (session, entity) in
self?.sessionHandler
.start(session: userID, entity: entity)
.map { return (session, entity) }
}.materialize.share()
let startSessionError =
sharedStartSession
.flatMapLatest {
switch $0 {
case .error(let error):
return Observable.merge(
.just(ViewModelAction.inputEnabled(isEnabled: true)),
.just(ViewModelAction.loadingIndicatorShown(isShown: false)),
.just(ViewModelAction.errorMessageShownIfAvailable(error: error)),
.complete()
)
default:
return .never()
}
let startSessionSuccess =
sharedStartSession
.flatMapLatest {
switch $0 {
case .next(let element):
return Observable.merge(
.just(ViewModelAction.inputEnabled(isEnabled: true)),
.just(ViewModelAction.loadingIndicatorShown(isShown: false)),
.just(ViewModelAction.startSession(for: element.1)),
.complete
)
default:
return .never()
}
return Observable.merge(
initialUIObservable,
createSessionError,
entityPollerError,
doOnEntityPollerSuccessWithReadyStatus,
startSessionError,
startSessionSuccess
)
}
}
As you can see, the function is pretty big and not that clear. Do you have a suggestion how to refactor this into a cleaner code? Thanks.
If you start with a flow chart, you will end up with an imperative solution. Instead consider each side effect independently. Make each observable sequence a declaration of what causes that side effect.
The essence of your code is this (Note, I wrapped up your entity poller into a more reasonable interface. That wrapper is shown later):
let errors = PublishSubject<Error>()
let session = sessionHandler.create(for: userId)
.catch { errors.onSuccess($0); return .empty() }
.share()
let entity = session
.flatMapLatest { [entityPoller] in
entityPoller($0)
.catch { errors.onSuccess($0); return .empty() }
}
.share()
let started = entity
.compactMap { $0 }
.filter { $0.status == .ready }
.withLatestFrom(session) { ($1, $0) }
.flatMapLatest { [sessionHandler] session, entity in
sessionHandler.start(session: session, entity: entity)
.catch { errors.onSuccess($0); return .empty() }
}
.take(1)
.share()
Everything else is just notifications. I think showing the above, cleanly and without complications, is a huge simplification and will aid in other's understanding your code (including future you.)
Here is what I ended up with, including the wrapper around the poller I mentioned above:
class StartSessionStrategy {
private let sessionHandler: SessionHandlerProtocol
private let entityPoller: (SessionData) -> Observable<Entity?>
init(sessionHandler: SessionHandlerProtocol, poller: EntityPollerProtocol) {
self.sessionHandler = sessionHandler
self.entityPoller = entityPolling(poller)
}
func handleSession(with userId: String) -> Observable<ViewModelAction> {
// the fundamental operations as above:
let errors = PublishSubject<Error>()
let session = sessionHandler.create(for: userId)
.catch { errors.onSuccess($0); return .empty() }
.share()
let entity = session
.flatMapLatest { [entityPoller] in
entityPoller($0)
.catch { errors.onSuccess($0); return .empty() }
}
.share()
let started = entity
.compactMap { $0 }
.filter { $0.status == .ready }
.withLatestFrom(session) { ($1, $0) }
.flatMapLatest { [sessionHandler] session, entity in
sessionHandler.start(session: session, entity: entity)
.catch { errors.onSuccess($0); return .empty() }
}
.take(1)
.share()
// now go through all the notifications:
// input is disabled at start, then enabled once started or if an error occurs
let inputEnabled = Observable.merge(
started,
errors.take(until: started).map(to: ())
)
.map(to: ViewModelAction.inputEnabled(isEnabled: true))
.startWith(ViewModelAction.inputEnabled(isEnabled: false))
.take(2)
// show the loading indicator at start, remove it once started or if an error occurs
let loadingIndicator = Observable.merge(
started,
errors.take(until: started).map(to: ())
)
.map(to: ViewModelAction.loadingIndicatorShown(isShown: false))
.startWith(ViewModelAction.loadingIndicatorShown(isShown: true))
.take(2)
// emit the loadUIData if an entity is ready.
let loadUIData = entity
.compactMap { $0 }
.filter { $0.status == .ready }
.map { ViewModelAction.loadUIData(entity: $0) }
.take(1)
// emit the startSession event once the session starts.
let startSession = started
.withLatestFrom(entity.compactMap { $0 })
.map { entity in ViewModelAction.startSession(entity: entity) }
.take(until: errors)
return Observable.merge(
inputEnabled,
loadingIndicator,
errors
.take(until: started)
.map { ViewModelAction.errorMessageShownIfAvailable(error: $0) }, // emit an error message for all errors.
loadUIData,
startSession
)
}
}
func entityPolling(_ poller: EntityPollerProtocol) -> (SessionData) -> Observable<Entity?> {
{ session in
Observable.create { observer in
let disposable = poller.entity
.subscribe(observer)
poller.startPolling(for: session)
return Disposables.create {
disposable.dispose()
poller.stopPolling()
}
}
}
}
extension ObserverType {
func onSuccess(_ element: Element) -> Void {
onNext(element)
onCompleted()
}
}
extension ObservableType {
func map<T>(to: T) -> Observable<T> {
return map { _ in to }
}
}
I want to get data from server and update my DB after that I'll show received data to the user. For this goal I have a method(getData()) in my view model that returns a Single I call and subscribe this method in the view controller(myVC.getData.subscribe({single in ...})) in this method at first I call and subscribe(#1)(getUnread()->Single) the method run but I can not get the single event, I can not understand why I can't get the event(#3) in call back(#4)
after that I want to save data with calling(#2)(save([Moddel])->single)
//I removed some part of this code it was to big
//This method is View Model
func getData() -> Single<[Model]> {
return Single<[Model]>.create {[weak self] single in
//#1
self!.restRepo.getUnread().subscribe({ [weak self] event in
//#4
switch event {
case .success(let response):
let models = response
//#2
self!.dbRepo.save(issues!).subscribe({ event in
switch event {
case .success(let response):
let models = response
single(.success(models))
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
case .error(let error):
single(.error(error))
}
}).disposed(by: self!.disposeBag)
return Disposables.create()
}
}
.
.
//I removed some part of this code it was to big
//This method is in RestRepo class
func getUnread() -> Single<[Model]> {
return Single<[Model]>.create { single in
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
var urlRequest = URLRequest(url: urlComponent.url!)
ApiHelper.instance.alamofire.request(urlRequest).intercept().responseJSON { response in
debugPrint(response)
let statusCode = response.response?.statusCode
switch statusCode {
case 200:
do {
let models = try JSONDecoder().decode([Model].self, from: response.data!)
//#3
single(.success(models))
}catch{
print(error)
}
case 304:
debugPrint(response)
default:
single(.error(IssueResponseStatusCodeError(code: statusCode ?? 0)))
}
}
return Disposables.create()
}
First you need to change your thinking. You don't do anything in the app. At best, you lay out the Observable chains (which don't do anything anymore than water pipes "do" something.) Then you start the app and let the "water" flow.
So with that in mind, let's examine your question:
I want to get data from server...
It's not that "you" want to get the data. The request is made as a result of some action (probably a button tap) by the user or by some other side effect. What action is that? That needs to be expressed in the code. For the following I will assume it's a button tap. That means you should have:
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
The thing to notice here is that getUnread() is an effect that is caused by button.rx.tap. The above establishes a cause-effect chain.
Your question goes on to say "you" want to:
... update my DB...
Here, the cause is the network request and the effect is the DB save so we simply need to add this to the viewDidLoad (note that the code below uses RxEnumKit.):
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
Your question also says that "you" want to:
... show received data to the user.
Note here that showing the received data to the user has nothing to do with the DB save. They are two independent operations that can be done in parallel.
Showing the received data to the user has the serverResponse as the cause, and the showing as the effect.
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Lastly, you don't mention it, but you also have to handle the errors:
So add this to the viewDidLoad as well:
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
The code below is all of the above as a single block. This compiles fine...
import UIKit
import RxSwift
import RxCocoa
import EnumKit
import RxEnumKit
extension Result: CaseAccessible { }
class Example: UIViewController {
var button: UIButton!
var restRepo: RestRepo!
var dbRepo: DBRepo!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let serverResponse = button.rx.tap
.flatMapLatest { [restRepo] in
restRepo!.getUnread()
.map { Result<[Model], Error>.success($0) }
.catchError { .just(Result<[Model], Error>.failure($0)) }
}
.share(replay: 1)
let dbResponse = serverResponse
.capture(case: Result.success)
.flatMapLatest { [dbRepo] models in
dbRepo!.save(models)
.map { Result<Void, Error>.success(()) }
.catchError { .just(Result<Void, Error>.failure($0)) }
}
serverResponse
.capture(case: Result.success)
.subscribe(onNext: { models in
print("display the data to the user.", models)
})
.disposed(by: disposeBag)
Observable.merge(serverResponse.capture(case: Result.failure), dbResponse.capture(case: Result.failure))
.subscribe(onNext: { error in
print("an error occured:", error)
})
.disposed(by: disposeBag)
}
}
protocol RestRepo {
func getUnread() -> Observable<[Model]>
}
protocol DBRepo {
func save(_ models: [Model]) -> Observable<Void>
}
struct ProductionRestRepo: RestRepo {
func getUnread() -> Observable<[Model]> {
let urlComponent = ApiHelper.instance.dolphinURLComponents(for: ApiHelper.ISSUES_PATH)
let urlRequest = URLRequest(url: urlComponent.url!)
return URLSession.shared.rx.data(request: urlRequest)
.map { try JSONDecoder().decode([Model].self, from: $0) }
}
}
class ApiHelper {
static let ISSUES_PATH = ""
static let instance = ApiHelper()
func dolphinURLComponents(for: String) -> URLComponents { fatalError() }
}
struct Model: Decodable { }
I hope all this helps you, or at least generates more questions.
Lets say I have a function that takes in two arguments - Observable<String?> and Observable<Person?> and returns Observable
This is my sample code below:
return Observable.combineLatest(self.id, personStream)
map { [weak self] (id,person) -> Person? in
guard let person = person as? Person else {
return nil
}
if let id = id {
// How do I return this updated person
self?.updatePersonDetails(for: person, completion: { (updatedPerson) in
return updatedPerson
})
} else {
return person
}
}
How do I return this updated person inside this mapping function?
Update:
public func updatePerson(personStream: Observable<Person>) -> Observable<Person> {
return Observable.combineLatest(
self.id,
personStream
).flatMap { [weak self] (id,person) -> Observable<Person?> in
guard let person = person as? Double else {
return Observable.just(nil)
}
if let id = id {
// How do I return this updated person
return Observable.create { [weak self] observer in
self?.updatePersonDetails(for: person, completion: { (updatedPerson) in
observer.on(.next(updatedPerson))
observer.on(.completed)
})
return Disposables.create()
}
} else {
return Observable.just(person)
}
}
}
The above function is called as below:
let personUpdateStream = personService.updatePerson(personStream: personDataStream)
Observable.combineLatest(personUpdateStream, nameStream,profileEnabledStream)
.subscribe(onNext: { [weak self] (person, name, isEnabled) in
print(name)
self?.updatePersonView(person: person, name: name, isLocked: !isEnabled)
})
.disposed(by: disposeBag)
Issue: The block inside combineLatest with the print(name) never runs. May I know whats the issue here?
Description of personUpdateStream
personUpdateStream RxSwift.FlatMap<(String?, Person?), RxSwift.Observable<Person?>> 0x0000604001e5af70
You should use flatMap instead of map and to integrate your non Rx call of updatePersonDetails you should use Observable.create.
Your final code should look something similar to:
return Observable.combineLatest(
self.id,
personStream
).flatMap { [weak self] (id,person) -> Observable<Person?> in
guard let person = person as? Double else {
return Observable.just(nil)
}
if let id = id {
// How do I return this updated person
return Observable.create { [weak self] observer in
self?.updatePersonDetails(for: person, completion: { (updatedPerson) in
observer.on(.next(updatedPerson))
observer.on(.completed)
})
return Disposables.create()
}
} else {
return Observable.just(person)
}
}
struct SearchViewModel {
lazy var rx_SearchResults: Driver<[Repository]> = self.fetchSearchResults()
lazy var rx_FilteredSearchResults: Driver<[Repository]> = self.filteredSearchResults()
fileprivate var searchQuery: Observable<String>
fileprivate var scopeIndex: Observable<Int>
init( searchTextObservable: Observable<String>, changeInScopeIndex: Observable<Int>)
{
self.searchQuery = searchTextObservable
self.scopeIndex = changeInScopeIndex
}
fileprivate func fetchRepositories() -> Driver<[Repository]> {
return repositoryName
.subscribeOn(MainScheduler.instance) // Make sure we are on MainScheduler
.do(onNext: { response in
UIApplication.shared.isNetworkActivityIndicatorVisible = true
})
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.flatMapLatest { text in // .background thread, network request
return RxAlamofire
.requestJSON(.get, "https://api.github.com/users/\(text)/repos")
.debug()
.catchError { error in
return Observable.never()
}
}
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.map { (response, json) -> [Repository] in // again back to .background, map objects
if let repos = Mapper<Repository>().mapArray(JSONObject: json) {
return repos
} else {
return []
}
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.do(onNext: { response in
UIApplication.shared.isNetworkActivityIndicatorVisible = false
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
}
Now i am trying to filteredSearchResults like this
**fileprivate func filteredSearchResults() -> Driver<[FilesListSearchData]> {
return scopeIndex
.subscribeOn(MainScheduler.instance) // Make sure we are on MainScheduler
.do(onNext: { response in
UIApplication.shared.isNetworkActivityIndicatorVisible = true
})
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))
.map { index in // again back to .background, map objects
return self.rx_SearchResults.asDriver()
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.do(onNext: { response in
UIApplication.shared.isNetworkActivityIndicatorVisible = false
})
}**
// Above method is wrong i am stuck how can i filter data now based on scope index.
As you people understood i am using MVVM I have two quires.
How can show progress on my view controller which is doing here by this line
UIApplication.shared.isNetworkActivityIndicatorVisible = false/true
How can i filter Driver based on selected scope index.
I have a TableView of notification. I want to refresh by pull to refresh using UIRefreshControl. How to do that with rx-swift? This is my code. Why tableView not refreshed after I set value to variable data
var refreshControl = UIRefreshControl()
var disposeBag = DisposeBag()
let loadingData = ActivityIndicator()
var data: Observable<[Notification]>!
override func viewDidLoad() {
super.viewDidLoad()
self.view = v
v.tableView.registerClass(NotificationsViewCell.self, forCellReuseIdentifier: "Cell")
v.tableView.addSubview(refreshControl)
data = getNotifications()
configureTableDataSource()
configureActivityIndicatorsShow()
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
.trackActivity(self.loadingData)
}.subscribe(
onNext: {notification in
print("success")
self.data = Observable.just(notification) // NOT REFRESH TABLEVIEW
},
onError: { error in
print("Error \(error)")
},
onCompleted: {() in
print("complete")
},
onDisposed: {() in
print("disposed")
})
.addDisposableTo(disposeBag)
}
func configureTableDataSource(){
data
.retry(3)
.doOnError{ [weak self] error in
self?.v.emptyLabel.hidden = false
self?.v.retryButton.hidden = false
}
.doOnNext{ [weak self] result in
if result.count == 0 {
self?.v.emptyLabel.hidden = false
self?.v.emptyLabel.text = "Tidak ada bisnis favorit"
} else {
self?.v.emptyLabel.hidden = true
self?.v.retryButton.hidden = true
}
}
.trackActivity(loadingData)
.retryWhen{ _ in
self.v.retryButton.rx_tap
}
.asDriver(onErrorJustReturn: [])
.map{ results in
results.map(NotificationsViewModel.init)
}
.drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
cell.viewModel = viewModel
let tap = UITapGestureRecognizer(target: self, action: #selector(self.goToProfile(_:)))
tap.numberOfTapsRequired = 1
cell.photo.tag = index
cell.photo.addGestureRecognizer(tap)
}
.addDisposableTo(disposeBag)
}
func configureActivityIndicatorsShow(){
loadingData
.driveNext{ isLoading in
if !isLoading {
self.v.indicatorView.stopAnimating()
} else {
self.v.indicatorView.startAnimating()
self.v.retryButton.hidden = true
self.v.emptyLabel.hidden = true
}
}
.addDisposableTo(disposeBag)
loadingData.asObservable()
.bindTo(refreshControl.rx_refreshing)
.addDisposableTo(disposeBag)
}
func getNotifications() -> Observable<[Notification]> {
let parameters = [
"token": NSUserDefaults.standardUserDefaults().objectForKey("token")! as! String
]
return string(.POST, NOTIFICATION_LIST, parameters: parameters)
.map { json in
return Notification.parseJSON(JSON.parse(json)["notifications"])
}
.observeOn(MainScheduler.instance)
}
EDIT::
var data = Variable<[Notification]>([])
override func viewDidLoad() {
getNotifications()
.retry(3)
.doOnError{ [weak self] error in
self?.v.emptyLabel.hidden = false
self?.v.retryButton.hidden = false
}
.doOnNext{ [weak self] result in
if result.count == 0 {
self?.v.emptyLabel.hidden = false
self?.v.emptyLabel.text = "Tidak ada notifikasi"
} else {
self?.v.emptyLabel.hidden = true
self?.v.retryButton.hidden = true
}
}
.trackActivity(loadingData)
.retryWhen{ _ in
self.v.retryButton.rx_tap
}
.bindTo(data)
.addDisposableTo(disposeBag)
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
.doOnError{ [weak self] error in
// This not call after the second pull to refresh if No network connection, so refresh control still appear
self?.refreshControl.endRefreshing()
}
.doOnCompleted{ [weak self] result in
self?.refreshControl.endRefreshing()
}
}.bindTo(data)
.addDisposableTo(disposeBag)
}
func configureTableDataSource(){
datas.asObservable()
.asDriver(onErrorJustReturn: [])
.map{ results in
results.map(NotificationsViewModel.init)
}
.drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
cell.viewModel = viewModel
}
.addDisposableTo(disposeBag)
}
func configureActivityIndicatorsShow(){
loadingData
.driveNext{ isLoading in
if !isLoading {
self.v.indicatorView.stopAnimating()
} else {
self.v.indicatorView.startAnimating()
self.v.retryButton.hidden = true
self.v.emptyLabel.hidden = true
}
}
.addDisposableTo(disposeBag)
}
self.data = Observable.just(notification) is creating a new Observable and sending the new [Notification] element on that Observable, which no one is subscribed to.
You should be using a Subject such as Variable.
// instead of `var data: Observable<[Notification]>!`
let data = Variable<[Notification]>([])
// and then later, when you want to send out a new element:
self.data.value = notification
EDIT: To show you how to use this in conjunction with what you already have.
// this will update `data` upon `refreshControl` value change
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
}
.bindTo(data)
.addDisposableTo(disposeBag)
// this will update `loadingData` when `data` gets a new element
data.asDriver().trackActivity(self.loadingData)
// bind to your table view
data.asDriver().drive(//.....
Also, consider moving the retry and retryWhen to happen sooner, instead of happening downstream where you currently have it (in the table view binding). Instead, I think it should belong in getNotifications.