I know there are a lot of questions about this, I looked at all of them but it doesn't fix my problem. I also commented on one of them but the question doesn't seem to be active anymore so I don't expect an answer there.
I'm trying to implement RxDataSources. See my code below:
struct ActiveOrdersSection: Equatable {
static func == (lhs: ActiveOrdersSection, rhs: ActiveOrdersSection) -> Bool {
return true
}
var header: String
var orders: [Order]
}
extension ActiveOrdersSection: SectionModelType {
typealias Item = Order
var items: [Item] {
set {
orders = items
}
get {
return orders
}
}
init(original: ActiveOrdersSection, items: [Order]) {
self = original
self.items = items
}
}
And the ViewController:
class MainViewController: UITableViewDelegate, UITableViewDataSource {
var db: DisposeBag?
var dataSource: RxTableViewSectionedReloadDataSource<ActiveOrdersSection>?
private func setupOrderRx(_ shopId: Int64) {
let dataSource = RxTableViewSectionedReloadDataSource<ActiveOrdersSection>(
configureCell: { ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell", for: ip) as! UITableViewCell
cell.textLabel?.text = "Item \(item.id)"
return cell
},
titleForHeaderInSection: { ds, ip in
return ds.sectionModels[ip].header
}
)
self.dataSource = dataSource
db = DisposeBag()
let ors = OrderRxService.listAsShop(shopId, status: .active)
.map { Observable.just($0.items) } // Convert from Observable<CollectionResponse<Order>> to Observable<Order>
.observeOn(MainScheduler.instance)
.bind(to: self.rxTableView.rx.items(dataSource: dataSource))
}
}
I get Generic parameter 'Self' could not be inferred on .bind(to: self.rxTableView.rx.items(dataSource: dataSource)). I looked at the RxDataSources examples and seem to have it the same now, but I can't seem to fix this error.
Any ideas?
The Rx stream you bind to your RxTableViewSectionedReloadDataSource has to be of type Observable<[ActiveOrdersSection]>. I don't know exactly what your types are in this example because the code you provided is not enough but
I think that by using .map { Observable.just($0.items) } the result stream will be of type Observable<Observable<[Order]>>.
Try to change it to:
.map { [ActiveOrdersSection(header: "Your Header", orders: 0.items)] }
Related
I am having a behavior relay in my view model that is used as a source of data. Its defined like this:
var posts: BehaviorRelay<[PostModel]>
It is initialized with data through the network, and it initializes tableView normally when I bind data to it.
Now, if I try to change say, the like status of a post here, like this (this is also in my view model):
private func observeLikeStatusChange() {
self.changedLikeStatusForPost
.withLatestFrom(self.posts, resultSelector: { ($1, $0) })
.map{ (posts, changedPost) -> [PostModel] in
//...
var editedPosts = posts
editedPosts[index] = changedPost // here data is correct, index, changedContact
return editedPosts
}
.bind(to: self.posts)
.disposed(by: disposeBag)
}
So with this, nothing happens. If I remove the element from editedPosts, the tableView updates correctly and removes the row.
PostModel struct conforms to Equatable, and it requires all properties to be the same at the moment.
In my view controller, I create datasource like this:
tableView.rx.setDelegate(self).disposed(by: disposeBag)
let dataSource = RxTableViewSectionedAnimatedDataSource<PostsSectionModel>(
configureCell: { dataSource, tableView, indexPath, item in
//...
return postCell
})
postsViewModel.posts
.map({ posts in
let models = posts.map{ PostCellModel(model: $0) }
return [PostsSectionModel(model: "", items: models)]
}) // If I put debug() here, this is triggered and I get correct section model with correct values
.bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
So, as I said, I am getting correct values, but configureCell is not triggered. What I am doing wrong here?
EDIT:
Here is PostCellModel:
import Foundation
import RxDataSources
typealias PostsSectionModel = AnimatableSectionModel<String, PostCellModel>
struct PostCellModel : Equatable, IdentifiableType {
static func == (lhs: PostCellModel, rhs: PostCellModel) -> Bool {
return lhs.model.id == rhs.model.id
}
var identity: Int {
return model.id
}
var model: PostModel
}
and a PostModel:
struct PostModel: Codable, CellDataModel, Equatable {
static func == (lhs: PostModel, rhs: PostModel) -> Bool {
return
lhs.liked == rhs.liked &&
rhs.title == lhs.title &&
lhs.location == rhs.location &&
lhs.author == rhs.author &&
lhs.created == rhs.created
}
let id: Int
let title: String
let location: String?
let author: String
let created: Int
let liked:Bool
}
You have defined your Equatable conformance incorrectly in the PostCellModel. Because of that, the system is unable to tell whether a cell model has changed... Remove your manually defined ==(lhs:rhs:) and let the system generate them for you and you should be fine...
typealias PostsSectionModel = AnimatableSectionModel<String, PostCellModel>
struct PostCellModel : Equatable, IdentifiableType {
var identity: Int {
return model.id
}
var model: PostModel
}
struct PostModel: Codable, CellDataModel, Equatable {
let id: Int
let title: String
let location: String?
let author: String
let created: Int
let liked:Bool
}
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 a collectionview populated with data models. I am trying to update the bool property of the nested model when user taps on collectionview cell. In turn, the collectionview should reload and cell should be updated w.r.t to bool property. But the property changes in the model is not updating the collectionview.
//Model
struct MultiSelectionQuestionModel {
var header: String
var items: [Item]
}
extension MultiSelectionQuestionModel: SectionModelType {
typealias Item = MultiSelectionAnswerModel
init(original: MultiSelectionQuestionModel, items: [Item]) {
self = original
self.items = items
}
}
struct MultiSelectionAnswerModel {
var text: String
var isSelected: Bool = false //property to be updated
var cellType: CellType = .CustomType
}
//CollectionView methods
func populateCells() {
let dataSource = RxCollectionViewSectionedReloadDataSource
<MultiSelectionQuestionModel>(
configureCell: { (_, collectionView, indexPath, item) in
guard let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: item.cellType.rawValue, for: indexPath) as? MultiSelectionBaseCell else {
return MultiSelectionBaseCell()
}
cell.configure(item: item)
return cell
})
//handle collectionview cell tap
collectionView.rx.itemSelected.asObservable().map { (indexPath) -> Result in
//This method is called to update `isSelected` property. Once `isSelected` is updated. I am expecting the collectionview to reload and update the cell.
self.viewModel.toggleItemSelected(indexPath: indexPath)
}
collectionView.rx.setDelegate(self).disposed(by: disposeBag)
viewModel.items
.bind(to: collectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
//ViewModel
struct MultiSelectionCollectionViewModel {
var items: BehaviorRelay<[MultiSelectionQuestionModel]> = BehaviorRelay(value: [])
var delegate:
init(questions: BehaviorRelay<[MultiSelectionQuestionModel]>) {
self.items = questions
}
//This method is called to update `isSelected` property. Once `isSelected` is updated. I am expecting the collectionview to reload and update the cell.
func toggleItemSelected(indexPath: IndexPath) {
let item = self.items.value[indexPath.section]
if let options = item.items as? [MultiSelectionAnswerModel] {
var optionItem = options[indexPath.row]
optionItem.isSelected = true // Collectionview reload Not working.
}
}
}
I just started learning RxSwift. Any help is appreciated. Thanks
You have to call items.accept(_:) to push a new array out of your BehaviorRelay. In order to do that, you have to build a new array. Also, BehaviorRelays (any Relays or Subjects) should never be vars; they should always be lets.
Also, keep in mind that you can't actually modify the array in the relay. Instead you replace it with a new array.
This should work:
struct MultiSelectionCollectionViewModel {
let items: BehaviorRelay<[MultiSelectionQuestionModel]>
init(questions: BehaviorRelay<[MultiSelectionQuestionModel]>) {
self.items = questions
}
//This method is called to update `isSelected` property. Once `isSelected` is updated. I am expecting the collectionview to reload and update the cell.
func toggleItemSelected(indexPath: IndexPath) {
var multiSelectionQuestionModel = items.value // makes a copy of the array contained in `items`.
var item = multiSelectionQuestionModel[indexPath.section].items[indexPath.row] // makes a copy of the item to be modified
item.isSelected = true // modifies the item copy
multiSelectionQuestionModel[indexPath.section].items[indexPath.row] = item // modifies the copy of items by replacing the old item with the new one
items.accept(multiSelectionQuestionModel) // tells BehaviorRelay to update with the new array of items (it will emit the new array to all subscribers.)
}
}
protocol SectionModelType { }
enum CellType {
case CustomType
}
struct MultiSelectionQuestionModel {
var header: String
var items: [Item]
}
extension MultiSelectionQuestionModel: SectionModelType {
typealias Item = MultiSelectionAnswerModel
init(original: MultiSelectionQuestionModel, items: [Item]) {
self = original
self.items = items
}
}
struct MultiSelectionAnswerModel {
var text: String
var isSelected: Bool = false //property to be updated
var cellType: CellType = .CustomType
}
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
}
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.