I'm using RxSwift and currently, I have a list of addresses that could be edited inline
I.E: The user clicks on a button and the cell is transformed into editing mode essentially displaying a few UITextFields
Now the problem is that when I bind the input of my TextField to my model the TableView gets reloaded and therefore keyboard dismisses, I've also tried RxAnimatableDataSource but no avail it still dismisses the keyboard on each keystroke.
ViewModel:
class UpdateAddressViewModel: ViewModel, ViewModelType {
struct Input {
let viewDidLoad: AnyObserver<Void>
let selectItemTrigger: AnyObserver<Int>
let editButtonTrigger: AnyObserver<Int>
let editTrigger: AnyObserver<Int>
let firstNameIndexed: AnyObserver<(String, Int)>
let lastNameIndexed: AnyObserver<(String, Int)>
let address1Indexed: AnyObserver<(String, Int)>
let address2Indexed: AnyObserver<(String, Int)>
let zipIndexed: AnyObserver<(String, Int)>
let cityIndexed: AnyObserver<(String, Int)>
let stateIndexed: AnyObserver<(String, Int)>
let phoneIndexed: AnyObserver<(String, Int)>
}
struct Output {
let addresses: Driver<[AddressViewModel]>
let reloadAndScroll: Driver<(Int, Bool)>
let showMenu: Driver<Int>
let error: Driver<Error>
}
private(set) var input: Input!
private(set) var output: Output!
//Input
private let viewDidLoad = PublishSubject<Void>()
private let editButtonTrigger = PublishSubject<Int>()
private let editTrigger = PublishSubject<Int>()
private let firstNameIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let lastNameIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let address1Indexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let address2Indexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let zipIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let cityIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let stateIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let phoneIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
//Output
private let addresses = BehaviorRelay<[AddressViewModel]>(value: [AddressViewModel()])
override init() {
super.init()
observeViewDidLoad()
observeEditAddress()
.bind(to: addresses)
.disposed(by: disposeBag)
firstNameIndexed
.withLatestFrom(addresses) { firstNameIndexed, viewModels -> (String, Int, [AddressViewModel]) in
let (firstName, index) = firstNameIndexed
return (firstName, index, viewModels)
}
.map { firstName, index, viewModels in
var viewModels = viewModels
viewModels[index].address.firstName = firstName
return viewModels
}
.bind(to: addresses)
.disposed(by: disposeBag)
input = Input(
viewDidLoad: viewDidLoad.asObserver(),
selectItemTrigger: selectItemTrigger.asObserver(),
editButtonTrigger: editButtonTrigger.asObserver(),
editTrigger: editTrigger.asObserver(),
firstNameIndexed: firstNameIndexed.asObserver(),
lastNameIndexed: lastNameIndexed.asObserver(),
address1Indexed: address1Indexed.asObserver(),
address2Indexed: address2Indexed.asObserver(),
zipIndexed: zipIndexed.asObserver(),
cityIndexed: cityIndexed.asObserver(),
stateIndexed: stateIndexed.asObserver(),
phoneIndexed: phoneIndexed.asObserver()
)
output = Output(
addresses: addresses.asDriver(onErrorJustReturn: []),
reloadAndScroll: reloadAndScroll,
showMenu: editButtonTrigger.asDriverOnErrorJustComplete(),
error: errorTracker.asDriver()
)
}
private func observeViewDidLoad() {
//Loading work here
}
private func observeEditAddress() -> Observable<[AddressViewModel]> {
editTrigger
.withLatestFrom(addresses) { index, viewModels in
return (index, viewModels)
}
.map { index, viewModels in
var viewModels = viewModels
for currentIndex in viewModels.indices {
viewModels[currentIndex].isSelected = currentIndex == index
viewModels[currentIndex].isEditing = currentIndex == index
if currentIndex == index {
viewModels[currentIndex].copyAddress()
}
}
return viewModels
}
}
}
And here's my ViewController binding of data source to tableview
viewModel
.output
.addresses
.drive(tableView.rx.items) { [weak self] tableView, row, viewModel in
guard let self = self else { return UITableViewCell() }
let indexPath = IndexPath(row: row, section: 0)
if viewModel.address.rechargeId == nil && viewModel.address.shopifyId == nil {
let cell: NewAddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
return cell
} else if viewModel.isEditing {
let cell: UpdateAddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
cell
.rx
.firstName
.map { ($0, row) }
.bind(to: self.viewModel.input.firstNameIndexed)
.disposed(by: cell.disposeBag)
return cell
} else {
let cell: AddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
cell
.rx
.editButtonTapped
.map { _ in return row }
.bind(to: self.viewModel.input.editButtonTrigger)
.disposed(by: cell.disposeBag)
return cell
}
}
.disposed(by: disposeBag)
You need to disconnect the stream that causes table view updates from the stream that changes the addresses.
And example might be something like:
typealias ID = Int // an example
typealias Address = String // probably needs to be more involved.
struct Input {
let updateAddress: Observable<(ID, Address)>
}
struct Output {
let cells: Observable<[ID]>
let addresses: Observable<[ID: Address]>
}
The table view would be bound to the cells while each cell would be bound to the addresses observable with a compactMap to extract its particular information.
That way, you can update a particular cell without reloading any of the cells in the table view.
A fully fleshed out example of this can be found in my RxMultiCounter example.
Related
I am trying to make multiple sections (two actually) using RxDatasources. Usually with one section, I would go like this:
Section model:
import Foundation
import RxDataSources
typealias NotificationSectionModel = AnimatableSectionModel<String, NotificationCellModel>
struct NotificationCellModel : Equatable, IdentifiableType {
static func == (lhs: NotificationCellModel, rhs: NotificationCellModel) -> Bool {
return lhs.model.id == rhs.model.id
}
var identity: String {
return model.id
}
var model: NotificationModel
var cellIdentifier = "NotificationTableViewCell"
}
then the actual model:
struct NotificationModel: Codable, Equatable {
let body: String
let title:String
let id:String
}
And I would use that like this (in view controler):
private func observeTableView(){
let dataSource = RxTableViewSectionedAnimatedDataSource<NotificationSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
if let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseTableViewCell{
cell.setup(data: item.model)
return cell
}
return UITableViewCell()
})
notificationsViewModel.notifications
.map{ notifications -> [NotificationCellModel] in
return notifications.map{ NotificationCellModel( model: $0, cellIdentifier: NotificationTableViewCell.identifier) }
}.map{ [NotificationSectionModel(model: "", items: $0)] }
.bind(to: self.tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
}
But how I would go with multiple sections, with different type of models/cells?
Here is a kind of worst case situation. You might be able to simplify this code depending on your use case:
// MARK: Model Code
struct ViewModel {
let sections: Observable<[SectionModel]>
}
typealias SectionModel = AnimatableSectionModel<String, CellModel>
enum CellModel: IdentifiableType, Equatable {
case typeA(TypeAInfo)
case typeB(TypeBInfo)
var identity: Int {
switch self {
case let .typeA(value):
return value.identity
case let .typeB(value):
return value.identity
}
}
var cellIdentifier: String {
switch self {
case .typeA:
return "TypeA"
case .typeB:
return "TypeB"
}
}
}
struct TypeAInfo: IdentifiableType, Equatable {
let identity: Int
}
struct TypeBInfo: IdentifiableType, Equatable {
let identity: Int
}
// MARK: View Code
class Example: UIViewController {
var tableView: UITableView!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
private func observeTableView(){
let dataSource = RxTableViewSectionedAnimatedDataSource<SectionModel>(
configureCell: { _, tableView, indexPath, item in
guard let cell = tableView.dequeueReusableCell(withIdentifier: item.cellIdentifier, for: indexPath) as? BaseCell else { fatalError() }
cell.setup(model: item)
return cell
})
viewModel.sections
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
class BaseCell: UITableViewCell {
func setup(model: CellModel) { }
}
final class TypeACell: BaseCell { }
final class TypeBCell: BaseCell { }
I have simple tableview. . When I want to delete cell from table view, I get that error.
View controller code:
class FoodCategoryDetailTableViewController: UITableViewController {
var foodCategoryDetailViewModel: FoodCategoryDetailTableViewViewModelType?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
guard let foodCategoryDetailViewModel = foodCategoryDetailViewModel else { return }
tableView.delegate = nil
tableView.dataSource = nil
foodCategoryDetailViewModel.foodsInSelectedCategory
.bind(to: tableView.rx.items(cellIdentifier: FoodCategoryDetailTableViewCell.cellIdentifier, cellType: FoodCategoryDetailTableViewCell.self))
{ row, food, cell in
cell.foodCategoryDetailCellViewModel = foodCategoryDetailViewModel.cellViewModel(forRow: row)
}.disposed(by: disposeBag)
tableView.rx.itemDeleted.subscribe({ [unowned self] indexPath in
self.foodCategoryDetailViewModel?.removeFoodFromApplication(atRow: (indexPath.element?.row)!)
self.tableView.reloadData()
}).disposed(by: disposeBag)
}
View Model code:
class FoodCategoryDetailTableViewViewModel: FoodCategoryDetailTableViewViewModelType {
var foodsInSelectedCategory: BehaviorRelay<[Food]>
private var foodCategoryId: Int16
func cellViewModel(forRow row: Int) -> FoodTableViewCellViewModelType? {
if let food = getFood(atRow: row) {
return FoodTableViewCellViewModel(foodModel: food)
}
return nil
}
func removeFoodFromApplication(atRow row: Int) {
if let food = getFood(atRow: row) {
var foods = foodsInSelectedCategory.value
foods.remove(at: row)
self.foodsInSelectedCategory = BehaviorRelay(value: foods)
CoreDataHelper.sharedInstance.removeFoodFromApplication(foodName: food.name!)
}
}
private func getFood(atRow row: Int) -> Food? {
let food = foodsInSelectedCategory.value[row]
return food
}
init(foodCategoryId: Int16) {
self.foodCategoryId = foodCategoryId
self.foodsInSelectedCategory = BehaviorRelay(value: CoreDataHelper.sharedInstance.fetchFoodsFromSelectedCategory(foodCategoryId: self.foodCategoryId))
}
}
I am using Core Data. I get error in function getFood(). It error exist because row in view controller have old count of items in tableview. It is not updating with new count of items (foods) after delete cell.
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.
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.
How can I disable auto scroll to the top of table view when I append new data to data source of it.
The problem is visible in the following gif.
Edit: Added ViewController, ViewModel and MessageEntity.
Used frameworks are: RxSwift, RxDataSources for reactive datasource of table view.
ViewController:
class RabbitMqVC: BaseViewController {
struct Cells {
static let message = ReusableCell<MessageCell>(nibName: "MessageCell")
static let messageTheir = ReusableCell<MessageCellTheir>(nibName: "MessageCellTheir")
}
#IBOutlet
weak var tableView: UITableView!{
didSet{
rabbitMqViewModel.sections
.drive(tableView.rx.items(dataSource: dataSource))
.addDisposableTo(disposeBag)
}
}
private let dataSource = RxTableViewSectionedAnimatedDataSource<RabbitMqViewModel.MessageSections>()
private let rabbitMqViewModel : rabbitMqViewModel
init(rabbitMqViewModel: rabbitMqViewModel) {
self.rabbitMqViewModel = rabbitMqViewModel
super.init(nibName: "RabbitMqVC", bundle: nil)
dataSource.configureCell = { _, tableView, indexPath, item in
let randomNumber = 1.random(to: 2)
let cell = randomNumber == 1 ? tableView.dequeue(Cells.message, for: indexPath) : tableView.dequeue(Cells.messageTheir, for: indexPath)
cell.message = item
return cell
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cells.message)
tableView.register(Cells.messageTheir)
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 80
}
}
ViewModel:
class RabbitMqViewModel: ViewModel {
enum MessageSections: AnimatableSectionModelType {
typealias Item = MessageEntity
typealias Identity = Int
case messages(messages: [MessageEntity])
var items: [Item] {
switch self {
case .messages(messages:let messages):
return messages
}
}
var identity: Int {
return 1
}
init(original: MessageSections, items: [Item]) {
switch original {
case .messages:
self = .messages(messages: items)
}
}
}
// input
let didLoad = PublishSubject<Void>()
//output
let sections: Driver<[MessageSections]>
init(service: RabbitMqService,){
let messages: Observable<[MessageEntity]> = didLoad
.flatMapLatest { _ -> Observable<[MessageEntity]> in
return service.listenMessages()
}
.share()
self.sections = messages
.map { (messages) -> [RabbitMqViewModel.MessageSections] in
var sections: [MessageSections] = []
sections.append(.messages(messages: messages))
return sections
}
.asDriver(onErrorJustReturn: [])
}
}
MessageEntity:
struct MessageEntity {
let id: String
let conversationId: String
let messageText: String
let sent: Date
let isSentByClient: Bool
let senderName: String
let commodityClientId : Int?
}
extension MessageEntity: IdentifiableType, Equatable {
typealias Identity = Int
public var identity: Identity {
return id.hashValue
}
public static func ==(lhs: MessageEntity, rhs: MessageEntity) -> Bool {
return lhs.id == rhs.id
}
}
estimatedRowHeight = 1
Fixed it.