Files
2nd/10_Wiki/Topics/Coding/iOS_Charts_Animation.md
T
2026-05-10 22:08:15 +09:00

7.6 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
ios-charts-animation iOS Charts (Swift Charts) — chart + animation Coding draft B conceptual 2026-05-09 2026-05-09
ios
charts
swiftui
vibe-coding
language applicable_to
Swift
iOS
Swift Charts
BarMark
LineMark
AreaMark
chartXScale
chartGesture
animated chart

Swift Charts

iOS 16+ native chart. BarMark / LineMark / AreaMark / PointMark. Declarative + animatable.

📖 핵심 개념

  • iOS 16+ (SwiftUI).
  • Mark = data point.
  • Scale = axis range.
  • Composable + animatable.

💻 코드 패턴

Bar chart

import Charts

struct Sale: Identifiable {
    let id = UUID()
    let day: String
    let amount: Double
}

let data: [Sale] = [
    Sale(day: "Mon", amount: 100),
    Sale(day: "Tue", amount: 150),
    Sale(day: "Wed", amount: 80),
]

Chart(data) { sale in
    BarMark(
        x: .value("Day", sale.day),
        y: .value("Amount", sale.amount)
    )
}

Line chart

Chart(data) { sale in
    LineMark(
        x: .value("Day", sale.day),
        y: .value("Amount", sale.amount)
    )
    .interpolationMethod(.catmullRom)   // smooth curve
}

Multi-series

Chart {
    ForEach(salesA) { sale in
        LineMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
            .foregroundStyle(by: .value("Series", "A"))
    }
    ForEach(salesB) { sale in
        LineMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
            .foregroundStyle(by: .value("Series", "B"))
    }
}

Combined chart

Chart(data) { sale in
    BarMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
        .opacity(0.5)
    
    LineMark(x: .value("Day", sale.day), y: .value("Trend", sale.trend))
        .foregroundStyle(.red)
}

→ Bar + line overlay.

Area chart

Chart(data) { sale in
    AreaMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
        .foregroundStyle(LinearGradient(
            gradient: Gradient(colors: [.blue.opacity(0.5), .clear]),
            startPoint: .top,
            endPoint: .bottom
        ))
}

Point + rule

Chart(data) { sale in
    PointMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
    
    if let max = data.max(by: { $0.amount < $1.amount }) {
        RuleMark(y: .value("Max", max.amount))
            .foregroundStyle(.red)
            .lineStyle(StrokeStyle(lineWidth: 2, dash: [5]))
            .annotation(position: .top) {
                Text("Max: \(max.amount, specifier: "%.0f")")
            }
    }
}

Scale (axis)

.chartXScale(domain: 0...100)
.chartYScale(domain: 0...200, type: .log)

Axis customization

.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { value in
        AxisGridLine()
        AxisTick()
        AxisValueLabel(format: .dateTime.day().month())
    }
}

.chartYAxis {
    AxisMarks(position: .leading) { value in
        AxisValueLabel { Text("$\(value.as(Int.self) ?? 0)") }
    }
}

Animation

@State var data: [Sale] = []
@State var animate = false

Chart(data) { sale in
    BarMark(x: .value("Day", sale.day), y: .value("Amount", animate ? sale.amount : 0))
}
.onAppear {
    withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
        animate = true
    }
}

→ Initial animate.

Update animation (자동)

@State var data: [Sale] = initial

Chart(data) { sale in
    BarMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
}
.animation(.spring(), value: data)

// 매 변경 시 smooth.
Button("Refresh") {
    data = newData
}

→ State 변경 = 자동 animate.

Interaction (tap / drag)

@State var selected: Sale?

Chart(data) { sale in
    BarMark(x: .value("Day", sale.day), y: .value("Amount", sale.amount))
        .foregroundStyle(selected?.id == sale.id ? .red : .blue)
}
.chartOverlay { proxy in
    GeometryReader { geo in
        Rectangle()
            .fill(.clear)
            .contentShape(Rectangle())
            .gesture(DragGesture()
                .onChanged { value in
                    let x = value.location.x
                    if let day: String = proxy.value(atX: x) {
                        selected = data.first { $0.day == day }
                    }
                }
            )
    }
}

if let s = selected {
    Text("\(s.day): $\(s.amount)")
}

chartGesture (iOS 17)

.chartGesture { proxy in
    DragGesture()
        .onChanged { value in
            // proxy 가 chart space → data space.
        }
}

Selection (iOS 17+)

@State var selected: Date?

Chart(data) { sale in
    BarMark(x: .value("Date", sale.date), y: .value("Amount", sale.amount))
}
.chartXSelection(value: $selected)

→ 자동 selection UI.

HealthKit data

import HealthKit

let store = HKHealthStore()
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!

let query = HKStatisticsCollectionQuery(
    quantityType: stepType,
    quantitySamplePredicate: nil,
    options: .cumulativeSum,
    anchorDate: startDate,
    intervalComponents: DateComponents(day: 1)
)

query.initialResultsHandler = { _, results, _ in
    // → Chart data.
}

iOS_Charts_Health.

Performance

- 1000+ point = lazy / sample.
- Animation 가 큰 data = jank.
- Chart 의 size 가 작으면 skip mark.

@State var sampledData = downsample(rawData, to: 100)

Format

.chartXAxisLabel("Day")
.chartYAxisLabel("Sales ($)")

// Annotation
.chartXAxis(.hidden)
.chartLegend(.hidden)

Gradient

LineMark(...)
    .foregroundStyle(.linearGradient(
        colors: [.blue, .purple],
        startPoint: .leading, endPoint: .trailing
    ))

Sector chart (pie, iOS 17)

Chart(data) { item in
    SectorMark(
        angle: .value("Amount", item.amount),
        innerRadius: .ratio(0.6),
        outerRadius: .ratio(1.0)
    )
    .foregroundStyle(by: .value("Type", item.type))
}

→ Donut / pie chart.

Dynamic data (live update)

@State var data: [Sale] = []

Chart(data) { sale in
    LineMark(x: .value("Time", sale.time), y: .value("Value", sale.value))
}
.onReceive(timer) { _ in
    data.append(Sale(time: Date(), value: random()))
    if data.count > 100 { data.removeFirst() }
}

→ Real-time chart.

vs other charts

Swift Charts: native, iOS 16+, declarative.
Charts (DGCharts): iOS 12+, mature, big.
SwiftUICharts (open): simple.
WordSwiftUICharts: niche.

→ iOS 16+ = Swift Charts default.

Limits

- iOS 16+ 만 (15 아래 사용 불가).
- 매우 복잡 chart (financial OHLC) = 어려움.
- WatchOS 가 작은 size 친화.

→ Complex = DGCharts / native draw.

🤔 의사결정 기준

작업 Mark
Bar BarMark
Line / smooth LineMark + interpolationMethod
Area / gradient AreaMark
Scatter PointMark
Threshold RuleMark + annotation
Pie / donut SectorMark (iOS 17)
Combined Bar + Line
Real-time onReceive + state

안티패턴

  • 1000+ point 그대로: jank.
  • Animation 가 큰 list: drop frame.
  • iOS 15 + Swift Charts: 안 됨.
  • Custom UI 없이 mark 만: 인터랙션 X.
  • Format / scale 무시: scale 깨짐.
  • HealthKit 없이 mock: real data X.

🤖 LLM 활용 힌트

  • iOS 16+ Swift Charts 가 default.
  • Mark + Scale + Animation 가 declarative.
  • chartOverlay 가 interaction.
  • HealthKit / async data 친화.

🔗 관련 문서