delaySubscription doesn't work with rx_tap - ios

This is a short version of my code which will reproduce the problem:
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let source = button.rx_tap.map { _ in "source" }
let delay = source.map { _ in "delayed" }
.delaySubscription(2.0, MainScheduler.sharedInstance)
[source, delay].toObservable().merge()
.subscribeNext { print($0) }
.addDisposableTo(disposeBag)
}
}
I want the 'delayed' signal to fire 2 seconds after I tap the button, but no such luck. What actually happens: the first time I tap the button, 'source' fires but nothing else happens. Then when I tap again, 'source' and 'delayed' fire at the same time. I figured it was some thread problem, so I tried adding observeOn(MainScheduler.sharedInstance) everywhere but it didn't help. Any ideas?
Update: by adding .debug() to the streams I found out that the delayed stream actually subscribes to the source 2 seconds later. But that still doesn't explain why it doesn't fire its notifications 2 seconds later as well.

To answer my own question, it seems that delaySubscription only works on cold observables.
A cold observable, like for example a timer, only starts to fire notifications when it has been subscribed to, and everyone that subscribes to it gets a fresh sequence. This is why simply delaying the subscription on a cold observable will also delay all the notifications.
A hot observable, like a UI event for example, shares the same sequence with all its subscribers, so delaying the subscription has absolutely no influence on its notifications.
Instead, I can use the flatMap operator to transform each source notification into another observable that fires its only notification after a certain delay, and merges the results of these observables:
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let source = button.rx_tap.map { _ in "source" }
let delayed = source.flatMap { _ in
timer(1.0, MainScheduler.sharedInstance)
.map { _ in "delayed" }
}
[source, delayed]
.toObservable().merge()
.subscribeNext { print($0) }
.addDisposableTo(disposeBag)
}
}

Related

How to subscribe on the value changed control event of a UISwitch Using Rxswift

I want to use Rxswift and not IBActions to solve my issue below,
I have a UISwitch and I want to subscribe to the value changed event in
it,
I usually subscribe on Buttons using this manner
#IBOutlet weak var myButton: UIButton!
myButton
.rx
.tapGesture()
.when(.recognized)
.subscribe(onNext : {_ in /*do action here */})
Does anyone know how to subscribe to UISwitch control events?
I found the answer Im looking for, in order to subscribe on and control event we should do the below :
#IBOutlet weak var mySwitch : UISwitch!
mySwitch
.rx
.controlEvent(.valueChanged)
.withLatestFrom(mySwitch.rx.value)
.subscribe(onNext : { bool in
// this is the value of mySwitch
})
.disposed(by: disposeBag)
Below are some caveats you would use for UISwitch:
1. Make sure the event subscribes to unique changes so use distinctUntilChanged
2. Rigorous switching the switch can cause unexpected behavior so use debounce.
Example:
anySwitch.rx
.isOn.changed //when state changed
.debounce(0.8, scheduler: MainScheduler.instance) //handle rigorous user switching
.distinctUntilChanged().asObservable() //take signal if state is different than before. This is optional depends on your use case
.subscribe(onNext:{[weak self] value in
//your code
}).disposed(by: disposeBag)
There are couple of ways to do that. But this one is how I usually do it:
Try this out.
self.mySwitch.rx.isOn.subscribe { isOn in
print(isOn)
}.disposed(by: self.disposeBag)
I hope this helps.
EDIT:
Another would be subscribing to the value rx property of the UISwitch, like so:
mySwitch.rx.value.subscribe { (isOn) in
print(isOn)
}.disposed(by: self.disposeBag)
Now, as for your comment:
this worked for me thanks , but I preferred subscribing on the control
event it self, not the value.
We could do this below, I'm not sure though if there's a better way than this. Since UISwitch is a UIControl object, you can subscribe to its .valueChanged event, like so:
mySwitch.rx.controlEvent([.valueChanged]).subscribe { _ in
print("isOn? : \(mySwitch.isOn)")
}.disposed(by: self.disposeBag)
More info: https://developer.apple.com/documentation/uikit/uiswitch
The following code works for me, building on prex's answer. Please feel free to correct or suggest any improvements.
RxSwift/RxCocoa 4.4, iOS 12, Xcode 10
private let _disposeBag = DisposeBag()
let viewModel: ViewModel ...
let switchControl: UIControl ...
let observable = switchControl
.rx.isOn
.changed // We want user-initiated changes only.
.distinctUntilChanged()
.share(replay: 1, scope: .forever)
observable
.subscribe(onNext: { [weak self] value in
self?.viewModel.setSomeOption(value)
})
.disposed(by: _disposeBag)
In this example, we are notified when the user (and only the user) changes the state of the UISwitch. We then update viewModel.
In turn, any observers of the viewModel, will be told of the change of state, allowing them to update UI, etc.
e.g.
class ViewModel
{
private var _someOption = BehaviorRelay(value: false)
func setSomeOption(_ value: Bool) { _someOption.accept(value) }
/// Creates and returns an Observable on the main thread.
func observeSomeOption() -> Observable<Bool>
{
return _someOption
.asObservable()
.observeOn(MainScheduler())
}
}
...
// In viewDidLoad()
self.viewModel
.observeSomeOption()
.subscribe(onNext: { [weak self] value in
self?.switchControl.isOn = value
// could show/hide other UI here.
})
.disposed(by: _disposeBag)
See: https://stackoverflow.com/a/53479618/1452758

