Binding to a UIRefreshControl after refresh using RxSwift - ios

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.

Related

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.

Not getting Completed event on RxSwift with RxMoya

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: [])

how to filter Driver<[Repository]> objects in rxswift

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.

RxSwift+Moya+Moya_ObjectMapper+MJRefresh, refresh failed?

I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind

LongPolling With rxSwift

I studying rxSwift, and I want to do service for the interaction of c longpolling server to this service imitating a permanent connection. I wrote it, but it seems to me, is not that the decision could have been done better? Is it possible to somehow repeat the Observable, regardless of the error, and depending on longpoll server response.
Can anyone can share the solution? Or help with advice? How it is better to organize? I would like to see a better solution, since only began studying rxswift
class LongPollingService {
public var messageReciver: PublishSubject<EventProtocol> = PublishSubject<EventProtocol>()
private let transport = DefaultTransport()
private let disposeBag = DisposeBag()
private var currentRequestInfo = Variable<LongpollingServerInfo?>(nil)
private var currentRequestDisposable: Disposable?
private var currentLongpollingConnection: Disposable? // Subsribee for request server info
private var eventListener : Disposable?
private var currentReqursiveConnection: Disposable? // Subscriber for event listener from longpoll server
func startObservableEvents() {
getServerConnection()
subscribeServerInfo()
//testing listen events
eventListener = messageReciver.showMessagesInDebugMode().subscribe()
eventListener?.addDisposableTo(disposeBag)
}
func disconnect() {
currentRequestDisposable?.dispose()
currentLongpollingConnection?.dispose()
currentReqursiveConnection?.dispose()
}
private func subscribeServerInfo() {
currentLongpollingConnection = currentRequestInfo
.asObservable()
.filter({$0 != nil})
.subscribe(onNext: { [weak self] (info) in
guard let sSelf = self else { return }
sSelf.subscribeToEvents(timeStamp: info!.ts)
})
currentLongpollingConnection?.addDisposableTo(disposeBag)
}
private func subscribeToEvents(timeStamp: TimeInterval) {
if let serverInfo = currentRequestInfo.value {
currentReqursiveConnection?.dispose()
currentReqursiveConnection = getEventsFromLongpollServer(serverInfo: serverInfo, with: timeStamp)
.flatMap(parseUpdates)
.flatMap(reciveEvents)
.showErrorsSwiftMessagesInDebugMode()
.subscribe(onNext: { [weak self] updates in
guard let sSelf = self else { return }
sSelf.subscribeToEvents(timeStamp: updates)
},
onError: { [weak self] error in
guard let sSelf = self else { return }
if let error = error as? LongPollError {
switch error {
case .olderHistory(let ts): sSelf.subscribeToEvents(timeStamp: ts)
default: sSelf.getServerConnection()
}
}
})
currentReqursiveConnection?.addDisposableTo(disposeBag)
}
}
private func getServerConnection() {
//get longpolling server info for connection.
currentRequestDisposable = getLongpollServerInfo()
.subscribe(onNext: {[weak self] info in
guard let sSelf = self else { return }
sSelf.currentRequestInfo.value = info
})
currentRequestDisposable?.addDisposableTo(disposeBag)
}
private func parseUpdates(json: Any) throws -> Observable<LongPollingUpdates> {
let response = try Mapper<LongPollingUpdates>().map(JSONObject: json)
return .just(response)
}
private func reciveEvents(updates:LongPollingUpdates) throws -> Observable<TimeInterval> {
if let errors = updates.failed {
throw parseErrors(errors: errors)
}
if let events = updates.updates {
parseUpdates(updates: events)
}
return Observable.just(updates.timeStamp!)
}
private func parseUpdates(updates: [[Any]]) {
updates.forEach { (array) in
let firstElementInUpdate = array.first
if let update = firstElementInUpdate as? Int {
switch update {
case 1: break
case 2: break
case 3: break
case 4: messageReciver.onNext(NewMessage(array: array))
default: break
}
}
}
}
private func parseErrors(errors: [String: Any]) -> LongPollError {
if let error = errors["failed"] as? Int {
switch error {
case 1:
guard let ts = errors["ts"] as? TimeInterval else { return .unkownError }
return .olderHistory(ts: ts)
case 2: return .needNewkey
case 3: return .needCaseAndTs
case 4: return .unkownVersion
default:
return .unkownError
}
}
return .unkownError
}
private func getEventsFromLongpollServer(serverInfo: LongpollingServerInfo, with ts: TimeInterval) -> Observable<Any> {
let url = buildLongPollingServerRoute(from: serverInfo, with: ts)
let request = buldLongPollRequst(route: url)
let requestConvert = try? URLEncoding.default.encode(request!, with: nil)
return transport.makeRequest(request: requestConvert!)
}
private func getEventsFromLongpollServer(serverInfo: LongpollingServerInfo) -> Observable<Any> {
let url = buildLongPollingServerRoute(from: serverInfo)
let request = buldLongPollRequst(route: url)
let requestConvert = try? URLEncoding.default.encode(request!, with: nil)
return transport.makeRequest(request: requestConvert!)
}
private func getLongpollServerInfo() -> Observable<LongpollingServerInfo> {
let request = MessageRouter.getLongpollServer(useSsl: false, needPts: false)
return transport.makeModel(request: request)
}
}
So assuming you have a function like:
func getData() -> Observable<Data>
And you want to long poll it at a specific period, you can do something like this:
Observable<Int>.interval(period, scheduler: MainScheduler.instance)
.map { _ in return }
.flatMap(getData)
.subscribe( /* ... handle data ... */)
.disposed(by: disposeBag)
You can use other schedulers than MainScheduler if that is more appropriate.
Now if you want also handle Errors that getData might emit and you don't want that to necessarily unsubscribe the long polling, then you can do this:
func handleError(error: Error) -> Observable<Data> {
return Observable.empty()
}
Observable<Int>.interval(period, scheduler: MainScheduler.instance)
.map { _ in return }
.flatMap { return getData.catchError(handleError) }
.subscribe( /* ... handle data ... */)
.disposed(by: disposeBag)
You can also analyze the error in handleError and decide if you want to continue by emitting an empty Observable or cancel the long polling by emitting another error.

Resources