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