I'm implementing infinite scrolling with Swift Charts where additional historical data loads when scrolling near the beginning of the dataset. However, when new data is loaded, the chart's scroll position jumps unexpectedly.
Current behavior:
Initially loads 10 data points, displaying the latest 5
When scrolling backwards with only 3 points remaining off-screen, triggers loading of 10 more historical points
After loading, the scroll position jumps to the 3rd position of the new dataset instead of maintaining the current view
Expected behavior:
Scroll position should remain stable when new data is loaded
User's current view should not change during data loading
Here's my implementation logic using some mock data:
import SwiftUI
import Charts
struct DataPoint: Identifiable {
let id = UUID()
let date: Date
let value: Double
}
class ChartViewModel: ObservableObject {
@Published var dataPoints: [DataPoint] = []
private var isLoading = false
init() {
loadMoreData()
}
func loadMoreData() {
guard !isLoading else { return }
isLoading = true
let newData = self.generateDataPoints(
endDate: self.dataPoints.first?.date ?? Date(),
count: 10
)
self.dataPoints.insert(contentsOf: newData, at: 0)
self.isLoading = false
print("\(dataPoints.count) data points.")
}
private func generateDataPoints(endDate: Date, count: Int) -> [DataPoint] {
var points: [DataPoint] = []
let calendar = Calendar.current
for i in 0..<count {
let date = calendar.date(
byAdding: .day,
value: -i,
to: endDate
) ?? endDate
let value = Double.random(in: 0...100)
points.append(DataPoint(date: date, value: value))
}
return points.sorted { $0.date < $1.date }
}
}
struct ScrollableChart: View {
@StateObject private var viewModel = ChartViewModel()
@State private var scrollPosition: Date
@State private var scrollDebounceTask: Task<Void, Never>?
init() {
self.scrollPosition = .now.addingTimeInterval(-4*24*3600)
}
var body: some View {
Chart(viewModel.dataPoints) { point in
BarMark(
x: .value("Time", point.date, unit: .day),
y: .value("Value", point.value)
)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 5 * 24 * 3600)
.chartScrollPosition(x: $scrollPosition)
.chartXScale(domain: .automatic(includesZero: false))
.frame(height: 300)
.onChange(of: scrollPosition) { oldPosition, newPosition in
scrollDebounceTask?.cancel()
scrollDebounceTask = Task {
try? await Task.sleep(for: .milliseconds(300))
if !Task.isCancelled {
checkAndLoadMoreData(currentPosition: newPosition)
}
}
}
}
private func checkAndLoadMoreData(currentPosition: Date?) {
guard let currentPosition,
let earliestDataPoint = viewModel.dataPoints.first?.date else {
return
}
let timeInterval = currentPosition.timeIntervalSince(earliestDataPoint)
if timeInterval <= 3 * 24 * 3600 {
viewModel.loadMoreData()
}
}
}
I attempted to compensate for this jump by adding:
scrollPosition = scrollPosition.addingTimeInterval(10 * 24 * 3600)
after viewModel.loadMoreData(). However, this caused the chart to jump in the opposite direction by 10 days, rather than maintaining the current position.
What's the problem with my code and how to fix it?
Post
Replies
Boosts
Views
Activity
I'm trying to visualize some data using AreaMark of Swift Charts, however, different data structures seem to affect the result. Really confused here.
The following two example structs hold the same data, but in different ways:
import SwiftUI
import Charts
struct Food: Identifiable {
let name: String
let sales: Int
let day: Int
let id = UUID()
init(name: String, sales: Int, day: Int) {
self.name = name
self.sales = sales
self.day = day
}
}
struct Sales: Identifiable {
let day: Int
let burger: Int
let salad: Int
let steak: Int
let id = UUID()
var total: Int {
burger + salad + steak
}
init(day: Int, burger: Int, salad: Int, steak: Int) {
self.day = day
self.burger = burger
self.salad = salad
self.steak = steak
}
}
Now if I populate data and use Swift Charts to plot the data, the first struct works perfectly with all types of charts. However, the second struct, while works well with BarMark and PointMark, doesn't seem to work with AreaMark.
To reproduce, change "AreaMark" to "BarMark" or "PointMark" in the following code:
struct ExperimentView: View {
let cheeseburgerSalesByItem: [Food] = [
.init(name: "Burger", sales: 29, day: 1),
.init(name: "Salad", sales: 35, day: 1),
.init(name: "Steak", sales: 30, day: 1),
.init(name: "Burger", sales: 32, day: 2),
.init(name: "Salad", sales: 38, day: 2),
.init(name: "Steak", sales: 42, day: 2),
.init(name: "Burger", sales: 35, day: 3),
.init(name: "Salad", sales: 29, day: 3),
.init(name: "Steak", sales: 41, day: 3),
.init(name: "Burger", sales: 29, day: 4),
.init(name: "Salad", sales: 38, day: 4),
.init(name: "Steak", sales: 39, day: 4),
.init(name: "Burger", sales: 43, day: 5),
.init(name: "Salad", sales: 42, day: 5),
.init(name: "Steak", sales: 30, day: 5),
.init(name: "Burger", sales: 45, day: 6),
.init(name: "Salad", sales: 39, day: 6),
.init(name: "Steak", sales: 31, day: 6),
.init(name: "Burger", sales: 37, day: 7),
.init(name: "Salad", sales: 35, day: 7),
.init(name: "Steak", sales: 30, day: 7),
]
let cheeseburgerSalesByDay: [Sales] = [
.init(day: 1, burger: 29, salad: 35, steak: 30),
.init(day: 2, burger: 32, salad: 38, steak: 42),
.init(day: 3, burger: 35, salad: 29, steak: 41),
.init(day: 4, burger: 29, salad: 38, steak: 39),
.init(day: 5, burger: 43, salad: 42, steak: 30),
.init(day: 6, burger: 45, salad: 39, steak: 31),
.init(day: 7, burger: 37, salad: 35, steak: 30)
]
var body: some View {
VStack {
Chart(cheeseburgerSalesByItem) { sale in
AreaMark(
x: .value("Day", sale.day),
y: .value("Sales", sale.sales)
)
.foregroundStyle(by: .value("Food Item", sale.name))
}
.chartXScale(domain: 1...7)
.padding()
Spacer()
Chart(cheeseburgerSalesByDay) { sale in
AreaMark(
x: .value("Day", sale.day),
y: .value("Burger", sale.burger)
)
.foregroundStyle(.brown)
AreaMark(
x: .value("Day", sale.day),
y: .value("Salad", sale.salad)
)
.foregroundStyle(.green)
AreaMark(
x: .value("Day", sale.day),
y: .value("Steak", sale.steak)
)
.foregroundStyle(.red)
}
.padding()
}
}
}
If two of the three AreaMarks are commented out, the left one would work. Just more than one AreaMark plots don't work for this struct.
So what is the problem here? What to do if I want to use the second structure and plot a AreaMark chart?