How to update data at cell in MVVM? - ios

I'm making a Twitter clone app.
If I press the like button on the posts in each cell, I want to reflect the number of likes to increase. Also, I want to make it reflect in the cell as soon as I delete or modify the post. There is also a video player in the cell like Twitter, but every time I press the like button, the ViewModel accepts again, which causes the video that was running to turn off. Please tell me the best way to implement the Like button...
Cell.swift
func bind(_ viewModel: PostItemViewModel) {
.....
likesLabel.text = String(viewModel.likes)
....
}
....
likesBtn.rx.tapGesture().when(.ended).mapToVoid().asSignalOnErrorJustComplete()
.emit(onNext: { [unowned self] in
delegate?.pressLikesBtn(tag)
}).disposed(by: disposeBag)
ViewController.swift
class PostsViewController: UIViewController {
private let pressLikesRelay = PublishRelay<Int>()
private let deleteRelay = PublishRelay<Int>()
private let disposeBag = DisposeBag()
var viewModel: PostsViewModel!
.......
.......
private func bindViewModel() {
assert(viewModel != nil)
let viewWillAppear = rx.viewWillAppear
.mapToVoid()
.asSignalOnErrorJustComplete()
let pull = timeLineTableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asSignal()
let reachedBottom = timeLineTableView.rx.reachedBottom().asSignal()
let input = PostsViewModel.Input(fetchInitial: Signal.merge(viewWillAppear, pull), fetchNext: reachedBottom, likesTrigger: pressLikesRelay.asSignal(), deleteTrigger: deleteRelay.asSignal())
let output = viewModel.transform(input: input)
output.posts.drive(timeLineTableView.rx.items(cellIdentifier: PostTableViewCell.identifier, cellType: PostTableViewCell.self)) {
row, item, cell in
cell.delegate = self
cell.bind(item)
cell.tag = row
}.disposed(by: disposeBag)
}
extension PostsViewController: PostTableCellDelegate {
func pressLikesBtn(_ index: Int) {
pressRelay.accept(index)
}
func deletePost(_ index: Int) {
deleteRelay.accept(index)
}
}
ViewModel.swift
final class PostsViewModel: ViewModelType {
...
private let postsRelay = BehaviorRelay<[PostItemViewModel]>(value: [])
.....
func transform(input: Input) -> Output {
.......
input.likesTrigger.emit(onNext: { [unowned self] index in
var updated = self.postsRelay.value
updated[index].plusLikes()
self.postsRelay.accept(updated)
}).disposed(by: disposeBag)
input.deleteTrigger.emit(onNext: { [unowned self] index in
var deleted = self.postsRelay.value
deleted.remove(at: index)
self.postsRelay.accept(deleted)
}).disposed(by: disposeBag)
return Output(
...,
posts: postsRelay.asDriver(),
...)
}
}
PostItemViewModel.swift
public struct PostItemViewModel {
.....
var likes: Int
....
mutating func plusLikes() {
self.vote1 +=1
}
}

Here is a sample app that shows how to do what you want. RxMultiCounter.
The key is to only update the table view when you add or remove cells. Each cell needs to independently observe the state and when the inner state of the cell changes, the cell updates directly.

Related

How can I simulate a tableView row selection using RxSwift

I have a UITableViewController setup as below.
View Controller
class FeedViewController: BaseTableViewController, ViewModelAttaching {
var viewModel: Attachable<FeedViewModel>!
var bindings: FeedViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return FeedViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bind(viewModel: FeedViewModel) -> FeedViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: FeedTableViewCell.reuseID, cellType: FeedTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
View Model
class FeedViewModel: ViewModelType {
let fetching: Driver<Bool>
let posts: Driver<[FeedDetailViewModel]>
let selectedPost: Driver<Post>
let errors: Driver<Error>
required init(dependency: Dependency, bindings: Bindings) {
let activityIndicator = ActivityIndicator()
let errorTracker = ErrorTracker()
posts = bindings.fetchTrigger
.flatMapLatest {
return dependency.feedService.getFeed()
.trackActivity(activityIndicator)
.trackError(errorTracker)
.asDriverOnErrorJustComplete()
.map { $0.map(FeedDetailViewModel.init) }
}
fetching = activityIndicator.asDriver()
errors = errorTracker.asDriver()
selectedPost = bindings.selection
.withLatestFrom(self.posts) { (indexPath, posts: [FeedDetailViewModel]) -> Post in
return posts[indexPath.row].post
}
}
typealias Dependency = HasFeedService
struct Bindings {
let fetchTrigger: Driver<Void>
let selection: Driver<IndexPath>
}
}
I have binding that detects when a row is selected, a method elsewhere is triggered by that binding and presents a view controller.
I am trying to assert that when a row is selected, a view is presented, however I cannot successfully simulate a row being selected.
My attempted test is something like
func test_ViewController_PresentsDetailView_On_Selection() {
let indexPath = IndexPath(row: 0, section: 0)
sut.start().subscribe().disposed(by: rx_disposeBag)
let viewControllers = sut.navigationController.viewControllers
let vc = viewControllers.first as! FeedViewController
vc.tableView(tableView, didSelectRowAt: indexPath)
XCTAssertNotNil(sut.navigationController.presentedViewController)
}
But this fails with the exception
unrecognized selector sent to instance
How can I simulate the row is selected in my unit test?
The short answer is you don't. Unit tests are not a place for UIView objects and UI object manipulation. You want to add a UI Testing target for that sort of test.

How edit/delete UICollectionView cells using MVVM and RxSwift

I am trying to understand how to implement MVVM with a list of objects and an UICollectionView. I am not understanding how to implement the User iteration -> Model flow.
I have setup a test application, the Model is just a class with an Int, and the View is an UICollectionViewCell which shows a text with the corresponding Int value and have plus, minus and delete buttons to increment, decrease and remove a element respectively.
Each entry looks like:
I would like to know the best way to use MVVM and RxSwift the update/remove a cell.
I have a list random generated Int values
let items: [Model]
the Model which just have the Int value
class Model {
var number: Int
init(_ n: Int = 0) {
self.number = n
}
}
The ViewModel class which just hold the Model and has an Observable
class ViewModel {
var value: Observable<Model>
init(_ model: Model) {
self.value = Observable.just(model)
}
}
And the Cell
class Cell : UICollectionViewCell {
class var identifier: String { return "\(self)" }
var bag = DisposeBag()
let label: UILabel
let plus: UIButton
let minus: UIButton
let delete: UIButton
....
var viewModel: ViewModel? = nil {
didSet {
....
viewModel.value
.map({ "number is \($0.number)" })
.asDriver(onErrorJustReturn: "")
.drive(self.label.rx.text)
.disposed(by: self.bag)
....
}
}
}
What I don't understand clearly how to do is how to connect the buttons to the corresponding action, update the model and the view afterwards.
Is the Cell's ViewModel responsible for this? Should it be the one receiving the tap event, updating the Model and then the view?
In the remove case, the cell's delete button needs to remove the current Model from the data list. How can this be done without mixing everything all together?
Here is the project with the updates below in GitHub: https://github.com/dtartaglia/RxCollectionViewTester
The first thing we do is to outline all our inputs and outputs. The outputs should be members of the view model struct and the inputs should be members of an input struct.
In this case, we have three inputs from the cell:
struct CellInput {
let plus: Observable<Void>
let minus: Observable<Void>
let delete: Observable<Void>
}
One output for the cell itself (the label) and two outputs for the cell's parent (presumably the view controller's view model.)
struct CellViewModel {
let label: Observable<String>
let value: Observable<Int>
let delete: Observable<Void>
}
Also we need to setup the cell to accept a factory function so it can create a view model instance. The cell also needs to be able to reset itself:
class Cell : UICollectionViewCell {
var bag = DisposeBag()
var label: UILabel!
var plus: UIButton!
var minus: UIButton!
var delete: UIButton!
// code to configure UIProperties omitted.
override func prepareForReuse() {
super.prepareForReuse()
bag = DisposeBag() // this resets the cell's bindings
}
func configure(with factory: #escaping (CellInput) -> CellViewModel) {
// create the input object
let input = CellInput(
plus: plus.rx.tap.asObservable(),
minus: minus.rx.tap.asObservable(),
delete: delete.rx.tap.asObservable()
)
// create the view model from the factory
let viewModel = factory(input)
// bind the view model's label property to the label
viewModel.label
.bind(to: label.rx.text)
.disposed(by: bag)
}
}
Now we need to build the view model's init method. This is where all the real work happens.
extension CellViewModel {
init(_ input: CellInput, initialValue: Int) {
let add = input.plus.map { 1 } // plus adds one to the value
let subtract = input.minus.map { -1 } // minus subtracts one
value = Observable.merge(add, subtract)
.scan(initialValue, accumulator: +) // the logic is here
label = value
.startWith(initialValue)
.map { "number is \($0)" } // create the string from the value
delete = input.delete // delete is just a passthrough in this case
}
}
You will notice that the view model's init method needs more than what is provided by the factory function. The extra info will be provided by the view controller when it creates the factory.
The view controller will have this in its viewDidLoad:
viewModel.counters
.bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { index, element, cell in
cell.configure(with: { input in
let vm = CellViewModel(input, initialValue: element.value)
// Remember the value property tracks the current value of the counter
vm.value
.map { (id: element.id, value: $0) } // tell the main view model which counter's value this is
.bind(to: values)
.disposed(by: cell.bag)
vm.delete
.map { element.id } // tell the main view model which counter should be deleted
.bind(to: deletes)
.disposed(by: cell.bag)
return vm // hand the cell view model to the cell
})
}
.disposed(by: bag)
For the above example I assume that:
counters is of type Observable<[(id: UUID, value: Int)]> and comes from the view controller's view model.
values is of type PublishSubject<(id: UUID, value: Int)> and is input into the view controller's view model.
deletes is of type PublishSubject<UUID> and is input into the view controller's view model.
The construction of the view controller's view model follows the same pattern as the one for the cell:
Inputs:
struct Input {
let value: Observable<(id: UUID, value: Int)>
let add: Observable<Void>
let delete: Observable<UUID>
}
Outputs:
struct ViewModel {
let counters: Observable<[(id: UUID, value: Int)]>
}
Logic:
extension ViewModel {
private enum Action {
case add
case value(id: UUID, value: Int)
case delete(id: UUID)
}
init(_ input: Input, initialValues: [(id: UUID, value: Int)]) {
let addAction = input.add.map { Action.add }
let valueAction = input.value.map(Action.value)
let deleteAction = input.delete.map(Action.delete)
counters = Observable.merge(addAction, valueAction, deleteAction)
.scan(into: initialValues) { model, new in
switch new {
case .add:
model.append((id: UUID(), value: 0))
case .value(let id, let value):
if let index = model.index(where: { $0.id == id }) {
model[index].value = value
}
case .delete(let id):
if let index = model.index(where: { $0.id == id }) {
model.remove(at: index)
}
}
}
}
}
I'm doing it this way:
ViewModel.swift
import Foundation
import RxSwift
import RxCocoa
typealias Model = (String, Int)
class ViewModel {
let disposeBag = DisposeBag()
let items = BehaviorRelay<[Model]>(value: [])
let add = PublishSubject<Model>()
let remove = PublishSubject<Model>()
let addRandom = PublishSubject<()>()
init() {
addRandom
.map { _ in (UUID().uuidString, Int.random(in: 0 ..< 10)) }
.bind(to: add)
.disposed(by: disposeBag)
add.map { newItem in self.items.value + [newItem] }
.bind(to: items)
.disposed(by: disposeBag)
remove.map { removedItem in
self.items.value.filter { (name, _) -> Bool in
name != removedItem.0
}
}
.bind(to: items)
.disposed(by: disposeBag)
}
}
Cell.swift
import Foundation
import Material
import RxSwift
import SnapKit
class Cell: Material.TableViewCell {
var disposeBag: DisposeBag?
let nameLabel = UILabel(frame: .zero)
let valueLabel = UILabel(frame: .zero)
let removeButton = FlatButton(title: "REMOVE")
var model: Model? = nil {
didSet {
guard let (name, value) = model else {
nameLabel.text = ""
valueLabel.text = ""
return
}
nameLabel.text = name
valueLabel.text = "\(value)"
}
}
override func prepare() {
super.prepare()
let textWrapper = UIStackView()
textWrapper.axis = .vertical
textWrapper.distribution = .fill
textWrapper.alignment = .fill
textWrapper.spacing = 8
nameLabel.font = UIFont.boldSystemFont(ofSize: 24)
textWrapper.addArrangedSubview(nameLabel)
textWrapper.addArrangedSubview(valueLabel)
let wrapper = UIStackView()
wrapper.axis = .horizontal
wrapper.distribution = .fill
wrapper.alignment = .fill
wrapper.spacing = 8
addSubview(wrapper)
wrapper.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(8)
}
wrapper.addArrangedSubview(textWrapper)
wrapper.addArrangedSubview(removeButton)
}
}
ViewController.swift
import UIKit
import Material
import RxSwift
import SnapKit
class ViewController: Material.ViewController {
let disposeBag = DisposeBag()
let vm = ViewModel()
let tableView = UITableView()
let addButton = FABButton(image: Icon.cm.add, tintColor: .white)
override func prepare() {
super.prepare()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
addButton.pulseColor = .white
addButton.backgroundColor = Color.red.base
view.layout(addButton)
.width(48)
.height(48)
.bottomRight(bottom: 16, right: 16)
addButton.rx.tap
.bind(to: vm.addRandom)
.disposed(by: disposeBag)
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
vm.items
.bind(to: tableView.rx.items) { (tableView, row, model) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
cell.model = model
cell.disposeBag = DisposeBag()
cell.removeButton.rx.tap
.map { _ in model }
.bind(to: self.vm.remove)
.disposed(by: cell.disposeBag!)
return cell
}
.disposed(by: disposeBag)
}
}
Note that a common mistake is creating the DisposeBag inside Cell only once, this will causing confusing when the action got triggered.
The DisposeBag must be re-created every time the Cell got reused.
A complete working example can be found here.

