RxSwift Input Output, private subject but being triggered outside the class - ios

Im reading this blog about the input output ViewModel approach:
https://medium.com/blablacar-tech/rxswift-mvvm-66827b8b3f10
full code: https://gist.github.com/MartinMoizard
Im just puzzled on how did the
let greeting = validateSubject
.withLatestFrom(nameSubject)
.map { name in
return "Hello \(name)!"
}
.asDriver(onErrorJustReturn: ":-(")
https://gist.github.com/MartinMoizard/4d66528a9959cbbdefa6d50394d2bfb1
is being triggered if the validateSubject is private upon tapped in
https://gist.github.com/MartinMoizard/449be0d30920010210988f1773a2ca90
final class ButtonCell: UITableViewCell, SayHelloViewModelBindable {
#IBOutlet weak var validateButton: UIButton!
var disposeBag: DisposeBag?
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = nil
}
func bind(to viewModel: SayHelloViewModel) {
let bag = DisposeBag()
validateButton.rx
.tap
.bind(to: viewModel.input.validate)
.disposed(by: bag)
disposeBag = bag
}
}
TIA

He is not really accessing validateSubject upon tap but eventually he has created some accessible input/output layer see for example:
struct Input {
let name: AnyObserver<String>
let validate: AnyObserver<Void>
}
struct Output {
let greeting: Driver<String>
}
to communicate with the inner private layer/logic (such as validateSubject and nameSubject).
And then he pass all kind of information in input through name and validate and expect information back from the output through greeting.

Related

Best practice for binding controls in UITableViewCell to ViewModel using RxSwift

