Search code examples
swiftuitableviewcore-datagrouping

Group Array or Dictionary to show sections to UITableView in Swift


I have this implementation:

Invoice - Hold info about Invoices

import Foundation

class Invoice {
    
    var content: String?
    var createdAt: Date?
    var dateA: Date?
    var dateB: Date?
    var employee: String?
    var expenseCategory: String?
    var from: Date?
    var id: String?
    var ncf: String?
    var ncfA: String?
    var ncfB: String?
    var numeration: Int64
    var paymentMethod: String?
    var rejectReason: String?
    var sessionId: String?
    var status: String?
    var subtotal: Double
    var sync: Bool
    var taxExempt: Bool
    var taxType: String?
    var taxValue: Double
    var to: Date?
    var total: Double
    var totalInvoice: Int64
    var typeA: String?
    var typeB: String?
    var updatedAt: Date?
    var user: String?
    var toCompany: Company?
    var toDocument: [Document]?
    var storeId: String?
    var incomeType: String?
    var expenseType: String?
    var toOtherCompany: Company?
    var invoiceDate: Date?
    var otherTax: Double
    var companyId: String?
    
    
    init(_content: String?, _storeId: String?, _createdAt: Date?, _dateA: Date?, _dateB: Date?, _employee: String?, _expenseCategory: String?,
        _from: Date?, _id: String?, _ncf: String?, _ncfA: String?, _ncfB: String?, _numeration: Int64, _paymentMethod: String?,
        _rejectReason: String?, _sessionId: String?, _status: String?, _subtotal: Double, _sync: Bool, _taxExempt: Bool, _taxType: String?,
        _taxValue: Double, _to: Date?, _total: Double, _totalInvoice: Int64, _typeA: String?, _typeB: String?, _updatedAt: Date?,
        _user: String?, _incomeType: String?, _expenseType: String?, _invoiceDate: Date?,  _otherTax: Double, _companyId: String?, _toCompany: Company? = nil, _toDocument: [Document]? = nil,
        _toOtherCompany: Company? = nil) {
        
        self.content = _content
        self.createdAt = _createdAt
        self.dateA = _dateA
        self.dateB = _dateB
        self.employee = _employee
        self.expenseCategory = _expenseCategory
        self.from = _from
        self.id = _id
        self.ncf = _ncf
        self.ncfA = _ncfA
        self.ncfB = _ncfB
        self.numeration = _numeration
        self.paymentMethod = _paymentMethod
        self.rejectReason = _rejectReason
        self.sessionId = _sessionId
        self.status = _status
        self.subtotal = _subtotal
        self.sync = _sync
        self.taxExempt = _taxExempt
        self.taxType = _taxType
        self.taxValue = _taxValue
        self.to = _to
        self.total = _total
        self.totalInvoice = _totalInvoice
        self.typeA = _typeA
        self.typeB = _typeB
        self.updatedAt = _updatedAt
        self.user = _user
        self.toCompany = _toCompany
        self.toDocument = _toDocument
        self.storeId = _storeId
        self.incomeType = _incomeType
        self.expenseType = _expenseType
        self.toOtherCompany = _toOtherCompany
        self.invoiceDate = _invoiceDate
        self.otherTax = _otherTax
        self.companyId = _companyId
    }
    
    // Get the descriptive status
    func getDescriptiveStatus() -> String {
        
        switch self.status {
            case InvoiceStatus.waiting.toString():
                return InvoiceStatus.waiting.description.uppercased()
            case InvoiceStatus.processing.toString():
                return InvoiceStatus.processing.description.uppercased()
            case InvoiceStatus.processed.toString():
                return InvoiceStatus.processed.description.uppercased()
            case InvoiceStatus.pending_rejection.toString():
                return InvoiceStatus.pending_rejection.description.uppercased()
            case InvoiceStatus.rejected.toString():
                return InvoiceStatus.rejected.description.uppercased()
            case InvoiceStatus.posted.toString():
                return InvoiceStatus.posted.description.uppercased()
            case InvoiceStatus.reviewing.toString():
                return InvoiceStatus.reviewing.description.uppercased()
            case InvoiceStatus.pending_upload.toString():
                return InvoiceStatus.pending_upload.description.uppercased()
            default:
                return "UNKNOW STATUS"
        }
    }
    