RxSwift correct way to subscribe to data model property change

New to RxSwift here. I have a (MVVM) view model that represents a Newsfeed-like page, what's the correct way to subscribe to change in data model's properties? In the following example, startUpdate() constantly updates post. The computed properties messageToDisplay and shouldShowHeart drives some UI event.
struct Post {
var iLiked: Bool
var likes: Int
...
}
class PostViewModel: NSObject {
private var post: Post
var messageToDisplay: String {
if post.iLiked { return ... }
else { return .... }
}
var shouldShowHeart: Bool {
return iLiked && likes > 10
}
func startUpdate() {
// network request and update post
}
...
}
It seems to me in order to make this whole thing reactive, I have to turn each properties of Post and all computed properties into Variable? It doesn't look quite right to me.
// Class NetworkRequest or any name
static func request(endpoint: String, query: [String: Any] = [:]) -> Observable<[String: Any]> {
do {
guard let url = URL(string: API)?.appendingPathComponent(endpoint) else {
throw EOError.invalidURL(endpoint)
}
return manager.rx.responseJSON(.get, url)
.map({ (response, json) -> [String: Any] in
guard let result = json as? [String: Any] else {
throw EOError.invalidJSON(url.absoluteString)
}
return result
})
} catch {
return Observable.empty()
}
}
static var posts: Observable<[Post]> = {
return NetworkRequest.request(endpoint: postEndpoint)
.map { data in
let posts = data["post"] as? [[String: Any]] ?? []
return posts
.flatMap(Post.init)
.sorted { $0.name < $1.name }
}
.shareReplay(1)
}()
class PostViewModel: NSObject {
let posts = Variable<[Post]>([])
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
posts
.asObservable()
.subscribe(onNext: { [weak self] _ in
DispatchQueue.main.async {
//self?.tableView?.reloadData() if you want to reload all tableview
self.tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation.none) // OR if you want to insert one or multiple rows.
//OR update UI
}
})
.disposed(by: disposeBag)
posts.asObservable()
.bind(to: tableView.rx.items) { [unowned self] (tableView: UITableView, index: Int, element: Posts) in
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell") as! PostCell
//for example you need to update the view model and cell with textfield.. if you want to update the ui with a cell then use cell.button.tap{}. hope it works for you.
cell.textField.rx.text
.orEmpty.asObservable()
.bind(to: self.posts.value[index].name!)
.disposed(by: cell.disposeBag)
return cell
}
startDownload()
}
}
func startDownload {
posts.value = NetworkRequest.posts
}
If you want to change anything then use subscribe, bind, concat .. There are many methods and properties which you can use.