I'm in the process of migrating an existing app using MVC that makes heavy use of the delegation pattern to MVVM using RxSwift and RxCocoa for data binding.
In general each View Controller owns an instance of a dedicated View Model object. Let's call the View Model MainViewModel for discussion purposes. When I need a View Model that drives a UITableView, I generally create a CellViewModel as a struct and then create an observable sequence that is converted to a driver that I can use to drive the table view.
Now, let's say that the UITableViewCell contains a button that I would like to bind to the MainViewModel so I can then cause something to occur in my interactor layer (e.g. trigger a network request). I'm not sure what is the best pattern to use in this situation.
Here is a simplified example of what I've started out with (see 2 specific question below code example):
Main View Model:
class MainViewModel {
private let buttonClickSubject = PublishSubject<String>() //Used to detect when a cell button was clicked.
var buttonClicked: AnyObserver<String> {
return buttonClickSubject.asObserver()
}
let dataDriver: Driver<[CellViewModel]>
let disposeBag = DisposeBag()
init(interactor: Interactor) {
//Prepare the data that will drive the table view:
dataDriver = interactor.data
.map { data in
return data.map { MyCellViewModel(model: $0, parent: self) }
}
.asDriver(onErrorJustReturn: [])
//Forward button clicks to the interactor:
buttonClickSubject
.bind(to: interactor.doSomethingForId)
.disposed(by: disposeBag)
}
}
Cell View Model:
struct CellViewModel {
let id: String
// Various fields to populate cell
weak var parent: MainViewModel?
init(model: Model, parent: MainViewModel) {
self.id = model.id
//map the model object to CellViewModel
self.parent = parent
}
}
View Controller:
class MyViewController: UIViewController {
let viewModel: MainViewModel
//Many things omitted for brevity
func bindViewModel() {
viewModel.dataDriver.drive(tableView.rx.items) { tableView, index, element in
let cell = tableView.dequeueReusableCell(...) as! TableViewCell
cell.bindViewModel(viewModel: element)
return cell
}
.disposed(by: disposeBag)
}
}
Cell:
class TableViewCell: UITableViewCell {
func bindViewModel(viewModel: MyCellViewModel) {
button.rx.tap
.map { viewModel.id } //emit the cell's viewModel id when the button is clicked for identification purposes.
.bind(to: viewModel.parent?.buttonClicked) //problem binding because of optional.
.disposed(by: cellDisposeBag)
}
}
Questions:
Is there a better way of doing what I want to achieve using these technologies?
I declared the reference to parent in CellViewModel as weak to avoid a retain cycle between the Cell VM and Main VM. However, this causes a problem when setting up the binding because of the optional value (see line .bind(to: viewModel.parent?.buttonClicked) in TableViewCell implemenation above.
The solution here is to move the Subject out of the ViewModel and into the ViewController. If you find yourself using a Subject or dispose bag inside your view model, you are probably doing something wrong. There are exceptions, but they are pretty rare. You certainly shouldn't be doing it as a habit.
class MyViewController: UIViewController {
var tableView: UITableView!
var viewModel: MainViewModel!
private let disposeBag = DisposeBag()
func bindViewModel() {
let buttonClicked = PublishSubject<String>()
let input = MainViewModel.Input(buttonClicked: buttonClicked)
let output = viewModel.connect(input)
output.dataDriver.drive(tableView.rx.items) { tableView, index, element in
var cell: TableViewCell! // create and assign
cell.bindViewModel(viewModel: element, buttonClicked: buttonClicked.asObserver())
return cell
}
.disposed(by: disposeBag)
}
}
class TableViewCell: UITableViewCell {
var button: UIButton!
private var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
func bindViewModel<O>(viewModel: CellViewModel, buttonClicked: O) where O: ObserverType, O.Element == String {
button.rx.tap
.map { viewModel.id } //emit the cell's viewModel id when the button is clicked for identification purposes.
.bind(to: buttonClicked) //problem binding because of optional.
.disposed(by: disposeBag)
}
}
class MainViewModel {
struct Input {
let buttonClicked: Observable<String>
}
struct Output {
let dataDriver: Driver<[CellViewModel]>
}
private let interactor: Interactor
init(interactor: Interactor) {
self.interactor = interactor
}
func connect(_ input: Input) -> Output {
//Prepare the data that will drive the table view:
let dataDriver = interactor.data
.map { data in
return data.map { CellViewModel(model: $0) }
}
.asDriver(onErrorJustReturn: [])
//Forward button clicks to the interactor:
_ = input.buttonClicked
.bind(to: interactor.doSomethingForId)
// don't need to put in dispose bag because the button will emit a `completed` event when done.
return Output(dataDriver: dataDriver)
}
}
struct CellViewModel {
let id: String
// Various fields to populate cell
init(model: Model) {
self.id = model.id
}
}
you can use this RxReusable.
this is Rx extension of UITableViewCell, UICollectionView…

RXSwift bind view model data to view controller uiimageview

I am new to RXSwift, please help to find best solution.
I have view model with instance variable:
var capturedImageData: Data?
I need to unhide UIImageView view and set image after capturedImageData receives data, for example on capture image from camera.
You will need to observe the value of capturedImageData
You can create a behaviorRelay of capturedImageData
Something like
var capturedImageData:<Data?> = BehaviorRelay.init(value: nil)
And when you get the data, you add do something like this
capturedImageData.accept(data)
And in your viewController, you subscribe to capturedImageData
self.capturedImageData.asObservable().subscribe(onNext: { (data) in
self.imageView.image = UIImage.init(data: data)
self.imageView.isHidden = false
}).disposed(by: bag)
Something of this sort.
Haven't tested the code, but you can follow this approach.
Hope this helps
From your code, I mocked a very simple example.
From your ViewModel layer, you can try to keep an Input / Output structu to make it easy to reuse and consume. Here the input would be your Data wherever it comes from, and the output UIImage?. In short, ViewModel does the logic to transform the data, the View will load only if available.
struct ViewModel {
// input
let loadingImageData: PublishRelay<Data>
// output
let showImage: Driver<UIImage?>
init() {
let dataRelay = PublishRelay<Data>()
self.loadingImageData = dataRelay
self.showImage = dataRelay.map({ UIImage(data: $0) }).asDriver(onErrorJustReturn: nil)
}
func loadimage() {
// your code here to load Data ...
loadingImageData.accept(imageData)
}
}
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
// bind output viewmodel to UIImageView
viewModel.showImage
.drive(imageView.rx.image)
.disposed(by: disposeBag)
}
}
You can extend the logic to hide / show element the same way creating Bool events instead.
struct ViewModel {
// ...
let isImageHidden: Driver<Bool>
init() {
let dataRelay = PublishRelay<Data>()
self.loadingImageData = dataRelay
self.showImage = dataRelay.map({ UIImage(data: $0) }).asDriver(onErrorJustReturn: nil)
self.isImageHidden = showImage.map({ $0 == nil })
}
}
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
// ...
viewModel.isImageHidden
.drive(imageView.rx.isHidden)
.disposed(by: disposeBag)
}
}
Note that I use Driver to make sure it runs on main thread.

