RxSwift calling bind fires immediately vs subscribe(onNext: ) - ios

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)

Related

Combine Framework Update UI doesn't work properly

I want to try Combine framework, very simple usage, press a UIButton, and update UILabel.
My idea is:
Add a publisher
#Published var cacheText: String?
Subscribe
$cacheText.assign(to: \.text, on: cacheLabel)
assign a value when button pressed.
cacheText = "testString"
Then the label's text should be updated.
The problem is when the button pressed, the #Published value is updated, but the UILabel value doesn't change.
e.g the cacheLabel1 was assigned 123 initially but not 789 when button pressed.
Here's the full code:
ViewModel.swift
import Foundation
import Combine
class ViewModel {
#Published var cacheText: String?
func setup(_ text: String) {
cacheText = text
}
init() {
setup("123")
}
}
ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var cacheLabel: UILabel!
var viewModel = ViewModel()
#IBAction func buttonPressed(_ sender: Any) {
viewModel.setup("789")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$cacheText.assign(to: \.text, on: cacheLabel)
}
}
Not sure if I missed something, thanks for the help.
The pipeline is dying before you have a chance to tap the button. You have to preserve it, like this:
var storage = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$cacheText.assign(to: \.text, on: cacheLabel).store(in: &storage)
}

How to use RxSwift to implement whether a view is hidden by a button's click state (isSelected)

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)
}
}

Cannot pass immutable value of type 'NSObject' as inout argument

This should work, but I've got no clue why it doesn't. The code is self-explanatory.
class Themer {
class func applyTheme(_ object: inout NSObject) {
//do theming
}
}
And I apply theme to the button like so:
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
Themer.applyTheme(&button)
}
The button object is a variable, yet the compiler throws an error.
Since button is an object, this syntax
Themer.applyTheme(&button)
means that you want to change the reference to that object. But this is not what you want. You want to change the referenced object so you simply need to write
Themer.applyTheme(button)
Finally you also don't need the inout annotation
class Themer {
class func applyTheme(_ object: AnyObject) {
//do theming
}
}
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
Themer.applyTheme(self.button)
}
}
But...
However, what should your applyTheme method do? It receives AnyObject and then what? You could make it a little but more specific and use a UIView as param
class Themer {
class func applyTheme(view: UIView) {
//do theming
}
}
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
Themer.applyTheme(view: button)
}
}
Now you have a chance to write meaningful code inside Themer.applyTheme.
inout is for the case that you want to change the reference, that is replace one object with another object. That's a very, very, very bad thing to do with an IBOutlet. That button is used in a view, connected up to lots of things, and if you change the variable, all hell will break lose.
Apart from that, listen to appzYourLife.

How to recovery dataSource after disposed from a catchError

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 )
}

Swift: Delegation protocol not setting UILabel properly

I have the following Protocol:
protocol SoundEventDelegate{
func eventStarted(text:String)
}
which I call in this class:
class SoundEvent {
var text:String
var duration:Double
init(text: String, duration: Double){
self.text = text
self.duration = duration
}
var delegate : SoundEventDelegate?
func startEvent(){
delegate?.eventStarted(self.text)
}
func getDuration() -> Double{
return self.duration //TODO is this common practice?
}
}
Which I have my ViewController conform to:
class ViewController: UIViewController, SoundEventDelegate {
//MARK:Properties
#IBOutlet weak var beginButton: UIButton!
#IBOutlet weak var kleinGrossLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//DELEGATE method
func eventStarted(text:String){
kleinGrossLabel.text = text
}
//MARK: actions
#IBAction func startImprovisation(sender: UIButton) {
var s1:Sentence = Sentence(type: "S3")
var s2:Sentence = Sentence(type: "S1")
var newModel = SentenceMarkov(Ult: s1, Penult: s2)
s1.start()
beginButton.hidden = true
}
}
But when I run the app kleinGrossLabel.text does not change. Am I referring to the label in the wrong way? Or is it the way that I do delegation that is incorrect?
Here are links to the complete Class definitions of Sentence and SentenceMarkov
https://gist.github.com/anonymous/9757d0ff00a4df7a29cb - Sentence
https://gist.github.com/anonymous/91d5d6a59b0c69cba915 - SentenceMarkov
You never set the delegate property. It's nil. It will never be called.
First off it's not common practice to have a setter in swift. if you want to have a readonly property you can use private(set) var propertyName
in other cases simply access the property like mentioned in the comment
Also i don't see a reason why you eventArray in sentence is of type [SoundEvent?] not [SoundEvent] as SoundEventdoes not seem to have a failable initialiser
Like mentioned before you need to not only implement the SoundEventDelegate protocol but also set the delegate
the problem is that you can't really access the SoundEventDelegate from the viewcontroller because you instantiate the SoundEvents inside Sentence
var soundEventDelegate: SoundEventDelegate?
the easiest way to do this would be adding a soundEventDelegate property for sentence and setting it like this:
let s1:Sentence = Sentence(type: "S3")
let s2:Sentence = Sentence(type: "S1")
s1.soundEventDelegate = self
s2.soundEventDelegate = self
and inside sound you would need the set the delegate for every event to the soundEventDelegate of Sentence
you could do it like this:
var soundEventDelegate: SoundEventDelegate? = nil {
didSet {
eventArray.forEach({$0.delegate = soundEventDelegate})
}
}
or write another initialiser that takes the delegate
hope this helps
p.s: you shouldn't inherit form NSObject in swift excepts it's really necessary

Resources