--- 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]]