    // Get status (based in enum)
    func getStatus() -> InvoiceStatus? {
        
        switch self.status {
            case InvoiceStatus.waiting.toString():
                return InvoiceStatus.waiting
            case InvoiceStatus.processing.toString():
                return InvoiceStatus.processing
            case InvoiceStatus.processed.toString():
                return InvoiceStatus.processed
            case InvoiceStatus.pending_rejection.toString():
                return InvoiceStatus.pending_rejection
            case InvoiceStatus.rejected.toString():
                return InvoiceStatus.rejected
            case InvoiceStatus.posted.toString():
                return InvoiceStatus.posted
            case InvoiceStatus.reviewing.toString():
                return InvoiceStatus.reviewing
            case InvoiceStatus.pending_upload.toString():
                return InvoiceStatus.pending_upload
            default:
                return nil
        }
    }
}

InvoiceGroup - Hold info about invoices grouped by header

import Foundation

struct InvoiceGroup {
    var header: String
    var invoices: [Invoice] = []
}

This function group all invoices by their relative date string using https://github.com/malcommac/SwiftDate framework

func groupInvoicesByDate(Invoices invoices: [Invoice]) -> [InvoiceGroup] {
        
    let grouped = Dictionary(grouping: invoices) {
        $0.createdAt?.toRelative()
    }
        
    let invoiceGroups = grouped.map {
        InvoiceGroup(header: $0.key!, invoices: $0.value)
    }
        
    return invoiceGroups
}

To display headers and cells I do this:

extension HistoryViewController : UITableViewDataSource, UITableViewDelegate
{
    
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return self.invoicesGroups[section].header
    }
    
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.invoicesGroups.count
    }
    
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.invoicesGroups[section].invoices.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let currentInvoice = self.invoicesGroups[indexPath.section].invoices[indexPath.row]
        
        switch currentInvoice.getStatus() {
            case .posted:
                let cell = (tableView.dequeueReusableCell(withIdentifier: "HistoryItem", for: indexPath) as? HistoryViewCell)!
                cell.data(item: currentInvoice, page: self, hidden: !self.isTab0(), selected: self.invoiceSelected)
                cell.setCallbackListener(callback: self)
                return cell
            case .waiting:
                let cell = (tableView.dequeueReusableCell(withIdentifier: "HistoryItem", for: indexPath) as? HistoryViewCell)!
                cell.data(item: currentInvoice, page: self, hidden: !self.isTab0(), selected: self.invoiceSelected)
                cell.setCallbackListener(callback: self)
                return cell
            case .rejected, .pending_rejection:
                let cell = (tableView.dequeueReusableCell(withIdentifier: "HistoryItemReturn", for: indexPath) as? HistoryItemReturn)!
                cell.data(item: currentInvoice, page: self)
                return cell
            case .pending_upload:
                let cell = (tableView.dequeueReusableCell(withIdentifier: "HistoryItem", for: indexPath) as? HistoryViewCell)!
                cell.data(item: currentInvoice, page: self)
                return cell
        default:
            let cell = (tableView.dequeueReusableCell(withIdentifier: "HistoryItem", for: indexPath) as? HistoryViewCell)!
            cell.data(item: currentInvoice, page: self)
            return cell
        }
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        let currentInvoice = self.invoicesGroups[indexPath.section].invoices[indexPath.row]
        
        switch currentInvoice.getStatus() {
            case .posted:
                // HistoryItem cell
                return 95
            case .waiting:
                // HistoryItem cell
                return 75
            case .rejected:
                // HistoryItemReturn cell
                return 110
            case .pending_upload:
                return 75
        default:
            return 100
        }
    }
}

When I get the invoices from CoreData, I take all of them ordered descending by the createdAt field

