Home > Net >  SwiftUI List with custom data structure with nested for each loops
SwiftUI List with custom data structure with nested for each loops

Time:10-31

I have been able to successfully implement infinite scroll in my swiftui application but it doesn't behave correctly and it has to do with the list it iterates over. For example, I get a list of product orders from a graphql endpoint and its stored as

@Published var groupedPurchaseOrders = [[PurchaseOrderFragment]]()

The reason it is nested in an array is because I process this data into groups separated by order date.

func groupPurchaseOrders() {
        
    let dateGroups = Dictionary(grouping: purchaseOrders) { (element) -> Date in
        return element.orderForDate.reduceToMonthDayYear()
    }
      
    let sortedKeys = dateGroups.keys.sorted { (Date1, Date2) -> Bool in
        Date1 > Date2
    }
        
    sortedKeys.forEach { key in
        let values = dateGroups[key]
        groupedPurchaseOrders.append(values)
    }
}

This way I can create a list that has headers with a date (which I derive from the first item in the list) then the purchase order.

ForEach(posModel.groupedPurchaseOrders, id: \.self) { group in         
    Section(header: <Derived date from first item>) {
        ForEach(group, id: \.self) { order in
            NavigationLink(destination: PurchaseOrderDetailView(purchaseOrder: order)) {
                PurchaseOrderListItemOnlyView(purchaseOrder: order)
                    .padding(.vertical, 4)
                    .onAppear {
                         let purchaseOrders = posModel.groupedPurchaseOrders.flatMap { $0 
                         if purchaseOrders.last == order {
                              self.loadOrders()
                         }
            }
        }
    }
}

Date
    Order
    Order
Date
    Order
    ...

The Problem:

The list loads the first 20 items fine, but when it gets to the bottom of the list and loads more (in this instance, 5 more items) it adds the 5, the list has the original 20 plus 25. But when I create a normal list without the grouping it works as intended and just appends the 5 to the end.

As a beginner with ios dev, Im not sure if I can create a data structure like [Date, [PurchaseOrderFragment]] as apposed to [PurchaseOrderFragment]. I've have tried it but just can't seem to get the syntax and implementation right... Any ideas on how to implement the list with the grouping? where it appends the records correctly?

EDIT:

I have tried

struct GroupedOrder: Hashable {
    var date: Date
    var orders: [PurchaseOrderFragment]
    
    public func hash(into hasher: inout Hasher) {
        hasher.combine(date)
        hasher.combine(orders)
    }
}

and it still duplicates the list when it loads 5 more

CodePudding user response:

I wrote a playground based on your code. What it is missing is the concept of "loading 5 more" that your code doesn't show. But the grouped dates is simply formed from the same Dictionary(grouping: purchaseOrders) { $0.date } as your original code. Theoretically you could use any list of purchase orders with that. `

import UIKit
import SwiftUI
import PlaygroundSupport

let mockPurchaseOrders = """
[
    { "date" : "1/1/2020", "title": "The First Purchase Order" },
    { "date" : "1/1/2020", "title": "The Second Purchase Order" },
    { "date" : "1/1/2020", "title": "The Third Purchase Order" },
    { "date" : "3/2/2020", "title": "The Fourth Purchase Order" },
    { "date" : "3/2/2020", "title": "The Fifth Purchase Order" },
    { "date" : "5/1/2020", "title": "The Sixth Purchase Order" },
    { "date" : "7/1/2020", "title": "The Seventh Purchase Order" },
    { "date" : "7/1/2020", "title": "The Eighth Purchase Order" },
    { "date" : "7/1/2020", "title": "The Ninth Purchase Order" }
]
"""

struct PurchaseOrder : Decodable, Hashable {
    static var dateFormatter : DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .none
        return formatter;
    }()

    let title : String
    let date : Date

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Self.CodingKeys)
        title = try container.decode(String.self, forKey: CodingKeys.title)

        let dateString = try container.decode(String.self, forKey: .date)
        date = PurchaseOrder.dateFormatter.date(from: dateString)!

    }

    enum CodingKeys: String, CodingKey {
        case title
        case date
    }
}

typealias GroupedDates = [Date : [PurchaseOrder]]

struct NestedListView : View {
    let groupedOrders : GroupedDates
    var orderDates : [Date] { Array<Date>(groupedOrders.keys).sorted() }
    static var dateFormatter : DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .none
        return formatter;
    }()

    init(orders : GroupedDates) {
        groupedOrders = orders
    }

    var body : some View {
        ForEach(orderDates, id: \.self) { date in
            Section(header: Text(NestedListView.dateFormatter.string(from: date))) {
                ForEach(groupedOrders[date]!, id: \.self) { order in
                    NavigationLink(destination: Text("Hello!")) {
                        Text(order.title)
                    }
                }
            }
        }
        .frame(minWidth: 320, idealWidth: 320, maxWidth: 320, minHeight: 480)
    }
}


let jsonDecoder = JSONDecoder()
let purchaseOrders = try jsonDecoder.decode([PurchaseOrder].self, from:  mockPurchaseOrders.data(using: .utf8)!)
let groupedOrders = Dictionary(grouping: purchaseOrders) { $0.date }

let hostingController = UIHostingController(rootView: NestedListView(orders: groupedOrders))
PlaygroundSupport.PlaygroundPage.current.liveView = hostingController
  • Related