Symptom: chart (bar chart at least) doesn't snap to beginning of a bar mark when specified with chartScrollTargetBehavior. It could stop anywhere in a bar mark. In the following reproduction, I'm using a bar chart with x values being Date objects, and y values being Double. I attached a ProbeBehavior which runs ValueAlignedChartScrollTargetBehavior(matching: DateComponents(hour: 0), majorAlignment: .page)'s updateTarget, but with some debug info printed out. So essentially, I'm running .chartScrollTargetBehavior(.valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page)).
Minimal reproduction
import SwiftUI
import Charts
@main struct ChartSnapApp: App { var body: some Scene { WindowGroup { TestChart() } } }
private struct TestChart: View {
let calendar: Calendar = .current
@State private var scrollX = Date.now
var body: some View {
let half = 42
let today = calendar.startOfDay(for: .now)
let start = calendar.date(byAdding: .day, value: -half, to: today)!
let end = calendar.date(byAdding: .day, value: half, to: today)!
VStack {
Text("Scroll: \(scrollX.formatted(.iso8601)) | secondsFromMidnight: \(scrollX.timeIntervalSince(calendar.startOfDay(for: scrollX)), specifier: "%.3f")")
.font(.caption).accessibilityIdentifier("scrollReadout")
Chart {
ForEach(0 ..< half * 2, id: \.self) { offset in
let date = calendar.date(byAdding: .day, value: offset, to: start)!
BarMark(x: .value("Date", date, unit: .day), y: .value("Value", 1.0))
}
}
.chartXScale(domain: start ... end)
.chartXVisibleDomain(length: 7 * 24 * 3600)
.chartYScale(domain: 0 ... 3)
.chartScrollableAxes(.horizontal)
.chartScrollTargetBehavior(ProbeBehavior())
.chartScrollPosition(x: $scrollX)
.onChange(of: scrollX) { _, new in print("SCROLL \(new.formatted(.iso8601)) secondsFromMidnight=\(new.timeIntervalSince(calendar.startOfDay(for: new)))") }
.environment(\.calendar, calendar)
.padding(.all, 12)
.accessibilityIdentifier("chart")
}
}
}
private struct ProbeBehavior: ChartScrollTargetBehavior {
func updateTarget(_ target: inout ScrollTarget, context: ChartScrollTargetBehaviorContext) {
let before = target.rect
let orig = context.originalTarget.rect
let beforeDate: Date? = context.chartProxy.value(atX: before.minX)
let origDate: Date? = context.chartProxy.value(atX: orig.minX)
ValueAlignedChartScrollTargetBehavior(matching: DateComponents(hour: 0), majorAlignment: .page).updateTarget(&target, context: context)
let afterDate: Date? = context.chartProxy.value(atX: target.rect.minX)
let domain = context.chartProxy.xDomain(dataType: Date.self)
let zero = context.chartProxy.value(atX: 0, as: Date.self)
let todayPosition = context.chartProxy.position(forX: Calendar.current.startOfDay(for: .now))
let line = "PROBE calendar=\(Calendar.current.identifier) timezone=\(Calendar.current.timeZone.identifier) plotSize=\(context.chartProxy.plotSize) xDomain=\(domain) valueAtZero=\(String(describing: zero)) todayPosition=\(String(describing: todayPosition)) velocity=\(context.velocity) originalRect=\(orig) beforeRect=\(before) afterRect=\(target.rect) originalDate=\(String(describing: origDate)) beforeDate=\(String(describing: beforeDate)) afterDate=\(String(describing: afterDate))\n"
print(line)
let url = URL.documentsDirectory.appending(path: "probe.log")
if !FileManager.default.fileExists(atPath: url.path) { FileManager.default.createFile(atPath: url.path, contents: nil) }
if let handle = try? FileHandle(forWritingTo: url) {
handle.seekToEndOfFile()
handle.write(Data(line.utf8))
try? handle.close()
}
}
}
Comparing video, left iOS 26.5 simulator, right iOS 27.0 simulator
https://jumpshare.com/s/SVdDV27qHlXdbxMp8mWA