이 글은 누구를 위한 것인가
- 외부 차트 라이브러리 없이 네이티브 차트를 구현하려는 팀
- SwiftUI Charts로 건강, 피트니스, 금융 데이터를 시각화하려는 개발자
- 인터랙티브 차트와 애니메이션을 적용하려는 팀
들어가며
iOS 16에서 Swift Charts가 등장해 서드파티 라이브러리 없이 네이티브 차트를 만들 수 있다. 선언적 문법으로 라인, 바, 파이, 에어리어 차트를 구현하고, 다크모드와 다이나믹 타입을 자동 지원한다.
이 글은 bluefoxdev.kr의 SwiftUI Charts 데이터 시각화 가이드 를 참고하여 작성했습니다.
1. Swift Charts 핵심 개념
[Mark 유형]
LineMark: 라인 차트
BarMark: 바 차트 (수직/수평)
PointMark: 산점도
AreaMark: 에어리어 차트
RectangleMark: 히트맵
RuleMark: 기준선/임계값
[수식어]
.foregroundStyle(by: .value("분류", 카테고리)): 색상 분류
.symbol(by: .value("타입", 타입)): 심볼 분류
.interpolationMethod(.catmullRom): 곡선 보간
.annotation(position: .top): 값 레이블
[축 커스터마이징]
AxisMarks: 눈금, 레이블, 그리드
AxisValueLabel: 레이블 포맷
AxisGridLine: 그리드 스타일
[성능]
데이터 1000개 이상: LazyVStack으로 분할
.chartXScale(), .chartYScale(): 스케일 고정
애니메이션: .animation(.easeInOut, value: data)
2. Swift Charts 구현
import Charts
import SwiftUI
struct SalesData: Identifiable {
let id = UUID()
let month: String
let sales: Double
let category: String
}
// 멀티 라인 차트
struct MultiLineChart: View {
let data: [SalesData]
@State private var selectedMonth: String?
var body: some View {
Chart {
ForEach(data) { item in
LineMark(
x: .value("월", item.month),
y: .value("매출", item.sales)
)
.foregroundStyle(by: .value("카테고리", item.category))
.interpolationMethod(.catmullRom)
.symbol(by: .value("카테고리", item.category))
// 선택된 월 표시
if item.month == selectedMonth {
PointMark(
x: .value("월", item.month),
y: .value("매출", item.sales)
)
.symbolSize(100)
.annotation(position: .top) {
Text("₩\(Int(item.sales / 10000))만")
.font(.caption2).fontWeight(.bold)
.padding(4).background(.white).cornerRadius(4)
}
}
}
// 평균 기준선
if let avg = data.map(\.sales).reduce(0, +) / Double(max(data.count, 1)) as Double? {
RuleMark(y: .value("평균", avg))
.foregroundStyle(.gray.opacity(0.5))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
.annotation(position: .trailing) {
Text("평균").font(.caption2).foregroundColor(.gray)
}
}
}
.chartYAxis {
AxisMarks(format: .currency(code: "KRW").presentation(.narrow))
}
.chartXAxis {
AxisMarks(values: .automatic) { value in
AxisValueLabel()
AxisGridLine()
}
}
.chartOverlay { proxy in
GeometryReader { geo in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
let x = value.location.x - geo[proxy.plotAreaFrame].origin.x
if let month: String = proxy.value(atX: x) {
selectedMonth = month
}
}
.onEnded { _ in selectedMonth = nil }
)
}
}
.frame(height: 250)
.padding()
}
}
// 스택 바 차트
struct StackedBarChart: View {
let data: [SalesData]
var body: some View {
Chart(data) { item in
BarMark(
x: .value("월", item.month),
y: .value("매출", item.sales)
)
.foregroundStyle(by: .value("카테고리", item.category))
.cornerRadius(4)
}
.chartForegroundStyleScale([
"전자기기": Color.blue,
"의류": Color.green,
"식품": Color.orange
])
.frame(height: 200)
}
}
// 애니메이션 차트
struct AnimatedChart: View {
@State private var animate = false
let data: [SalesData]
var body: some View {
Chart(data) { item in
BarMark(
x: .value("월", item.month),
y: .value("매출", animate ? item.sales : 0)
)
}
.animation(.spring(duration: 0.8).delay(0.1), value: animate)
.onAppear { animate = true }
}
}
// Apple Watch 차트 (watchOS 9+)
struct WatchHeartRateChart: View {
let heartRates: [(date: Date, bpm: Double)]
var body: some View {
Chart {
ForEach(heartRates.indices, id: \.self) { i in
LineMark(
x: .value("시간", heartRates[i].date),
y: .value("심박수", heartRates[i].bpm)
)
.foregroundStyle(.red)
}
}
.chartYScale(domain: 40...200)
.frame(height: 80)
}
}
마무리
Swift Charts의 핵심은 선언적 구성이다. Chart → Mark → .foregroundStyle(by:) 조합으로 복잡한 다중 시리즈 차트를 간결하게 표현한다. chartOverlay로 드래그 제스처를 추가하면 인터랙티브 선택이 가능하고, .animation(.spring, value: data)로 데이터 변경 시 자연스러운 전환을 구현한다.