RxSwift handle cell actions - mvvm

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.

Related

Swift convert Design Pattern Mvc to Mvvm how can I transfer code block?

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.

reloadData() from another viewController Swift

I have two viewControllers: the first one has a tableView in it and the second one has a textField with an action, if there is a specific text inserted in the textFiled, I want to call loadData1() function which has orderTable.reloadData() to reload the tableView from the logInviewController, but it returns nil when I call it.
tableViewController code :
import UIKit
import FirebaseFirestore
import Firebase
import FirebaseAuth
class orderTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet var orderTable: UITableView!
var db: Firestore!
var firstName = [String]()
var lastName = [String]()
override func viewDidLoad() {
super.viewDidLoad()
orderTable.register(UINib(nibName: "Order1TableViewCell", bundle: nil) , forCellReuseIdentifier: "orderCell")
}
func loadData1() {
Firestore.firestore().collection("hola").getDocuments() { [self]
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
self.firstName.append(document.get("firstname") as? String ?? "")
self.lastName.append(document.get("lastname") as? String ?? "")
}
}
orderTable.reloadData() // from here i got Unexpectedly found nil while unwrapping an Optional value:
}
}
}
}
logInViewController code :
import UIKit
import Firebase
import FirebaseAuth
class logInViewController: UIViewController, UITextFieldDelegate {
#IBOutlet var userNameField: UITextField!
#IBOutlet var passwordField: UITextField!
#IBOutlet var logInButton: UIButton!
var db: Firestore!
var order: orderTableViewController!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func textfieldDidChange(_ sender: Any) {
print(userNameField?.text ?? "")
if userNameField.text == "v#v.com" {
let i = orderTableViewController()
i.loadData1()
}
}
}
Where you have let i = orderTableViewController(), you are not referencing your existing table view controller, but rather are creating a new one, except this time it is not instantiated in conjunction with the storyboard scene, and thus all of your #IBOutlet references will be nil. Attempts to reference those #IBOutlet references will fail.
To fix this, you should pass a reference for the first view controller to the second one, using a protocol rather than an explicit class name, and then the second view controller can call a method in the first. Thus:
Create class protocol, e.g. LoginViewControllerDelegate:
protocol LoginViewControllerDelegate: class { }
Give that protocol one method requirement, loadData1:
protocol LoginViewControllerDelegate: class {
func loadData1()
}
Make your first view controller conform to that protocol:
extension OrderTableViewController: LoginViewControllerDelegate {
func loadData1() {
... your implementation here ...
}
}
Create a property in the second view controller, that LoginViewController, for this delegate-protocol reference, e.g.:
weak var delegate: LoginViewControllerDelegate?
When first view controller instantiates second, set this delegate property (e.g. if doing segues, it would be in prepareForSegue):
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? LoginViewController {
destination.delegate = self
}
}
The second view controller then would call delegate?.loadData1() rather than i.loadData1().
If you do what I understand then you can do this. But you should use delegate or closure callback to do that.
#IBAction func textfieldDidChange(_ sender: Any) {
print(userNameField?.text ?? "")
if userNameField.text == "v#v.com" {
if let i = order {
i.loadData1()
}
}
}
}

How to update RSS data in UITableView?

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

RxSwift. What to use for Observable<Void>?

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

How to get change the value of the UISlider

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