Writing a project on MVVM and trying to bind everything with RxSwift.
Unfortunately I did not manage to find a proper way how to bind an actions.
For example, I have a table and a simple cells with one button - "Select".
For this purposes I will have two view models: ListViewModel & CellViewModel
ListViewModel will be creating an array of CellViewModel and need to subscribe on selection event(custom event).
Now I'm using BehaviorSubject for this purposes, but it looks ugly. Who can point me how it need to be implemented with RxSwift?
class CellViewModel {
private let selectionSubject = BehaviorSubject<Void>(value: ())
// Will be used by ListViewModel
var selectionObservable: Observable<Void> {
return selectionSubject.asObservable()
}
func subscribeOnSelection(_ observable: Observable<Void>, disposeBag: DisposeBag) {
observable
.bind(to: selectionSubject)
.disposed(by: disposeBag)
}
private func autoSelect() {
selectionSubject.on(next: ())
}
}
class Cell: UITableViewCell {
#IBOutlet private var selectionButton: UIButton!
private let disposeBag = DisposeBag()
func bind(to viewModel: CellViewModel) {
viewModel.subscribeOnSelection(selectionButton.rx.tap.asObservable(), disposeBag: disposeBag)
}
}
You need a subject somewhere because the emitter of the event doesn't exist when the consumer of the event is created. Normally I put a single subject in the view controller rather than a subject in every cell. Something like this:
class Cell: UITableViewCell {
#IBOutlet private var selectionButton: UIButton!
private var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
func configure(with makeViewModel: (Observable<Void>, DisposeBag) -> Void) {
makeViewModel(selectionButton.rx.tap.asObservable(), disposeBag)
}
}
And the view controller would look something like:
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
let disposeBag = DisposeBag()
var makeViewModel: (Observable<CellID>) -> Observable<[CellID]> = { _ in fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
let cellSelection = PublishSubject<CellID>()
let cells = makeViewModel(cellSelection)
cells
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { index, element, cell in
cell.configure(with: { selected, disposeBag in
selected
.map { element }
.bind(to: cellSelection)
.disposed(by: disposeBag)
})
return
}
.disposed(by: disposeBag)
}
}
Related
I am at the stage of learning new swift and I designed my application as mvc design pattern. I went on an adventure to learn mvvm :D.
There are parts that I still don't understand. I learned that I need to transfer without using UIKit in the ViewModel part, but I couldn't figure out how to transfer it. I have to find the way to it. I have 10 Viewcontroller pages and I want to make them all according to mvvm.
I'm trying to convert my design from MVC to MVVM but i am getting this error how can i solve it?
BreedsViewController
import UIKit
import ProgressHUD
protocol BreedsViewControllerInterface: AnyObject {
func prepareCollectionView()
}
final class BreedsViewController: UIViewController {
#IBOutlet weak var categoryCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
#IBOutlet weak var popularCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
#IBOutlet weak var specialsCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
private lazy var viewModel = BreedsVM()
// data, move mvvm
var categories: [DogCategory] = []
var populars: [Breed] = []
var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.view = self
viewModel.viewDidLoad()
}
private func registerCell() {
categoryCollectionView.register(UINib(nibName: CategoryCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: CategoryCollectionViewCell.identifier)
popularCollectionView.register(UINib(nibName: DogPortraitCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogPortraitCollectionViewCell.identifier)
specialsCollectionView.register(UINib(nibName: DogLandscapeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogLandscapeCollectionViewCell.identifier)
}
}
extension BreedsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch collectionView {
case categoryCollectionView:
return categories.count
case popularCollectionView:
return populars.count
case specialsCollectionView:
return downCategories.count
default: return 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch collectionView {
case categoryCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCollectionViewCell.identifier, for: indexPath) as! CategoryCollectionViewCell
cell.setup(category: categories[indexPath.row])
return cell
case popularCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogPortraitCollectionViewCell.identifier, for: indexPath) as! DogPortraitCollectionViewCell
cell.setup(breed: populars[indexPath.row])
return cell
case specialsCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogLandscapeCollectionViewCell.identifier, for: indexPath) as! DogLandscapeCollectionViewCell
cell.setup(breed: downCategories[indexPath.row])
return cell
default: return UICollectionViewCell()
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == categoryCollectionView {
let controller = ListDogsViewController.instantiate()
controller.category = categories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
} else {
let controller = FavoriteDetailViewController.instantiate()
controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
}
}
}
extension BreedsViewController: BreedsViewControllerInterface {
func prepareCollectionView() {
registerCell()
ProgressHUD.show()
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
ProgressHUD.dismiss()
self?.categories = allBreed.categories ?? []
self?.populars = allBreed.populars ?? []
self?.downCategories = allBreed.downCategories ?? []
self?.categoryCollectionView.reloadData()
self?.popularCollectionView.reloadData()
self?.specialsCollectionView.reloadData()
case.failure(let error):
ProgressHUD.showError(error.localizedDescription)
}
}
}
}
BreedsVM
import Foundation
protocol BreedsVMInterface {
var view: BreedsViewControllerInterface? { get set }
func viewDidLoad()
func didSelectItemAt(indexPath: IndexPath)
}
final class BreedsVM {
weak var view: BreedsViewControllerInterface?
}
extension BreedsVM: BreedsVMInterface {
func didSelectItemAt(indexPath: IndexPath) {
}
func viewDidLoad() {
view?.prepareCollectionView()
}
}
For example, I want to apply didselectItemAt according to Mvvm. When I want to do this, I get the following error. How can I solve it?
Changed BreedsViewController
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
viewModel.didSelectItemAt(indexPath: indexPath)
}
Changed BreedsVM
import Foundation
protocol BreedsVMInterface {
var view: BreedsViewControllerInterface? { get set }
func viewDidLoad()
func didSelectItemAt(indexPath: IndexPath)
}
final class BreedsVM {
weak var view: BreedsViewControllerInterface?
var categories: [DogCategory] = []
var populars: [Breed] = []
var downCategories:[Breed] = []
}
extension BreedsVM: BreedsVMInterface {
func didSelectItemAt(indexPath: IndexPath) {
if collectionView == categoryCollectionView {
let controller = ListDogsViewController.instantiate()
controller.category = categories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
} else {
let controller = FavoriteDetailViewController.instantiate()
controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
}
}
func viewDidLoad() {
view?.prepareCollectionView()
}
}
BreedsVM's warnings and errors
Cannot find 'categoryCollectionView' in scope Cannot find 'collectionView' in scope Cannot find 'popularCollectionView' in scope
When we move from MVC to any other architecture, we do so to achieve the separation of business logic and UI Logic so for example in MVVM, the ViewModel shouldn't know anything about the UI and also the ViewController should be dumb just makes UI stuff ( changing color, show and hide UI elements, .. ) and also in MVVM, the connection should be from one side the ViewController, the ViewController should have an instance from the ViewModel but the ViewModel should have any reference from the ViewController, but how we achieve the changing of the UI after processing some logic? by binding, and this can be done through number of ways, for example: Combine or RxSwift or even closures, but for simplicity we can start by making the binding using closures so let's take an example:
// ViewModel
class BreedsViewModel {
// MARK: - Closures
var fetchCategoriesSucceeded: ( (_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) -> Void )?
var fetchCategoriesFailed: ( (_ errorMessage: String) -> Void )?
// MARK: - Fetch Categories API
func fetchCategories(){
// Also this should be injected to the ViewModel instead of using it as a singleton, read more about dependency injection
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
self?.fetchCategoriesSucceeded?(allBreed.categories, allBreed.populars, allBreed.downCategories)
case.failure(let error):
self?.fetchCategoriesFailed?(error.localizedDescription)
}
}
}
}
// ViewController
class BreedsViewController: UIViewController {
var viewModel = BreedsViewModel() // This should be injected to the view controller
private var categories: [DogCategory] = []
private var populars: [Breed] = []
private var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
fetchCategories()
}
private func fetchCategories(){
// ProgressHUD.show()
viewModel.fetchCategories()
}
private func bindViewModel() {
viewModel.fetchCategoriesSucceeded = { [weak self] categories, populars, downCategories in
// ProgressHUD.dismiss()
self?.categories = categories
self?.populars = populars
self?.downCategories = downCategories
// collectionView.reloadData()
}
viewModel.fetchCategoriesFailed = { [weak self] errorMessage in
// ProgressHUD.showError(errorMessage)
}
}
}
As you can see now, the ViewModel doesn't know anything about the UI, just getting the data from the API then notify the ViewController through the closure and when the ViewController notified, it should update the UI.
I can see also what you are trying to achive is more related to MVP, there are a Presenter and a ViewController, the Presenter will have a weak reference from the ViewController and update the view controller through a delegate
// Presenter
protocol BreedsPresenterDelegate: AnyObject {
func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed])
func fetchCategoriesFailed(_ errorMessage: String)
}
class BreedsPresenter {
weak var delegate: BreedsPresenterDelegate?
func fetchCategories(){
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
self?.delegate?.fetchCategoriesSucceeded(allBreed.categories, allBreed.populars, allBreed.downCategories)
case.failure(let error):
self?.delegate?.fetchCategoriesFailed(error.localizedDescription)
}
}
}
}
// ViewController
class BreedsViewController: UIViewController {
var presenter = BreedsPresenter() // This should be injected to the view controller
private var categories: [DogCategory] = []
private var populars: [Breed] = []
private var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
presenter.delegate = self
fetchCategories()
}
private func fetchCategories(){
// ProgressHUD.show()
presenter.fetchCategories()
}
}
extension BreedsViewController: BreedsPresenterDelegate {
func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) {
// ProgressHUD.dismiss()
self.categories = categories
self.populars = populars
self.downCategories = downCategories
// collectionView.reloadData()
}
func fetchCategoriesFailed(_ errorMessage: String) {
// ProgressHUD.showError(errorMessage)
}
}
I hope this helps.
I want to use a Combine in my project and face the problem.
Here is the code of the ViewController
import Combine
import UIKit
class ProfileDetailsController: ViewController {
//
// MARK: - Views
#IBOutlet private var tableView: UITableView!
// MARK: - Properties
private typealias DataSource = UITableViewDiffableDataSource<ProfileDetailsSection, ProfileDetailsRow>
private typealias Snapshot = NSDiffableDataSourceSnapshot<ProfileDetailsSection, ProfileDetailsRow>
#Published private var data: [ProfileDetailsSectionModel] = {
return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
}()
private lazy var dataSource: DataSource = {
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self
cell.setData(model: model)
return cell
}
dataSource.defaultRowAnimation = .fade
return dataSource
}()
}
// MARK: - Setup binding
extension ProfileDetailsController {
override func setupBinding() {
tableView.registerCellXib(cell: TextFieldTableCell.self)
$data.receive(on: RunLoop.main).sink { [weak self] models in
let sections = models.map { $0.section }
var snapshot = Snapshot()
snapshot.appendSections(sections)
models.forEach { snapshot.appendItems($0.data, toSection: $0.section) }
self?.dataSource.apply(snapshot, animatingDifferences: true)
}.store(in: &cancellable)
}
}
// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
func switcherAction() { }
}
And here is the code of the cell.
import UIKit
protocol TextFieldTableCellData {
var placeholder: String? { get }
}
protocol TextFieldTableCellDelegate: NSObjectProtocol {
func switcherAction()
}
class TextFieldTableCell: TableViewCell {
//
// MARK: - Views
#IBOutlet private var textField: ZWTextField!
// MARK: - Properties
public weak var delegate: TextFieldTableCellDelegate?
override class var height: CGFloat {
return 72
}
}
// MARK: - Public method
extension TextFieldTableCell {
func setData(model: TextFieldTableCellData) {
textField.placeholder = model.placeholder
}
}
ViewController's deinit was not called.
But when I use this code for ViewController
import UIKit
class ProfileDetailsController: ViewController {
//
// MARK: - Views
#IBOutlet private var tableView: UITableView!
// MARK: - Properties
#Published private var data: [ProfileDetailsSectionModel] = {
return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
}()
}
// MARK: - Startup
extension ProfileDetailsController {
override func startup() {
tableView.dataSource = self
tableView.registerCellXib(cell: TextFieldTableCell.self)
}
}
// MARK: - Startup
extension ProfileDetailsController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = data[indexPath.section].data[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self
cell.setData(model: model)
return cell
}
}
// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
func switcherAction() {}
}
Everything is fine. deinit called. I tried to set dataSource optional and set it nil on deinit, the same result. With Combine deinit called only when I comment this line:
cell.delegate = self
Does anyone know what's the matter?
Xcode 13.2 iOS 15.2
The Combine stuff is a total red herring. That's why you can't locate the problem; you're looking in the wrong place. The issue is the difference between an old-fashioned data source and a diffable data source. The problem is here:
private lazy var dataSource: DataSource = { // *
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self // *
I've starred the problematic lines:
On the one hand, you (self, the view controller) are retaining the dataSource.
On the other hand, you are giving the data source a cell provider function in which you speak of self.
That's a retain cycle! You need to break that cycle. Change
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
To
let dataSource = DataSource(tableView: tableView) { [weak self] tableView, _, model in
(That will compile, because although self is now an Optional, so is cell.delegate.)
In my main VC i bind data like this:
let outputs = viewModel.transform(input: inputs)
outputs.posts.drive(view.collectionView.rx.items(cellIdentifier: "postsPagingCollectionViewCell", cellType: PostsPagingCollectionViewCell.self)) { row, post, cell in
cell.post = post
}.disposed(by: disposeBag)
Cell:
var post: Post! {
didSet {
bindViewModel()
}
}
override func bindViewModel() {
guard let viewModel = cellViewModel as? PostsPagingCellViewModel, let cellView = cellView as? PostsPagingCellView else { return }
let _post = BehaviorRelay<Post>(value: post)
let outputs = viewModel.transform(input: .init(post: _post.asObservable()))
outputs.fullName.drive(cellView.fullNameLabel.rx.text).disposed(by: disposeBag)
outputs.nickname.drive(cellView.nickNameLabel.rx.text).disposed(by: disposeBag)
//etc
}
In my cell i have user profile image, and on tap i want to push specific VC. Also, there could be many buttons and i guess i should handle it in mainVC/mainViewModel. How can i do this?
Loading the profile image will require injecting a loader into the cell's view model.
Pushing a view controller generally relies on the tableView.rx.itemSelected operator so the cell isn't involved at all.
For buttons in the cell, it depends on whether the button's action only affects the contents of the cell or if it affects the view controller itself or the contents of other cells.
---UPDATE---
Given what you have said in the comments, I would write the code like this (all the imports are available in CocoaPods or through SPM):
import Cause_Logic_Effect
import EnumKit
import RxCocoa
import RxEnumKit
import RxSwift
final class MyViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
let disposeBag = DisposeBag()
}
final class MyCell: UITableViewCell {
#IBOutlet weak var pushProfile: UIButton!
#IBOutlet weak var anotherVC: UIButton!
private (set) var disposeBag = DisposeBag()
override func prepareForReuse() {
super.prepareForReuse()
disposeBag = DisposeBag()
}
}
extension MyViewController {
func viewModel() {
let stuff = Observable<[UUID]>.just([UUID(), UUID()]) // build table view here...
let action = PublishSubject<CellAction>()
stuff
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: MyCell.self)) { _, item, cell in
cell.viewModel(item: item)
.bind(to: action)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
action
.capture(case: CellAction.profile)
.bind(onNext: presentScene(animated: true) { item in
ProfileViewController.scene { $0.viewModel(item: item) }
})
.disposed(by: disposeBag)
action
.capture(case: CellAction.other)
.bind(onNext: presentScene(animated: true) { item in
OtherViewController.scene { $0.viewModel(item: item) }
})
.disposed(by: disposeBag)
}
}
extension MyCell {
func viewModel(item: UUID) -> Observable<CellAction> {
return Observable.merge(
pushProfile.rx.tap.map(to: CellAction.profile(item)),
anotherVC.rx.tap.map(to: CellAction.other(item))
)
}
}
enum CellAction: CaseAccessible {
case profile(UUID)
case other(UUID)
}
You likely use a heavier view model structure than I do but the key is that there is a PublishSubject in the view controller and the VC presents scenes based on the events output from that subject. The cells push events into the subject.
What I have: the project, written on SWIFT5, which is an rss reader (I use standard XMLParser ). I fill cells with data from parser. In order to update the data in cells I implemented UIRefreshControl and wrote objc method, which contains the same method(fetchData - see in code), as I use to get data, but it doesn't work. Moreover, this method is called only once, when app is launched. When I close app and then open, data is not updated... How can I deal with it?
What I want: when refreshControl is activated, data in cells should be updated
What I did: I declared a variable called refreshControl, add it to tableView and wrote a method #refresh related to control
import UIKit
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var table: UITableView!
private let url = "my url"
private var rssItems: [RSSItem]? {
didSet {
DispatchQueue.main.async {
self.table.reloadData()
}
}
}
var refreshControl = UIRefreshControl()
#objc func refresh (sender: UIRefreshControl) {
fetchData()
sender.endRefreshing()
}
override func viewDidLoad() {
super.viewDidLoad()
refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: .valueChanged)
self.table.refreshControl = refreshControl
table.addSubview(refreshControl)
fetchData()
}
private func fetchData() {
let feedParser = FeedParser()
feedParser.parseFeed(url: url) { (rssItems) in
self.rssItems = rssItems
DispatchQueue.main.async {
self.table.reloadData()
}
}
}
End refreshing once you get data and set or add refresh control
#objc func refresh (sender: UIRefreshControl) {
fetchData()
}
override func viewDidLoad() {
super.viewDidLoad()
refreshControl.addTarget(self, action: #selector(refresh(sender:)), for: .valueChanged)
self.table.refreshControl = refreshControl
// table.addSubview(refreshControl)
fetchData()
}
private func fetchData() {
let feedParser = FeedParser()
feedParser.parseFeed(url: url) { (rssItems) in
self.rssItems = rssItems // as you are reloading table here
DispatchQueue.main.async {
refreshControl.endRefreshing()
// self.table.reloadData()
}
}
}
How to get the values while moving the UISlider?
I'm using the following code:
ViewModel:
import Foundation
import RxSwift
final class ViewModel {
private let disposeBag = DisposeBag()
var value: Variable<Float>
init() {
self.value = Variable(Float(0.0))
}
}
ViewController:
#IBOutlet var slider: UISlider!
private var viewModel: ViewModel!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
viewModel = ViewModel()
slider.rx.value
.subscribe(onNext: { (value) in
self.viewModel.value = Variable(Float(value))
})
.addDisposableTo(disposeBag)
}
But this code does not work. What's my mistake?
You're replacing the Variable instead of inserting a new value into it. This is guaranteed to fail.
ViewModel.value should be a let instead of a var. You don't want to replace Variable, you want to assign a new value into it. While you are at it, make your ViewModel a struct:
struct ViewModel {
let value = Variable<Float>(0)
}
It can be a final class if you must, but value should still be a let not a var.
Your viewDidLoad should look like this:
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.subscribe(onNext: { value in
self.viewModel.value.value = value
})
.disposed(by: disposeBag)
}
Or better yet:
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.bind(to: viewModel.value)
.disposed(by: disposeBag)
}
Or even better... Whatever is subscribing to ViewModel.value should subscribe/bind directly to slider.rx.value instead. That way you can get rid of the middleman.
Something like this:
public class ViewController: UIViewController {
#IBOutlet weak var slider: UISlider!
#IBOutlet weak var label: UILabel!
private let disposeBag = DisposeBag()
public override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.map { "The slider's value is \($0)" }
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}
You will see the label's text change as you move the slider.
Not tested, but I would try:
override func viewDidLoad() {
viewModel = ViewModel()
slider.rx.value
.subscribe(onNext: { (value) in
self.viewModel.value.value = Float(value)
})
.addDisposableTo(disposeBag)
}
Also I would rename your value property in your viewModel to sliderValue (or whatever, but not value). If you do this, your code will look better:
self.viewModel.sliderValue.value = Float(value)
instead of
self.viewModel.value.value = ...
If you use new BehaviorRelay instead of old Variable:
struct MyViewModel {
let value = BehaviorRelay<Float>(value: 0)
}
class ViewController: UIViewController {
#IBOutlet var slider: UISlider!
#IBOutlet var valueLabel: UILabel!
private var viewModel = MyViewModel()
override func viewDidLoad() {
super.viewDidLoad()
slider.rx.value
.bind(to: viewModel.value)
.disposed(by: rx.disposeBag)
// If you want to listen and bind to a label
viewModel.value.asDriver()
.map { "Value: \($0 * 100)%" }
.drive(valueLabel.rx.text)
.disposed(by: rx.disposeBag)
}
}