How to bind image to UIImageView with rxswift?

I have viewModel:
class EditFoodViewViewModel {
private var food: Food
var foodImage = Variable<NSData>(NSData())
init(food: Food) {
self.food = food
self.foodImage.value = food.image!
}
}
And ViewController:
class EditFoodViewController: UIViewController {
public var food: EditFoodViewViewModelType?
#IBOutlet weak var foodThumbnailImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage.asObservable().bind(to: foodThumbnailImageView.rx.image).disposed(by: disposeBag)
}
}
In the last line of viewController (where my UIImageView) a get error:
Generic parameter 'Self' could not be inferred
How to solve my problem? How to set image to imageView with rxSwift?
Almost invariably, when you see the error: "Generic parameter 'Self' could not be inferred", it means that the types are wrong. In this case you are trying to bind an Observable<NSData> to an Observable<Image?>.
There's a few other issues with your code as well.
it is very rare that a Subject type should be defined with the var keyword and this is not one of those rare times. Your foodImage should be a let not a var.
Variable has been deprecated; don't use it. In this case, you don't even need a subject at all.
NSData is also inappropriate in modern Swift. Use Data instead.
Based on what you have shown here, I would expect your code to look more like this:
class EditFoodViewViewModel: EditFoodViewViewModelType {
let foodImage: Observable<UIImage?>
init(food: Food) {
self.foodImage = Observable.just(UIImage(data: food.image))
}
}
class EditFoodViewController: UIViewController {
#IBOutlet weak var foodThumbnailImageView: UIImageView!
public var food: EditFoodViewViewModelType?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
guard let foodViewModel = food else { return }
foodViewModel.foodImage
.bind(to: foodThumbnailImageView.rx.image)
.disposed(by: disposeBag)
}
}

How do you communicate between a UIViewController and its child UIView using MVVM and RxSwift events?

I'm using MVVM, Clean Architecture and RxSwift in my project. There is a view controller that has a child UIView that is created from a separate .xib file on the fly (since it is used in multiple scenes). Thus there are two viewmodels, the UIViewController's view model and the UIView's. Now, there is an Rx event in the child viewmodel that should be observed by the parent and then it will call some of its and its viewmodel's functions. The code is like this:
MyPlayerViewModel:
class MyPlayerViewModel {
var eventShowUp: PublishSubject<Void> = PublishSubject<Void>()
var rxEventShowUp: Observable<Void> {
return eventShowUp
}
}
MyPlayerView:
class MyPlayerView: UIView {
var viewModel: MyPlayerViewModel?
setup(viewModel: MyPlayerViewModel) {
self.viewModel = viewModel
}
}
MyPlayerSceneViewController:
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
#IBOutlet weak var otherView: UIView!
var viewModel: MyPlayerSceneViewModel
fileprivate var disposeBag : DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.myPlayerView.viewModel.rxEventShowUp.subscribe(onNext: { [weak self] in
self?.viewModel.doOnShowUp()
self?.otherView.isHidden = true
})
}
}
As you can see, currently, I am exposing the myPlayerView's viewModel to the public so the parent can observe the event on it. Is this the right way to do it? If not, is there any other suggestion about the better way? Thanks.
In general, nothing bad to expose view's stuff to its view controller but do you really need two separate view models there? Don't you mix viewModel and model responsibilities?
Some thoughts:
Model shouldn't subclass UIView.
You should avoid creating own subjects in a view model. It doesn't create events by itself, it only processes input and exposes results.
I encourage you to get familiar with Binder and Driver.
Here is the code example:
struct PlayerModel {
let id: Int
let name: String
}
class MyPlayerSceneViewModel {
struct Input {
let eventShowUpTrigger: Observable<Void>
}
struct Output {
let someUIAction: Driver<PlayerModel>
}
func transform(input: Input) -> Output {
let someUIAction = input.eventShowUpTrigger
.flatMapLatest(fetchPlayerDetails) // Transform input
.asDriver(onErrorJustReturn: PlayerModel(id: -1, name: "unknown"))
return Output(someUIAction: someUIAction)
}
private func fetchPlayerDetails() -> Observable<PlayerModel> {
return Observable.just(PlayerModel(id: 1, name: "John"))
}
}
class MyPlayerView: UIView {
var eventShowUp: Observable<Void> {
return Observable.just(()) // Expose some UI trigger
}
var playerBinding: Binder<PlayerModel> {
return Binder(self) { target, player in
target.playerNameLabel.text = player.name
}
}
let playerNameLabel = UILabel()
}
class MyPlayerSceneViewController: UIViewController {
#IBOutlet weak var myPlayerView: MyPlayerView!
private var viewModel: MyPlayerSceneViewModel!
private var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupBindings()
}
private func setupBindings() {
let input = MyPlayerSceneViewModel.Input(eventShowUpTrigger: myPlayerView.eventShowUp)
let output = viewModel.transform(input: input)
// Drive manually
output
.someUIAction
.map { $0.name }
.drive(myPlayerView.playerNameLabel.rx.text)
.disposed(by: disposeBag)
// or to exposed binder
output
.someUIAction
.drive(myPlayerView.playerBinding)
.disposed(by: disposeBag)
}
}

