I have one UITableView populated by reactive viewmodel using RxSwift, pagination and refresh are working well. The viewModel.dataSource() is consuming my API and sometime I can receive a empty result parsed as error type.
I want to catch this error and create an empty state, hiding tableview and showing a emptyViewState. I thought I could make it with the catchError.
My problem is after catchError, the dataSource is disposed and I couldn't be able to recovery the empty state and repopulated the tableview, I tried to recreate the dataSource calling self.bindDataSource() but I getting fatal error.
There is a way to avoid dataSource disposed ? How can I reconnect / rebuild the dataSource to recovery from the empty state ?
class MyViewControl: UIViewController {
fileprivate let disposeBag = DisposeBag()
fileprivate let viewModel = ViewModel()
let dataSource = SearchViewModel.SearchDataSource()
#IBOutlet fileprivate weak var tableView: UITableView!
#IBOutlet weak var emptyStateView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// When I disable tableview, can see a hidden view with empty state message and one button
viewModel.isTableViewHidden
.bindTo(tableView.rx.isHidden)
.addDisposableTo(disposeBag)
self.setupTableView()
}
fun setupTableView() {
// ... setup table view
self.bindDataSource()
}
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
viewModel.dataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBag )
}
#IBAction fileprivate func emptyStateAction(_ sender: UIButton) {
// Do something and try to recreate the bindDataSource
self.bindDataSource()
}
}
class SearchViewModel {
private let disposeBag = DisposeBag()
typealias SearchDataSource = RxTableViewSectionedReloadDataSource<PaginationStatus<WorkerEntity>>
let isTableViewHidden = BehaviorSubject<Bool>(value: false)
// Controls to refresh and paging tableview
let refreshTrigger = BehaviorSubject<Void>(value:())
let nextPageTrigger = PublishSubject<Void>()
// Others things happing herer
func dataSource() -> Observable<[PaginationStatus<WorkerEntity>]> {
return self.refreshTrigger.debug("[DEBUG] Refreshing dataSource")
.flatMapLatest { [unowned self] _ -> Observable<[PaginationStatus<WorkerEntity>]> in
// Access the API and return dataSource
}
.catchError { [unowned self] error -> Observable<[PaginationStatus<WorkerEntity>]> in
// Hidden the tableview
self.isTableViewHidden.onNext(true)
// Do others things
return Observable.of([PaginationStatus.sectionEmpty])
}
}
}
when you bindDataSource() you dont reinitialised your datasource, so you bind it to a error event.
You need to init it, to bind it again. And you might want to remove your binding too
let disposeBagTableView = DisposeBag()
//remove
let dataSource = SearchViewModel.SearchDataSource()
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
disposeBagTableView = DisposeBag()
SearchViewModel.SearchDataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBagTableView )
}
Related
Everything I've read says that bind(to:) calls subscribe(onNext:) within it. So I assume I should be able to swap out some stuff, but when I use `bind(to:) the thing it's binding to fires immediately. Here's my example:
ButtonCollectionViewCell
class ButtonCollectionViewCell: UICollectionViewCell {
lazy var buttonTapped: Observable<Void> = { _buttonTapped.asObservable() }()
private var _buttonTapped = PublishSubject<Void>()
private var disposeBag = DisposeBag()
#IBOutlet private weak var textLabel: UILabel!
#IBOutlet private weak var actionButton: UIButton!
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
actionButton.rx.tap.bind(to: _buttonTapped).disposed(by: disposeBag)
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}
}
Now when I do the following below everything works as expected and it prints to the console when I tap the button
ViewController with a collection view
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell() // there's more code to create it, this is just for simplicity
buttonCell.buttonTapped.subscribe { _ in
print("tapped")
}.disposed(by: disposeBag)
return buttonCell
}
However, if I change the above to:
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell()
buttonCell.buttonTapped.bind(to: buttonTapped)
return buttonCell
}
private func buttonTapped(_ sender: Observable<Void>) {
print("tapped")
}
The "tapped" is printed out right before I get to the cell when scrolling which I assume is when it's being created.
I don't understand this. I thought I could almost swap out the implementations? I would like to use the second example above there as I think it's neater but can't figure out how.
Your two examples are not identical...
In the first example you have: .subscribe { _ in print("tapped") } which is not a subscribe(onNext:) call. The last closure on the subscribe is being used, not the first. I.E., you are calling subscribe(onDisposed:).
Also, your ButtonCollectionViewCell is setup wrong. You bind in the awakeFromNib() which is only called once, and dispose in the prepareForReuse() which is called multiple times. One of the two needs to be move to a more appropriate place...
UPDATE
You could either rebind your subject after reseating the disposeBag, or you could just not put the chain in the dispose bag in the first place by doing:
_ = actionButton.rx.tap
.takeUntil(rx.deallocating)
.bind(to: _buttonTapped)
I want to have the view hidden when the button is selected and the view shown when button is deselected, how do I do it using RxSwift?
When I created a custom control, a checkbox out of UIButton, I actually struggled observing that isSelected property. But here's an easy way:
Subclass the UIButton (this is optional, but in this way, you get shorter lines in your controller).
In your custom button, subscribe to its own .rx.tap.
Have a BehaviorRelay called isSelectedBinder in your button.
Finally, you can now bind that isSelectedBinder of your instantiated button to the .rx.isHidden of your whatever view.
Controller
class ViewController: UIViewController {
#IBOutlet weak var button: MyButton!
#IBOutlet weak var someView: UIView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.button.isSelectedBinder
.bind(to: self.someView.rx.isHidden)
.disposed(by: disposeBag)
}
}
Button
class MyButton: UIButton {
var isSelectedBinder = BehaviorRelay<Bool>(value: true)
let disposeBag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
weak var weakSelf = self
self.rx.tap.subscribe { _ in
print("TAP")
guard let strongSelf = weakSelf else { return }
strongSelf.isSelectedBinder.accept(!strongSelf.isSelectedBinder.value)
}.disposed(by: self.disposeBag)
}
}
I am observing UISearchBar.rx.text attributes to perform some search related Action when User types some text.
But at some time, I also would like to trigger this search Action programmatically. For instance at the creation of the view like in this example, where unfortunately the "Searching for [...]" text is not printed.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var mySearchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Trigger search when text changes
mySearchBar.rx.text.subscribe(onNext: { (text)
print("Searching for \(text)...")
// do some search Action
}
.disposed(by: disposeBag)
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
}
}
The problem is changing mySearchBar.text does not trigger a new rx.text Event. Is there some way to do so?
For instance, I know thanks to this post that with an UITextField, this is possible using the UITextField.sendActions(for: .ValueChanged) function. Is there some similar way to do so with UISearchBar?
You could use a Variable<String?> as a sink for your search bar updates. That way you could also set its value programmatically, and use the variable instead of the search bar directly to drive your action:
class ViewController: UIViewController {
let searchText = Variable<String?>(nil)
let searchBar = UISearchBar()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
searchBar.rx.text.asDriver()
.drive(searchText)
.disposed(by: disposeBag)
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
if let welf = self, welf.searchBar.text != text {
welf.searchBar.text = text
}
})
.disposed(by: disposeBag)
searchText.value = "variables so cool"
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
self?.doStuff(text)
})
.disposed(by: disposeBag)
}
}
This is a simple usage of Drivers
import UIKit
import RxCocoa
import RxSwift
class SearchViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let dataModels = Driver.just(["David", "Behrad","Tony","Carl","Davidov"])
let searchingTextDriver = searchBar.rx.text.orEmpty.asDriver()
let matchedCases = searchingTextDriver.withLatestFrom(dataModels){ searchingItem,models in
return models.filter({ (item) -> Bool in
return item.contains(searchingItem)
})
}
matchedCases.do(onNext: { (items) in
print(items)
}).drive().disposed(by: disposeBag)
}
}
As you see we created a dataModels Driver that it could be a response of your network or dataBase fetching.
the searching text converted to Driver then mixed with the dataModels to create matchedCases.
Yes same works for UITextBar.
Setting .text programmatically does not trigger the action automatically, I don't know why! but this is the case for UITextField too.
Call sendActions after each time you update .text property of a UIView subclass.
Try this :
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
mySearchBar.sendActions(for: .valueChanged)
I setup the tableview and want to update the UI as soon as the data changes. I simulate a data change with the dispatcher. But the problem is, that the table won't update. Can someone explain how to setup a tableview with RxSwift to update it's cell on data change?
#IBOutlet private var tableView: UITableView!
let europeanChocolates: Variable<[Chocolate]> = Variable([])
let disposeBag = DisposeBag()
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
//MARK: Rx Setup
private func setupCellConfiguration() {
europeanChocolates.asObservable().bindTo(tableView.rx.items(cellIdentifier: ChocolateCell.Identifier, cellType: ChocolateCell.self)) {
row, chocolate, cell in
cell.configureWithChocolate(chocolate: chocolate)
}
.addDisposableTo(disposeBag)
}
You didnt provide implementation of your Chocolate, but I assume it's structure, in that case you are not changing anything, because in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
your chocolate from europeanChocolates is copied to new var choclate, but you never save it. You can easily check it, if you try this
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
europeanChocolates.asObservable().subscribe(onNext: { choco in
print("choco changed \(choco)")
})
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
I think it won't print "choco changed". If Im right this should help you:
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
self.europeanChocolates.value[0] = choclate
}
}
If it doesn't help, please provide implementation of Chocolate and ChocolateCell
In swift5 the Variable has been deprecated. So preferably you should use the behaviour subjects. To pass value in it use OnNext function. If it bind with the table view then it will automatically refresh the new data.
You have to reload the tableView just after the data change. To reload tableView call this:
this.tableView.reloadData()
When I started to use RxSwift I used to create BaseViewController and extend it with all my controllers where I use RxSwift.
The code of BaseViewController.swift:
class BaseViewController: UIViewController {
var mSubscriptions: CompositeDisposable?
func addSubscription(subscription: Disposable){
if(mSubscriptions == nil){
mSubscriptions = CompositeDisposable()
}
if let mSub = mSubscriptions{
mSub.addDisposable(subscription)
}
}
func unsubscribeAll(){
if let mSub = mSubscriptions{
mSub.dispose()
mSubscriptions = nil
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
unsubscribeAll()
}
deinit{
unsubscribeAll()
}
}
And I use addSubscription(:_) method everywhere in my child controllers. For example a piece of code from:
class TasksViewController: BaseViewController{
overrided func viewWillAppear(){
//...
var subscribe = dataLoader.load(requestNetwork, dataManager: taskDataManager)
.observeOn(ConcurrentDispatchQueueScheduler(queue: queue))
.subscribe({ (event) -> Void in
//...
})
addSubscription(subscribe!)
}
}
What if I do not use BaseViewController and just create an instance of DisposeBag() in every controller and add all my subscriptions to that disposeBag? And how should I treat Disposables correct?
You could just add a let disposeBag = DisposeBag() property to your view controllers. Adding Disposables to that is all you need to do. DisposeBag is like a CompositeDisposeBag that will dispose the Disposables for you when DisposeBag is deallocated (which will happen when the UIViewController is deallocated). There's no need to manage it manually.
However, you can continue to use a subclass if you wanted:
class BaseViewController: UIViewController {
let disposeBag = DisposeBag()
}
And then to use it:
override func viewDidLoad() {
super.viewDidLoad()
Observable.just(42)
.subscribeNext { i in
print(i)
}
.addDisposableTo(disposeBag)
}
This is in fact what ViewController base class does in RxExample:
property in ViewController
Usage in a subclass
If you're actually wanting to be able to deallocate everything manually (as you were doing with unsubscribeAll), then you can just set the disposeBag to nil or a new DisposeBag so that it gets deallocated: disposeBag = DisposeBag() or disposeBag = nil.