func histories(sessionId: String, types: [InvoiceType], status : [InvoiceStatus], companyId: String, sortDate: String = "createdAt") -> [Invoice] {
       
        var invoiceTypeList: [String] = []
        var invoiceStatusList: [String] = []
        
        for invTp in types {
            invoiceTypeList.append(invTp.toString())
        }
        
        for invSt in status {
            invoiceStatusList.append(invSt.toString())
        }
        
        var predicates: [NSPredicate] = []
        
        let predicate1 = NSPredicate(format: "status IN %@", invoiceStatusList)
        predicates.append(predicate1)
        
        if !types.isEmpty {
            let predicate2 = NSPredicate(format: "typeA IN %@", invoiceTypeList)
            predicates.append(predicate2)
        }
        
        let predicate3 = NSPredicate(format: "sessionId = %@", sessionId as CVarArg)
        predicates.append(predicate3)
        
        let predicate4 = NSPredicate(format: "companyId = %@", companyId as CVarArg)
        predicates.append(predicate4)
        
        let compoundPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
        let sortDescriptor = NSSortDescriptor(key: sortDate, ascending: false)
        let arrays = _invoiceDataRepository.getAll(predicate: compoundPredicate, sort: [sortDescriptor])
        
        return arrays
    }

But the header is displayed random in TableView with all their Invoices, for example:

{
    "Today": [
        {
          "id": 3,
          "ncf": "A1",
          "status": "posted"
        }
    ],
    "Yesterday": [
        {
          "id": 2,
          "ncf": "B1",
          "status": "posted"
        }
    ],
    "3 days ago": [
        {
          "id": 1,
          "ncf": "C3",
          "status": "posted"
        }
    ]
}

If I reload the TableView, order change, for example:

{
    "Today": [
        {
          "id": 3,
          "ncf": "A1",
          "status": "posted"
        }
    ],
    "3 days ago": [
        {
          "id": 1,
          "ncf": "C3",
          "status": "posted"
        }
    ],
    "Yesterday": [
        {
          "id": 2,
          "ncf": "B1",
          "status": "posted"
        }
    ]
}

Or

{
    "3 days ago": [
        {
          "id": 1,
          "ncf": "C3",
          "status": "posted"
        }
    ],
    "Today": [
        {
          "id": 3,
          "ncf": "A1",
          "status": "posted"
        }
    ],
    "Yesterday": [
        {
          "id": 2,
          "ncf": "B1",
          "status": "posted"
        }
    ]
}

I don't know why this is happening. I need the headers ordered descending by the relative date string off their elements.


Solution

  • You wrote:

    let grouped = Dictionary(grouping: invoices) {
        $0.createdAt?.toRelative()
    }
        
    let invoiceGroups = grouped.map {
        InvoiceGroup(header: $0.key!, invoices: $0.value)
    }
    

    Let's change it a little to debug:

    let invoiceGroups = grouped.map {
        print("Adding for key: \($0.key)")
        return InvoiceGroup(header: $0.key!, invoices: $0.value)
    }
    

    The issue is that a Dictionary isn't ordered, it's a Key-Value access, not Index-Value access. So there is no guarantee that the first key-value to be mapped will be the older date, or the latest one, and the following ones will keep that order.

    Instead, let's help you sort them. Sicne toRelative() will create a String, and "Yesterday" and "3 days ago" are "hard to compare", just let keep the dates first:

    Let's use dateTruncated(at: [.year,.month,.day]) for instance to make all the dates even with different hours in the same group. Or you could use dateAtStartOf(.day). I didn't digged into the library, but indeed local & timezone could causes issues, so to check on your end.

    let grouped = Dictionary(grouping: invoices) {
        $0.createdAt?.dateTruncated(at: [.year,.month,.day])
    }
    

    Then, let's sort it into tuples=

    let sortedTuples = grouped.sorted(by: { $0.key < $1.key }
    

    And then, we can just map the tuples into you custom struct:

    let invoiceGroups = sortedTuples.map {
        InvoiceGroupe(header: $0.key.toRelative, invoices: $0.values)
    }