RxCocoa-observable is emitting new data from API but tableview.rx.items doesn't show new data

I am new to both RxSwift and RxCocoa.
I have a working REST service on local which returns a Movie object for given "id".
On my storyboard there is a tableView (UITableView) which has custom cells (MovieCell). Each movie cell has a UILabel named "nameTextLabel". I also have a textField to get requested movie.id from user.
I am trying to show movie.title on each row of my tableView.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
#IBOutlet var tableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureReactiveBinding()
}
private func configureReactiveBinding() {
textField.rx.text.asObservable()
.map { ($0 == "" ? "15" : $0!).lowercased() }
.map { GetMovieByIdRequest(id: $0) }
.flatMap { request -> Observable<Movie> in
return ServiceManager.instance.send(apiRequest: request)
}
.debug("movie")
// output:
//2018-10-17 16:13:49.320: movie -> subscribed
//2018-10-17 16:13:49.401: movie -> Event next(MovieAdvisor.Movie)
//2018-10-17 16:13:52.021: movie -> Event next(MovieAdvisor.Movie)
.toArray()
.debug("tableView")
// output:
//2018-10-17 16:13:49.318: tableView -> subscribed
.bind(to: tableView.rx.items(cellIdentifier: "movie_cell", cellType:MovieCell.self)) { index, model, cell in
cell.nameTextLabel.text = model.title
}
.disposed(by: bag)
}
}
Each time textLabel is changed the new movie.id is printed, but tableView doesn't show any data-not even with initial value (when textField is "").
Any idea how can i fix this?
After adding debug() lines, i figured that tableView is subscribed just once and no next events triggered tableView. What should i do?
Code is uploaded on GitHub
github
The problem is your toArray() operator. That operator waits until the sequence completes and then emits the entire sequence as a single array. Your sequence doesn't complete until after you have exited the view controller.
Replace that with .map { [$0] } instead and the movie will show. You should ask yourself though why your endpoint is only returning one movie instead of an array of movies.
Try replacing
`a.subscribe(onNext: {movie in
print(movie.title)
})
.disposed(by: bag)
with .observeOn(MainScheduler.instance) and see whether it does the trick.
Perhaps move .toArray() to directly after ServiceManager.instance.send(apiRequest: request).
Also you can add *.debug("my custom message")* after every RxSwift operator to see whether it is getting executed.
BTW I would recommend using RxDataSources framework for this, not bare RxCocoa.

How to test the UI Binding between RxSwift Variable and RxCocoa Observable?

I have a simple ViewModel with one property:
class ViewModel {
var name = Variable<String>("")
}
And I'm binding it to its UITextField in my ViewController:
class ViewController : UIViewController {
var viewModel : ViewModel!
#IBOutlet weak var nameField : UITextField!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Binding
nameField.rx.text
.orEmpty
.bind(to: viewModel.name)
.disposed(by: disposeBag)
}
}
Now, I want to Unit Test it.
I'm able to fill the nameField.text property though Rx using .onNext event - nameField.rx.text.onNext("My Name Here").
But the viewModel.name isn't being filled. Why? How can I make this Rx behavior work in my XCTestCase?
Please see below the result of my last test run:
I believe the issue you are having is that the binding is not explicitly scheduled on the main thread. I recommend using a Driver in this case:
class ViewModel {
var name = Variable<String>("")
}
class ViewController: UIViewController {
let textField = UITextField()
let disposeBag = DisposeBag()
let vm = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
textField.rx.text
.orEmpty
.asDriver()
.drive(vm.name)
.disposed(by: disposeBag)
}
// ..
With a Driver, the binding will always happen on the main thread and it will not error out.
Also, in your test I would call skip on the binding:
sut.nameTextField.rx.text.skip(1).onNext(name)
because the Driver will replay the first element when it subscribes.
Finally, I suggest using RxTest and a TestScheduler instead of GCD.
Let me know if this helps.
rx.text relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Thus, explicitly send a onNext events will not send action for these event. You should send action manually.
sut.nameField.rx.text.onNext(name)
sut.nameField.sendActions(for: .valueChanged)

Throttling in Swift without going reactive

Is there a simple way of implementing the Throttle feature in Reactive programming without having to use RxSwift or similar frameworks.
I have a textField delegate method that I would like not to fire every time a character is inserted/deleted.
How to do that using vanilla Foundation?
Yes it is possible to achieve.
But first lets answer small question what is Throttling?
In software, a throttling process, or a throttling controller as it is
sometimes called, is a process responsible for regulating the rate at
which application processing is conducted, either statically or
dynamically.
Example of the Throttling function in the Swift.
In case that you have describe with delegate method you will have issue that delegate method will be called each time. So I will write short example how it impossible to do in the case you describe.
class ViewController: UIViewController {
var timer: Timer?
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
textField.delegate = self
}
#objc func validate() {
print("Validate is called")
}
}
extension ViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.validate), userInfo: nil, repeats: false);
return true
}
}
disclaimer: I am a writer.
Throttler can be the right katana to make you happy.
You can do debounce and throttle without going reactive programming using Throttler like,
import Throttler
// advanced debounce, running a first task immediately before initiating debounce.
for i in 1...1000 {
Throttler.debounce {
print("debounce! > \(i)")
}
}
// debounce! > 1
// debounce! > 1000
// equivalent to debounce of Combine, RxSwift.
for i in 1...1000 {
Throttler.debounce(shouldRunImmediately: false) {
print("debounce! > \(i)")
}
}
// debounce! > 1000
Throttler also can do advanced debounce, running a first event immediately before initiating debounce that Combine and RxSwift don't have by default.
You could, but you may need a complex implementation yourself for that.

How to unsubscribe from Observable in RxSwift?

I want to unsubscribe from Observable in RxSwift. In order to do this I used to set Disposable to nil. But it seems to me that after updating to RxSwift 3.0.0-beta.2 this trick does not work and I can not unsubscribe from Observable:
//This is what I used to do when I wanted to unsubscribe
var cancellableDisposeBag: DisposeBag?
func setDisposable(){
cancellableDisposeBag = DisposeBag()
}
func cancelDisposable(){
cancellableDisposeBag = nil
}
So may be somebody can help me how to unsubscribe from Observable correctly?
In general it is good practice to out all of your subscriptions in a DisposeBag so when your object that contains your subscriptions is deallocated they are too.
let disposeBag = DisposeBag()
func setupRX() {
button.rx.tap.subscribe(onNext : { _ in
print("Hola mundo")
}).addDisposableTo(disposeBag)
}
but if you have a subscription you want to kill before hand you simply call dispose() on it when you want too
like this:
let disposable = button.rx.tap.subcribe(onNext : {_ in
print("Hallo World")
})
Anytime you can call this method and unsubscribe.
disposable.dispose()
But be aware when you do it like this that it your responsibility to get it deallocated.
Follow up with answer to Shim's question
let disposeBag = DisposeBag()
var subscription: Disposable?
func setupRX() {
subscription = button.rx.tap.subscribe(onNext : { _ in
print("Hola mundo")
})
}
You can still call this method later
subscription?.dispose()

Resources