7.6 KiB
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 |
|
|
|
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.
}
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 친화.