Gene Bogdanovich
Gene Bogdanovich

Reputation: 1014

Swift Charts: RuleMark annotation is invisible when chartScrollableAxes are specified

enter image description here

I’m working on a project that displays the number of steps users have taken in the last 15 days on a chart. I’ve implemented the chart selection behavior introduced in iOS 17, which displays a RuleMark with an annotation for the selected value. However, when I enable a scrollable x-axis, the annotation becomes invisible. See the attached GIF image.

Chart(stepCountRecords) { record in
    BarMark(
        x: .value("Date", record.date, unit: .day),
        y: .value("Count", record.steps)
     )
            
    if let selectedDate {
        RuleMark(x: .value("Selected", selectedDate, unit: .day))
            .foregroundStyle(Color(.secondarySystemFill).opacity(0.3))
            .annotation(position: .top, overflowResolution: .init(x: .fit(to: .chart), y: .disabled)) {
                Text(selectedDate.formatted())
                    .padding()
                    .background(Color(.secondarySystemFill))
                    .clipShape(RoundedRectangle(cornerRadius: 10))
            }
    }
}
/*
 FIXME: Uncomment the following line to trigger the issue in question.
 .chartScrollableAxes(.horizontal)
 */
.chartXSelection(value: $selectedDate)

Is this intentional? Or I’m doing something wrong? It’s worth noting that Apple’s Health app supports scrolling and annotation at the same time.

Here’s a small sample project for your reference.

Upvotes: 0

Views: 128

Answers (1)

Sweeper
Sweeper

Reputation: 271410

The problem seems to be that the annotation is shown above the bars, which is clipped by some scroll view that allows scrolling.

You can enable overflowResolution on the y axis,

.annotation(
    position: .top, 
    overflowResolution: .init(
        x: .fit(to: .chart), 
        y: .fit // <---
    )
) { ... }

The annotation will now be placed at the top of the chart, overlaying the bars.

If you don't want the top of the bars to be hidden by the annotation, you can add some top padding instead.

.chartPlotStyle {
    $0.padding(.top, 100)
}

Using chartXSelection with a scrollable chart will often cause the selection gesture to be prioritised over the scrolling gesture. Consider using a SpatialTapGesture to change the selection,

.chartGesture { chartProxy in
    SpatialTapGesture().onEnded { value in
        selectedDate = chartProxy.value(atX: value.location.x, as: Date.self)
    }
}

Here is a complete example:

let data = (0..<30).map {
    Point(x: Date().addingTimeInterval(86400 * Double($0)), y: .random(in: 1...10))
}

struct ContentView: View {
    
    @State var selectedDate: Date?
    
    var body: some View {
        Chart(data) { record in
            BarMark(
                x: .value("Date", record.x, unit: .day),
                y: .value("Count", record.y)
             )
                    
            if let selectedDate {
                RuleMark(x: .value("Selected", selectedDate, unit: .day))
                    .foregroundStyle(Color(.secondarySystemFill).opacity(0.3))
                    .annotation(position: .top, overflowResolution: .init(x: .fit(to: .chart), y: .disabled)) {
                        Text(selectedDate.formatted())
                            .padding()
                            .background(Color(.secondarySystemFill))
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                    }
            }
        }
        .chartPlotStyle {
            $0.padding(.top, 100)
        }
        .chartGesture { chartProxy in
            SpatialTapGesture().onEnded { value in
                selectedDate = chartProxy.value(atX: value.location.x, as: Date.self)
            }
        }
        .chartScrollableAxes(.horizontal)
        .padding(.horizontal, 20)
        .padding(.vertical, 100)
    }
}

struct Point: Identifiable {
    let id = UUID()
    let x: Date
    let y: Int
}

Upvotes: 0

Related Questions