Binding UITextField or button with viewModel

I have been facing an issue with binding UITextField or button with observables in viewModel.
class VM {
var emailObservable: Observable<String?> = Observable.just("")
}
I have this observable for email in my viewModel and in controller. When i try to bind my textfield with it, it gives me error
Cannot invoke 'bind' with an argument list of type '(to: Observable)'.
But when i replace the observables with Variable, it works fine.
Can someone please help me with this. I found answers which mainly include passing the observable in the init method of viewModel, but i don't want to pass it in the init method.
This is the link i found for binding but it is through init method.
How to bind rx_tap (UIButton) to ViewModel?
Instead of
emailTextfield.rx.text.asObservable().bind(to: viewModel.emailObservable).disposed(by: disposeBag)
use this code
viewModel.emailObservable.bind(to: noteField.rx.text).disposed(by: disposeBag)
Probably, you want to make two way binding, so read more about it here
I think here what you looking for:
final class ViewModel {
private let bag = DisposeBag()
let string = BehaviorSubject<String>(value: "")
init() {
string.asObservable().subscribe(onNext: { string in
print(string)
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel()
textField.rx.text
.orEmpty
.bind(to: viewModel.string)
.disposed(by: bag)
}
}
Note, as #MaximVolgin mentioned Variable is deprecated in RxSwift 4, so you can use BehaviorSubject or other that's up to you.
UPD.
Implementation with Observable only.
final class ViewModel {
private let bag = DisposeBag()
var string = "" {
didSet {
print(string)
}
}
init(stringObservable: Observable<String>) {
stringObservable.subscribe(onNext: { string in
self.string = string
})
.disposed(by: bag)
}
}
final class ViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
private let bag = DisposeBag()
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = ViewModel(stringObservable: textField.rx.text.orEmpty.asObservable())
}
}
As you can see, your solution can be implemented using Observable, not Variable or any kind of Subject. Also should be mentioned that in most cases this is not the final logic (just bind textField or whatever to some variable). There can be some validation, enable/disable, etc. logic. For this cases RxSwift provide Driver. Also nice example about differences in using Observable and Driver for one project can be found here (by RxSwift).
Method .bind(to:) binds to an Observer, not Observable.
Variable (deprecated in RxSwift v4) is a special-purpose Subject.
Subjects are by definition both Observer and Observable.
This is what .bind(to:) does inside -
public func bind<O: ObserverType>(to observer: O) -> Disposable where O.E == E {
return self.subscribe(observer)
}
UPDATE:
How to avoid passing observables in .init() of VM:
// inside VM:
fileprivate let observableSwitch: BehaviorSubject<Observable<MyValue>>
fileprivate let myValueObservable = observableSwitch.switchLatest()
// instead of passing in init:
public func switch(to observable: Observable<MyValue>) {
self.observableSwitch.onNext(observable)
}
Take a subject of variable type in ViewModel class:
class ViewModel{
//MARK: - local Variables
var emailText = Variable<String?>("")
}
Now create object of viewmodel class in viewController class and bind this emailtext variable to textfield in viewcontroller.Whenever textfield text will change then it emailText of viewmodel gets value.
txtfield.rx.text
.bindTo(viewModel.emailText).addDisposableTo(disposeBag)
Try this,
override func viewDidLoad() {
super.viewDidLoad()
_ = userNameTextField.rx.text.map { $0 ?? "" }.bind(to: viewModel.userName)
}
in viewModel class,
class ViewModel{
var userName: Variable<String> = Variable("")
}

Resources