Dynamically filter results with RxSwift and Realm

I have a very simple project, where I want to dynamically filter content in UITableView regarding pressed index in UISegmentedControl. I'm using MVVM with RxSwift, Realm and RxDataSources. So my problem, that if I want to update content in UITableView I need to create 'special' DisposeBag, only for that purposes, and on each selection in UISegmentedControl nil it and create again. Only in this case, if I'm understand right, subscription is re-newed, and UITableView displays new results from Realm.
So is there any better way to do such operation? Without subscribing every time, when I switch tab in UISegmentedControl. Here's my code:
//ViewController
class MyViewController : UIViewController {
//MARK: - Props
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var segmentedControl: UISegmentedControl!
let dataSource = RxTableViewSectionedReloadDataSource<ItemsSection>()
let disposeBag = DisposeBag()
var tableViewBag: DisposeBag!
var viewModel: MyViewModel = MyViewModel()
//MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.setupRxTableView()
}
//MARK: - Setup observables
fileprivate func setupRxTableView() {
dataSource.configureCell = { ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "ItemCell") as! ItemTableViewCell
return cell
}
bindDataSource()
segmentedControl.rx.value.asDriver()
.drive(onNext: {[weak self] index in
guard let sSelf = self else { return }
switch index {
case 1:
sSelf.bindDataSource(filter: .active)
case 2:
sSelf.bindDataSource(filter: .groups)
default:
sSelf.bindDataSource()
}
}).disposed(by: disposeBag)
}
private func bindDataSource(filter: Filter = .all) {
tableViewBag = nil
tableViewBag = DisposeBag()
viewModel.populateApplying(filter: filter)
}).bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: tableViewBag)
}
}
//ViewModel
class MyViewModel {
func populateApplying(filter: Filter) -> Observable<[ItemsSection]> {
return Observable.create { [weak self] observable -> Disposable in
guard let sSelf = self else { return Disposables.create() }
let realm = try! Realm()
var items = realm.objects(Item.self).sorted(byKeyPath: "date", ascending: false)
if let predicate = filter.makePredicate() { items = items.filter(predicate) }
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
sSelf.itemsToken = items.addNotificationBlock { changes in
switch changes {
case .update(_, _, _, _):
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
default: break
}
}
return Disposables.create()
}
}
}
Don't recall if this is breaking MVVM off the top of my head, but would Variable not be what you're looking for?
Variable<[TableData]> data = new Variable<[TableData]>([])
func applyFilter(filter: Predicate){
data.value = items.filter(predicate) //Any change to to the value will cause the table to reload
}
and somewhere in the viewController
viewModel.data.rx.asDriver().drive
(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemTableViewCell.self))
{ row, data, cell in
//initialize cells with data
}

RxSwift+Moya+Moya_ObjectMapper+MJRefresh, refresh failed?

I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind

Resources