swiftrx-swiftrxdatasources

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

Solution

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