cellProvider doesn't update after section removal when using UITableViewDiffableDataSource - swift

I have some code which loads in some empty sections and items. Then if there is a network connection some calls are made for the visible cells. I have a button in each section which navigates to another view based on the section type, which uses the indexPath. My problem is that when I remove the fist section the cellProvider doesn't update the indexPath and button navigates to the wrong view. If I scroll however the problem goes away. How do I make sure the indexPath updates? EDIT: It appears that my headerViews are retaining their old indexPath, but the cellProvider correctly updates. The header views are set to the correct indexPath once you scroll away and come back.
private enum RowItemType: Hashable {
case itemTypeOne(collection: MyCollection)
case itemTypeTwo(collection: MyCollection)
case itemTypeThree(collection: OtherCollection)
}
private enum SectionType: Hashable {
case sectionOne
case sectionTwo
case sectionThree
}
class MyCollection: Codable, Hashable, Equatable {
var identifier = UUID()
var foos: [Foo]
init(foos: [Foo] = [Foo]()) {
self.foos = foos
}
// MARK: Equatable
var hash: Int {
var hasher = Hasher()
hasher.combine(identifier)
return hasher.finalize()
}
static func == (lhs: MyCollection, rhs: MyCollection) -> Bool {
lhs.identifier == rhs.identifier
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
class OtherCollection: Codable, Hashable, Equatable {
var identifier = UUID()
var bars: [Bar]
init(bars: [Bar] = [Bar]()) {
self.bars = bars
}
// MARK: Equatable
var hash: Int {
var hasher = Hasher()
hasher.combine(identifier)
return hasher.finalize()
}
static func == (lhs: OtherCollection, rhs: OtherCollection) -> Bool {
lhs.identifier == rhs.identifier
}
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
}
final class SomeTableViewController: UITableVIewController {
...code....
RELEVENT CODE
private func makeDataSource() -> UITableViewDiffableDataSource<SectionType, RowItemType> {
UITableViewDiffableDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, item in
guard let self = self else {
return UITableViewCell()
}
guard let marginTableView = tableView as? AutoMarginTableView else {
return UITableViewCell()
}
switch item {
case .itemTypeOne(collection: let collection):
let carousel = self.itemOneCarouCell(from: marginTableView, withSection: .sectionOne, atIndexPath: indexPath)
if !collection.foos.isEmpty {
carousel.configureCell(with: .sectionOne, fooCollection: collection)
self.showViewAllButtonForHeader(collection.foos.count > 3, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getFoosTypeOne(collection: collection)
}
return carousel
case .itemTypeTwo(let collection):
let carousel = self.itemTwoCarouCell(from: marginTableView, withSection: .sectionTwo, atIndexPath: indexPath)
if !collection.foos.isEmpty {
carousel.configureCell(with: .sectionTwo, fooCollection: collection)
self.showViewAllButtonForHeader(collection.foos.count > 3, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getFoosTypeTwo(collection: collection)
}
return carousel
case .ttemTypeThree(let collection):
let carousel = self.itemThreeCarouCell(from: marginTableView, withSection: .sectionThree, atIndexPath: indexPath)
if !collection.bars.isEmpty {
carousel.configureCell(with: collection)
self.showViewAllButtonForHeader(true, atIndexPath: indexPath)
} else {
carousel.configureCellForLoading()
self.getBars(collection: collection)
}
return carousel
})
}
private func showViewAllButtonForHeader(_ show: Bool, atIndexPath indexPath: IndexPath) {
if let headerView = self.tableView.headerView(forSection: indexPath.section) as? MyTableHeaderView {
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut, animations: {
headerView.showViewAll(show)
}, completion: nil)
}
}
private func getFoosTypeOne(collection: MyCollection) {
var snapshot = dataSource.snapshot()
if FeatureFlags.isFeatureEnabled && snapshot.sectionIdentifiers.contains(.sectionOne) {
someRepositry.getfoos { result in
if case .success(let foos) = result {
if !foos.isEmpty {
collection.foos = foos
snapshot.reloadItems([.itemTypeOne(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionOne) {
snapshot.deleteSections([.sectionOne])
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
private func getFoosTypeTwo(collection: MyCollection) {
var snapshot = dataSource.snapshot()
if FeatureFlags.isFeatureEnabled && snapshot.sectionIdentifiers.contains(.sectionTwo) {
someRepositry.getDifferentFoos { result in
if case .success(let foos) = result {
if !foos.isEmpty {
collection.foos = foos
snapshot.reloadItems([.itemTypeTwo(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionTwo) {
snapshot.deleteSections([.sectionTwo])
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
private func getBars(collection: OtherCollection) {
var snapshot = dataSource.snapshot()
someService.getSomeCollection { someColl in
DispatchQueue.main.async {
if let someColl = someColl {
if !someColl.bars.isEmpty {
collection.bars = someColl.bars
snapshot.reloadItems([.itemTypeThree(collection: collection)])
} else {
if snapshot.sectionIdentifiers.contains(.sectionThree) {
snapshot.deleteSections([.sectionThree])
}
}
}
self.dataSource.apply(snapshot, animatingDifferences: true)
}
}
}
}
extension SomeTabelViewController: SomeTableHeaderViewDelegate {
func someTableHeaderViewTappedViewAll(_ headerView: SomeTableHeaderView) {
guard headerView.section < dataSource.numberOfSections(in: tableView) else {
return
}
let snapshot = dataSource.snapshot()
let section = snapshot.sectionIdentifiers[headerView.section]
switch section {
case .sectionOne:
if case .itemTypeOne(let collection) = snapshot.itemIdentifiers(inSection: .sectionOne).first, !collection.foos.isEmpty {
let vc = SomeListViewController()
navigationController?.pushViewController(vc, animated: true)
}
case .sectionTwo:
if case .itemTypeTwo(let collection) = snapshot.itemIdentifiers(inSection: .sectionTwo).first, !collection.foos.isEmpty {
let vc = SomeListViewController()
navigationController?.pushViewController(vc, animated: true)
}
case .sectionThree:
if case .itemTypeThree(let collection) = snapshot.itemIdentifiers(inSection: .sectionThree).first, !collection.bars.isEmpty {
let vc = OtherViewController(collecton: collection)
navigationController?.pushViewController(vc, animated: true)
}
}
}

Related

swift Diffable data source error - 'Fatal: supplied item identifiers are not unique. Duplicate identifiers:

I think there is a problem with the calendar model or the data I put in the pile, but I can't find it no matter how much I look for it. I'm desperate for help 🥲
The contents of the error are as follows
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Fatal: supplied item identifiers are not unique. Duplicate identifiers: {(
HomeDataSource.Item.calendar
class HomeDataSource {
typealias DataSource = UITableViewDiffableDataSource<Section, Item>
private let tableView: UITableView
private lazy var dataSource = createDataSource()
private let postType: PostCase
var calendars: [Calendar] = [
Calendar(date: "haha"),
Calendar(date: "hhohoho"),
]
private var posts: [UserPost]
enum Section: CaseIterable {
case calendar
case post
init(rawValue: Int) {
switch rawValue {
case 0: self = .calendar
case 1: self = .post
default:
fatalError("not exist section")
}
}
}
enum Item: Hashable {
case calendar
case post
}
init(tableView: UITableView, postType: PostCase) {
self.tableView = tableView
self.postType = postType
self.posts = .init()
}
func createDataSource() -> UITableViewDiffableDataSource<Section, Item> {
tableView.register(CalendarTableViewCell.self, forCellReuseIdentifier: CalendarTableViewCell.identifier)
tableView.register(UserPostTableViewCell.self, forCellReuseIdentifier: UserPostTableViewCell.identifier)
tableView.register(EmptyPostTableViewCell.self, forCellReuseIdentifier: EmptyPostTableViewCell.identifier)
dataSource = UITableViewDiffableDataSource<Section, Item>(tableView: tableView) {
tableView, indexPath, item in
switch item {
case .calendar:
let cell = tableView.dequeueReusableCell(withIdentifier: CalendarTableViewCell.identifier, for: indexPath)
cell.selectionStyle = .none
return cell
case .post:
switch self.postType {
case .postExist:
guard let cell = tableView.dequeueReusableCell(withIdentifier: UserPostTableViewCell.identifier, for: indexPath) as? UserPostTableViewCell else { return UITableViewCell() }
cell.selectionStyle = .none
cell.configure()
return cell
case .friendPostEmpty:
guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptyPostTableViewCell.identifier, for: indexPath) as? EmptyPostTableViewCell else { return UITableViewCell() }
cell.configure(isFriend: true)
return cell
case .ownerPostEmpty:
guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptyPostTableViewCell.identifier, for: indexPath) as? EmptyPostTableViewCell else { return UITableViewCell() }
cell.configure(isFriend: false)
return cell
}
}
}
return dataSource
}
func updateSnapshot(posts: [UserPost]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.calendar, .post])
let calendarIds = calendars.map { _ in Item.calendar }
let postIds = posts.map { _ in Item.post }
snapshot.appendItems(calendarIds, toSection: .calendar)
snapshot.appendItems(postIds, toSection: .post)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
Calendar model is as follows
struct Calendar: Hashable {
let id = UUID()
let date: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Calendar, rhs: Calendar) -> Bool {
lhs.id == rhs.id
}
init(date: String) {
self.date = date
}
}
Also, I would like to ask you additionally what should I do if I want to put only one value in the snapshot data?
The problem is here:
let calendarIds = calendars.map { _ in Item.calendar }
let postIds = posts.map { _ in Item.post }
You just map all the different models into equal Item. Any Calendar with unique id turns into Item.calendar which have no id. All of them are equal.
if you still want to us enum then I suggest you declare it like that:
enum Item {
case calendar(Calendar)
case post(UserPost)
}
extension Item: Hashable {
public static func == (lhs: Item, rhs: Item) -> Bool {
switch (lhs, rhs) {
case (.calendar(let calendar1), .calendar(let calendar2)):
return calendar1.id == calendar2.id
case (.post(let post1), .post(let post2)):
return post1.id == post2.id
default:
return false
}
}
public func hash(into hasher: inout Hasher) {
switch self {
case .calendar(let calendar):
hasher.combine("\(calendar.id)_calendar")
case .post(let post):
hasher.combine("\(post.id)_post")
}
}
}
This should help you.
Moreover, as vadian mentioned, you better not use Calendar. Rename it to UserCalendar for example.

How to model a tree structure using NSDiffableDataSourceSectionSnapshot?

How can you model a tree structure using NSDiffableDataSourceSectionSnapshot? I would like to make something that resembles a family tree. SwiftUI makes this relatively simple by using the built in List.
First we have to make our node object to represent heretical tree data.
import Foundation
#resultBuilder
struct NodeBuilder {
static func buildBlock<Value>(_ children: Node<Value>...) -> [Node<Value>] {
children
}
}
struct Node<Value>: Identifiable {
var value: Value
private(set) var children: [Node]?
var id = UUID()
mutating func add(child: Node) {
children?.append(child)
}
init(_ value: Value) {
self.value = value
}
init(_ value: Value, children: [Node]) {
self.value = value
}
init(_ value: Value, #NodeBuilder builder: () -> [Node]) {
self.value = value
self.children = builder()
}
var count: Int {
1 + (children?.reduce(0) { $0 + $1.count } ?? 0)
}
var recursiveChildren: [Node] {
return [self] + (children?.flatMap { $0.recursiveChildren } ?? [])
}
}
extension Node: Equatable where Value: Equatable { }
extension Node: Hashable where Value: Hashable { }
extension Node: Codable where Value: Codable { }
Next, and most importantly we configure the NSDiffableDataSourceSectionSnapshot using recursion. See applySnapshot and addChildren.
class ViewController: UIViewController {
private var collectionView: UICollectionView! = nil
var dataSource: UICollectionViewDiffableDataSource<String, Node<String>>! = nil
let root = Node("Terry") {
Node("Paul") {
Node("Sophie")
Node("Timmy")
Node("Sandra") {
Node("Aimee")
Node("Niki")
}
Node("Bob")
}
Node("Andrew") {
Node("John")
Node("Adam")
Node("Suzzie")
Node("Ricky"){
Node("Taylor")
Node("Megan")
Node("Arthur") {
Node("Fred")
Node("George")
Node("Giny") {
Node("Harry")
Node("Harold")
}
}
}
}
}
//MARK: - View
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
configureDataSource()
applySnapshot(animated: false)
}
private func configureCollectionView() {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
//MARK: - Data Source
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Node<String>> { (cell, indexPath, node) in
var content = cell.defaultContentConfiguration()
content.text = node.value
cell.contentConfiguration = content
cell.accessories = node.children == nil ? [] : [.outlineDisclosure()]
}
dataSource = UICollectionViewDiffableDataSource<String, Node<String>>(collectionView: collectionView) { (collectionView, indexPath, node) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: node)
}
}
func applySnapshot(animated: Bool) {
guard let children = root.children else { return }
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Node<String>>()
sectionSnapshot.append([root])
sectionSnapshot.append(children, to: root)
addChildren(of: root, to: &sectionSnapshot)
sectionSnapshot.expand(sectionSnapshot.items)
dataSource.apply(sectionSnapshot, to: root.value, animatingDifferences: animated)
}
func addChildren(of node: Node<String>, to sectionSnapshot: inout NSDiffableDataSourceSectionSnapshot<Node<String>>) {
guard let children = node.children else { return }
for subChild in children {
if let grandChildren = subChild.children {
sectionSnapshot.append(grandChildren, to: subChild)
addChildren(of: subChild, to: &sectionSnapshot)
}
}
}
}
If you would like multiple sections, that's easily done as well by modifying the applySnapshot function.
func applySnapshot(animated: Bool) {
guard let children = root.children else { return }
for child in children {
if let grandChildren = child.children {
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Node<String>>()
sectionSnapshot.append([child])
sectionSnapshot.append(grandChildren, to: child)
addChildren(of: child, to: &sectionSnapshot)
sectionSnapshot.expand(sectionSnapshot.items)
dataSource.apply(sectionSnapshot, to: child.value, animatingDifferences: animated)
}
}
}

How to get notifications of Firestore Database changes & request a get call for only the changed documents (added or updated)

So far I am able to add a snapshotlistener to the collection:
db.collection("products/country/class/grade1/test").order(by: "qId").addSnapshotListener { [self] (querySnapshot, error) in
//Handle Error:
if let error = error {
print("Error getting documents: \(error.localizedDescription)")
} else {
//No Documents Found:
guard let documents = querySnapshot?.documents else {
return
}
documents.compactMap { doc in
let value = doc.data()
print (value)
}
}
}
However, I would like it where a little badge appears showing that there were databases changes & when the user presses the update button it loads only the changed (added or updated) documents
class ChannelsViewController: UITableViewController {
private var channelReference: CollectionReference {
return database.collection("channels")
}
private var channels: [Channel] = []
override func viewDidLoad() {
super.viewDidLoad()
channelListener = channelReference.addSnapshotListener { [weak self]
querySnapshot, error in
guard let self = self else { return }
guard let snapshot = querySnapshot else {
print("Error listening for channel updates: \.
(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { change in
self.handleDocumentChange(change)
}
}
}
private func addChannelToTable(_ channel: Channel) {
if channels.contains(channel) {
return
}
channels.append(channel)
channels.sort()
guard let index = channels.firstIndex(of: channel) else {
return
}
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func updateChannelInTable(_ channel: Channel) {
guard let index = channels.firstIndex(of: channel) else {
return
}
channels[index] = channel
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func removeChannelFromTable(_ channel: Channel) {
guard let index = channels.firstIndex(of: channel) else {
return
}
channels.remove(at: index)
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func handleDocumentChange(_ change: DocumentChange) {
guard let channel = Channel(document: change.document) else {
return
}
switch change.type {
case .added:
addChannelToTable(channel)
case .modified:
updateChannelInTable(channel)
case .removed:
removeChannelFromTable(channel)
}
}
}
this is an example automatically update the tableView when add,update and delete on collection name "channel"
and the Channel as:
import FirebaseFirestore
struct Channel {
let id: String?
let name: String
init(name: String) {
id = nil
self.name = name
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let name = data["name"] as? String else {
return nil
}
id = document.documentID
self.name = name
}
}
// MARK: - DatabaseRepresentation
extension Channel: DatabaseRepresentation {
var representation: [String: Any] {
var rep = ["name": name]
if let id = id {
rep["id"] = id
}
return rep
}
}
// MARK: - Comparable
extension Channel: Comparable {
static func == (lhs: Channel, rhs: Channel) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: Channel, rhs: Channel) -> Bool {
return lhs.name < rhs.name
}
}

RxDatasource in RxSwift reload animation don't update data source

RxDatasource in RxSwift [RxTableViewSectionedAnimatedDataSource] Reload Animation don't update data source. What mistake I am doing? I am even unable to bind my action with button properly.
TableDataSource and Table editing commands
struct TableDataSource {
var header: String
var items: [Item]
var SectionViewModel: SectionViewModel
}
extension TableDataSource: AnimatableSectionModelType {
var identity: String {
return header
}
type alias Item = Data
init(original: TableDataSource, items: [Item]) {
self = original
self.items = items
self.sectionViewModel = original.sectionViewModel
}
}
enum TableViewEditingCommand {
case deleteSingle(IndexPath)
case clearAll(IndexPath)
case readAll(IndexPath)
}
struct SectionedTableViewState {
var sections: [TableDataSource]
init(sections: [TableDataSource]) {
self.sections = sections
}
func execute(command: TableViewEditingCommand) -> SectionedTableViewState {
var sections = self.sections
switch command {
// Delete single item from datasource
case .deleteSingle(let indexPath):
var items = sections[indexPath.section].items
items.remove(at: indexPath.row)
if items.count <= 0 {
sections.remove(at: indexPath.section)
} else {
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: items) }
// Clear all item from datasource with isUnread = false
case .clearAll(let indexPath):
sections.remove(at: indexPath.section)
// Mark all item as read in datasource with isUnread = true
case .readAll(let indexPath):
var items = sections[indexPath.section].items
items = items.map { var unread = $0
if $0.isUnRead == true { unreadData.isUnRead = false }
return unreadData
}
sections.remove(at: indexPath.section)
if sections.count > 0 {
let allReadItems = sections[indexPath.section].items + items
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: allReadItems)
}
}
return SectionedTableViewState(sections: sections)
}
}
This is my controller and its extensions
class ViewController: UIViewController, Storyboardable {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var closeButton: UIButton!
#IBOutlet weak var titleText: UILabel!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
let sectionHeight: CGFloat = 70
let dataSource = ViewController.dataSource()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
bindInitials()
bindDataSource()
bindDelete()
}
private func bindInitials() {
tableView.delegate = nil
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
registerNibs()
}
private func registerNibs() {
let headerNib = UINib.init(
nibName: TableViewSection.identifier,
bundle: nil)
tableView.register(
headerNib,
forHeaderFooterViewReuseIdentifier: TableViewSection.identifier)
}
}
extension ViewController: Bindable {
func bindViewModel() {
bindActions()
}
private func bindDataSource() {
bindDelete()
// tableView.dataSource = nil
// Observable.just(sections)
// .bind(to: tableView.rx.items(dataSource: dataSource))
// .disposed(by: disposeBag)
}
private func bindDelete() {
/// TODO: to check and update delete code to work properly to sink with clear all and mark all as read
guard let sections = self.viewModel?.getSections() else {
return
}
let deleteState = SectionedTableViewState(sections: sections)
let deleteCommand = tableView.rx.itemDeleted.asObservable()
.map(TableViewEditingCommand.deleteSingle)
tableView.dataSource = nil
Observable.of(deleteCommand)
.merge()
.scan(deleteState) {
(state: SectionedTableViewState,
command: TableViewEditingCommand) -> SectionedTableViewState in
return state.execute(command: command) }
.startWith(deleteState) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
private func bindActions() {
guard let openDetailsObserver = self.viewModel?.input.openDetailsObserver,
let closeObserver = self.viewModel?.input.closeObserver else {
return
}
viewModel.output.titleTextDriver
.drive(titleText.rx.text)
.disposed(by: disposeBag)
// viewModel.input.dataSourceObserver
// .mapObserver({ (result) -> [Data] in
// return result
// })
// .disposed(by: disposeBag)
/// Close button binding with closeObserver
closeButton.rx.tap
.bind(to: (closeObserver))
.disposed(by: disposeBag)
/// Tableview item selected binding with openDetailsObserver
tableView.rx.itemSelected
.map { indexPath in
return (self.dataSource[indexPath.section].items[indexPath.row])
}.subscribe(openDetailsObserver).disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDelegate {
static func dataSource() -> RxTableViewSectionedAnimatedDataSource<TableDataSource> {
return RxTableViewSectionedAnimatedDataSource(
animationConfiguration: AnimationConfiguration(insertAnimation: .fade,
reloadAnimation: .fade,
deleteAnimation: .fade),
configureCell: { (dataSource, table, idxPath, item) in
var cell = table.dequeueReusableCell(withIdentifier: TableViewCell.identifier) as? TableViewCell
let cellViewModel = TableCellViewModel(withItem: item)
cell?.setViewModel(to: cellViewModel)
return cell ?? UITableViewCell()
}, canEditRowAtIndexPath: { _, _ in return true })
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard var headerView = tableView.dequeueReusableHeaderFooterView(
withIdentifier: TableViewSection.identifier)
as? TableViewSection
else { return UITableViewHeaderFooterView() }
let viewModel = self.dataSource[section].sectionViewModel
headerView.setViewModel(to: viewModel)
headerView.dividerLine.isHidden = section == 0 ? true : false
headerView.section = section
let data = TableViewEditingCommand.clearAll(IndexPath(row: 0, section: section ?? 0))
// /// Section button binding with closeObserver
// headerView.sectionButton.rx.tap
// .map(verseNum -> TableViewEditingCommand in (TableViewEditingCommand.deleteSingle))
// .disposed(by: disposeBag)
headerView.sectionButtonTappedClosure = { [weak self] (section, buttonType) in
if buttonType == ButtonType.clearAll {
self?.showClearAllConfirmationAlert(section: section, buttonType: buttonType)
} else {
self?.editAction(section: section, buttonType: buttonType)
}
}
return headerView
}
func editAction(section: Int, buttonType: ButtonType) {
var sections = self.dataSource.sectionModels
let updateSection = (sections.count == 1 ? 0 : section)
switch buttonType {
/// Clear all
case .clearAll:
sections.remove(at: updateSection)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
/// Mark all as read
case .markAllAsRead:
if updateSection == 0 { sections = self.viewModel.getSections() }
var items = sections[updateSection].items
items = items.map { var unread = $0
if $0.isUnRead == true { unread.isUnRead = false }
return unread
}
sections.remove(at: updateSection)
let allReadItems = sections[updateSection].items + items
sections[updateSection] = TableDataSource(
original: sections[updateSection],
items: allReadItems)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func showClearAllConfirmationAlert(section: Int, buttonType: ButtonType) {
let alert = UIAlertController(title: "Clear All",
message: "Are you sure, you want to clear all Items?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
self.editAction(section: section, buttonType: buttonType)
case .cancel: break
case .destructive: break
default:break
}}))
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
})
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeight
}
}
View model for controller
class ViewModel {
private enum Constants {
static let titleText = "Test".localized
static let testHistoryHeaderText = "test".localized
static let unreadHeaderText = "Unread".localized
}
struct Input {
let dataSourceObserver: AnyObserver<[Data]>
let openDetailsObserver: AnyObserver<Data>
let closeObserver: AnyObserver<Void>
let sectionButtonTappedObserver: AnyObserver<IndexPath>
}
struct Output {
let titleTextDriver: Driver<String>
let dataSourceDriver: Driver<[Data]>
let viewComplete: Observable<DataCoordinator.Event>
}
let input: Input
let output: Output
private let dataSourceSubject = PublishSubject<[Data]>()
private let closeSubject = PublishSubject<Void>()
private let openDetailsSubject = BehaviorSubject<Data>(value:Data())
private let sectionButtonTappedSubject = PublishSubject<IndexPath>()
private let disposeBag = DisposeBag()
init(withRepository repository: Repository) {
input = Input(
dataSourceObserver: dataSourceSubject.asObserver(),
openDetailsObserver: openDetailsSubject.asObserver(),
closeObserver: closeSubject.asObserver(), sectionButtonTappedObserver: sectionButtonTappedSubject.asObserver()
)
let closeEventObservable = closeSubject.asObservable().map { _ in
return Coordinator.Event.goBack
}
let openDetailsEventObservable = openDetailsSubject.asObservable().map { _ in
return Coordinator.Event.goToDetails
}
let viewCompleteObservable = Observable.merge(closeEventObservable, openDetailsEventObservable)
let list = ViewModel.getData(repository: repository)
output = Output(
titleTextDriver: Observable.just(Constants.titleText).asDriver(onErrorJustReturn: Constants.titleText),
dataSourceDriver: Observable.just(list).asDriver(onErrorJustReturn: list),
viewComplete: viewCompleteObservable)
}
///TODO: To be updated as per response after API integration
static func getData(repository: Repository) -> [Data] {
return repository.getData()
}
func getSections() -> [TableDataSource] {
let List = ViewModel.getData(repository: Repository())
let unread = list.filter { $0.isUnRead == true }
let read = list.filter { $0.isUnRead == false }
let headerText = String(format:Constants.unreadHeaderText, unread.count)
let sections = [TableDataSource(
header: headerText,
items: unread,
sectionViewModel: SectionViewModel(
withHeader: headerText,
buttonType: ButtonType.markAllAsRead.rawValue,
items: unread)),
TableDataSource(
header: Constants.historyHeaderText,
items: read,
SectionViewModel: SectionViewModel(
withHeader: Constants.historyHeaderText,
buttonType: ButtonType.clearAll.rawValue,
items: read))]
return sections
}
}
i think your problem is with :
var identity: String {
return header
}
make a uuid and pass it to identity:
let id = UUID().uuidString
var identity: String {
return id
}

How I can correctly combine items in section with RxDataSource swift?

I need to combine chat message in section when items send in one minutes.
ViewModel
.....
.scan([MessageSectionModel]()) { sectionModels, messageItem in
var models = sectionModels
if let lastSectionModel = sectionModels.last {
switch lastSectionModel {
case .incomingSection(var items):
if messageItem.0.isIncoming {
items.append(messageItem.0)
models[models.count-1] = .incomingSection(items: items)
} else {
models.append(.outcomingSection(items: [messageItem.0]))
}
case .outcomingSection(var items):
if messageItem.0.isIncoming {
models.append(.incomingSection(items: [messageItem.0]))
} else {
items.append(messageItem.0)
models[models.count-1] = .outcomingSection(items: items)
}
}
return models
}
if messageItem.0.isIncoming {
models.append(.incomingSection(items: [messageItem.0]))
} else {
models.append(.outcomingSection(items: [messageItem.0]))
}
return models
}
.....
ViewController
....
#IBOutlet private weak var messagesTableView: UITableView!
private let disposeBag = DisposeBag()
private var dataSource: RxTableViewSectionedAnimatedDataSource<MessageSectionModel>!
private let messageHeaderReuseIdentifier = String(describing: MessageHeaderView.self)
private let messageFooterReuseIdentifier = String(describing: MessageFooterView.self)
dataSource = RxTableViewSectionedAnimatedDataSource<MessageSectionModel>(
animationConfiguration: .init(insertAnimation: .none, reloadAnimation: .none, deleteAnimation: .none),
configureCell: { dataSource, tableView, indexPath, item in
switch dataSource.sectionModels[indexPath.section] {
case .incomingSection:
guard let cell = tableView.dequeueReusableCell(
withIdentifier: R.reuseIdentifier.incomingMessageTableViewCell,
for: indexPath
) else {
return UITableViewCell()
}
let isFirst = indexPath.row == dataSource[indexPath.section].items.count - 1
cell.bind(
messageText: item.text,
isFirstInSection: isFirst
)
return cell
case .userSection:
guard let cell = tableView.dequeueReusableCell(
withIdentifier: R.reuseIdentifier.outcomingMessageTableViewCell,
for: indexPath
) else {
return UITableViewCell()
}
cell.bind(
messageText: item.text,
isFirstInSection: indexPath.row == dataSource[indexPath.section].items.count - 1
)
return cell
}
})
....
Message items
....
import Foundation
import RxDataSources
enum MessageSectionModel {
case incomingSection(items: [MessageSectionItem])
case outcomingSection(items: [MessageSectionItem])
var lastMessageDate: Date {
switch self {
case .incomingSection(let items):
return items.last?.sentDate ?? Date()
case .outcomingSection(let items):
return items.last?.sentDate ?? Date()
}
}
}
struct MessageSectionItem {
let userId: String
let id: String = UUID().uuidString
let text: String
let sentDate: Date
let isIncoming: Bool
}
extension MessageSectionItem: IdentifiableType {
var identity : String {
return id
}
}
extension MessageSectionItem: Equatable {
static func == (lhs: MessageSectionItem, rhs: MessageSectionItem) -> Bool {
return lhs.identity == rhs.identity
}
}
extension MessageSectionModel: AnimatableSectionModelType {
init(original: MessageSectionModel, items: [MessageSectionItem]) {
switch original {
case .incomingSection(let items):
self = .incomingSection(items: items)
case .outcomingSection(let items):
self = .outcomingSection(items: items)
}
}
typealias Item = MessageSectionItem
var items: [MessageSectionItem] {
switch self {
case .incomingSection(let items):
return items.map { $0 }
case .outcomingSection(let items):
return items.map { $0 }
}
}
var identity: Date {
return lastMessageDate
}
}
....
My table view is rotated because i fetch messages is reverted. I understand it`s my mistake in scan, because when i comments this code, my cells sorted in correct way, but not combined in sections.
if let lastSectionModel = sectionModels.last {
switch lastSectionModel {
case .incomingSection(var items):
if messageItem.0.isIncoming {
items.append(messageItem.0)
models[models.count-1] = .incomingSection(items: items)
} else {
models.append(.outcomingSection(items: [messageItem.0]))
}
case .outcomingSection(var items):
if messageItem.0.isIncoming {
models.append(.incomingSection(items: [messageItem.0]))
} else {
items.append(messageItem.0)
models[models.count-1] = .outcomingSection(items: items)
}
}
return models
I think you are trying to do too much at one time, and in the wrong order. Break the job up into smaller jobs that can each be easily tested/verified... Also, first group your messages by time, then put them in your sections. I ended up with this:
struct MessageItem {
let userId: String
let id: String = UUID().uuidString
let text: String
let sentDate: Date
let isIncoming: Bool
}
struct MessageGroup {
let userId: String
var text: String {
return parts.map { $0.text }.joined(separator: "\n")
}
let isIncoming: Bool
struct Part {
let id: String
let text: String
let sentDate: Date
init(_ messageSectionItem: MessageItem) {
id = messageSectionItem.id
text = messageSectionItem.text
sentDate = messageSectionItem.sentDate
}
}
var parts: [Part]
init(from item: MessageItem) {
userId = item.userId
isIncoming = item.isIncoming
parts = [Part(item)]
}
}
enum MessageSectionModel {
case incomingSection(items: [MessageGroup])
case outcomingSection(items: [MessageGroup])
}
extension ObservableType where Element == MessageItem {
func convertedToSectionModels() -> Observable<[MessageSectionModel]> {
return
scan(into: ([MessageGroup](), MessageGroup?.none), accumulator: groupByTime(messages:item:))
.map(appendingLastGroup(messages:group:))
.map(groupedByIncoming(messages:))
.map(convertedToSectionModels(messages:))
}
}
func groupByTime(messages: inout ([MessageGroup], MessageGroup?), item: MessageItem) {
if let group = messages.1 {
let lastPart = group.parts.last!
if lastPart.sentDate.timeIntervalSince(item.sentDate) > -60 && group.userId == item.userId {
messages.1!.parts.append(MessageGroup.Part(item))
}
else {
messages.0.append(group)
messages.1 = MessageGroup(from: item)
}
}
else {
messages.1 = MessageGroup(from: item)
}
}
func appendingLastGroup(messages: [MessageGroup], group: MessageGroup?) -> [MessageGroup] {
guard let group = group else { return messages }
return messages + [group]
}
func groupedByIncoming(messages: [MessageGroup]) -> [[MessageGroup]] {
return messages.reduce([[MessageGroup]]()) { result, message in
guard let last = result.last else {
return [[message]]
}
if last.last!.isIncoming == message.isIncoming {
return Array(result.dropLast()) + [last + [message]]
}
else {
return result + [[message]]
}
}
}
func convertedToSectionModels(messages: [[MessageGroup]]) -> [MessageSectionModel] {
messages.map { messages in
if messages.first!.isIncoming {
return .incomingSection(items: messages)
}
else {
return .outcomingSection(items: messages)
}
}
}