---
id: ios-charts-animation
title: iOS Charts (Swift Charts) — chart + animation
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [ios, charts, swiftui, vibe-coding]
tech_stack: { language: "Swift", applicable_to: ["iOS"] }
applied_in: []
aliases: [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
```swift
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
```swift
Chart(data) { sale in
    LineMark(
        x: .value("Day", sale.day),
        y: .value("Amount", sale.amount)
    )
    .interpolationMethod(.catmullRom)   // smooth curve
}
```

### Multi-series
```swift
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
```swift
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
```swift
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
```swift
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)
```swift
.chartXScale(domain: 0...100)
.chartYScale(domain: 0...200, type: .log)
```

### Axis customization
```swift
.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
```swift
@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 (자동)
```swift
@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)
```swift
@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)
```swift
.chartGesture { proxy in
    DragGesture()
        .onChanged { value in
            // proxy 가 chart space → data space.
        }
}
```

### Selection (iOS 17+)
```swift
@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
```swift
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
```swift
.chartXAxisLabel("Day")
.chartYAxisLabel("Sales ($)")

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

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

### Sector chart (pie, iOS 17)
```swift
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)
```swift
@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 친화.

## 🔗 관련 문서
- [[iOS_Charts_Health]]
- [[iOS_SwiftUI_Animation_Deep]]
- [[Frontend_Animation_Motion]]
