RxSwift: Two way binding - ios

I used official two-way-binding solution
func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable{
let bindToUIDisposable = variable.asObservable()
.bindTo(property)
let bindToVariable = property
.subscribe(onNext: { n in
variable.value = n
}, onCompleted: {
bindToUIDisposable.dispose()
})
return Disposables.create(bindToUIDisposable, bindToVariable)
}
Usage: (textField.rx.text <-> object.property).addDisposableTo(disposeBag)
Property definition: var property = Variable<String?>(nil)
In onNext method all ok and variable changed its value, but my object.property doesn't changed.
Is there any way to set variable current value into ControlProperty inside of <-> method, bcs I need set initial value, before subscribe starts?

Working in Swift 4
Example of two way binding between String & Textfield with MVVM architecture:
In ViewController:
#IBOutlet weak var emailTextfield: UITextField!
var viewModel : CCRegisterViewModel?
In ViewModel:
var email = Variable<String>("")
Use this code in ViewController
viewModel?.email.asObservable()
.bind(to: emailTextfield.rx.text)
.disposed(by: disposeBag)
emailTextfield.rx.text.orEmpty.bind(to:viewModel!.email)
.disposed(by: disposeBag)

My fault. I replaced object with another instance after binding
This code works well and control property receive initial value from variable

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.

Unit testing Rx binding with UITextView

I have a ControlProperty binding like this in my UIViewController:
textView.rx.text.orEmpty.asObservable()
.bind(to: viewModel.descriptions)
.disposed(by: disposeBag)
, where viewModel.descriptions is of type BehaviorRelay<String>.
In a TDD approach, I would like to write an (initially failing) test (as the initial step of the red-green-refactor cycle) to see that this binding exists. I tried with a test code like this:
sut.textView.insertText("DESC")
sut.textView.delegate?.textViewDidChange?(sut.textView)
expect(mockViewModel.lastDescriptions).to(equal("DESC"))
, where sut is my ViewController and mockViewModel contains code to subscribe to the descriptions BehaviorRelay:
class MockViewModel: MyViewModelProtocol {
var descriptions = BehaviorRelay<String>(value: "")
var lastDescriptions: String?
private var disposeBag = DisposeBag()
init() {
descriptions.asObservable()
.skip(1)
.subscribe(onNext: { [weak self] (title: String) in
self?.lastDescriptions = title
})
.disposed(by: disposeBag)
}
}
However, I cannot push the new value to the textView so that the descriptions BehaviorRelay gets it.
How can I achieve descriptions.asObservable() to fire when entering a value in the textView? If it were a UITextField then I would do:
textField.text = "DESC"
textField.sendActions((for: .editingChanged))
Thanks in advance for any help.

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)

ios ViewModel with ReactiveCocoa v3 and Swift 1.2

I'm having trouble using ReactiveCocoa in version 3. I want to build some view model for my login view controller. In my view controller I have outlet for password text field:
#IBOutlet weak var passwordTextField: UITextField!
In view model I have property for the text that is the password
public let emailText = MutableProperty<String>("")
and the question is how to bind it together? I'm able to get SignalProducer from text field:
emailTextField.rac_textSignal().toSignalProducer()
but how to bind it to emailText property? I've read in documentation that SignalProducer is not a Signal, but it can create now. There is method start() but it takes Sink as parameter and I'm a bit confused with design at this moment. Shouldn't emailText be a Sink?
Note: this is not properly an answer to your question, but I think it might help you.
If you are just want to bind your view to your view model, I suggest you to read this post which provides a one-class solution to the problem.
From there, you can very simply implement a 2-way binding so your viewmodel get updated every time the view changes and vice-versa. Here is my extension:
class TwoWayDynamic<T> {
typealias Listener = T -> Void
private var viewListener: Listener?
private var controllerListener: Listener?
private(set) var value: T
func setValueFromController(value: T) {
self.value = value
viewListener?(value)
}
func setValueFromView(value: T) {
self.value = value
controllerListener?(value)
}
func setValue(value: T) {
self.value = value
controllerListener?(value)
viewListener?(value)
}
init(_ v: T) {
value = v
}
func bindView(listener: Listener?) {
self.viewListener = listener
}
func bindController(listener: Listener?) {
self.controllerListener = listener
}
func bindViewAndFire(listener: Listener?) {
self.viewListener = listener
listener?(value)
}
func bindControllerAndFire(listener: Listener?) {
self.controllerListener = listener
listener?(value)
}
}
Hope it helps!

Resources