Files
2nd/10_Wiki/Topics/Coding/iOS_Charts_Health.md
T
2026-05-09 22:47:42 +09:00

11 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-health iOS Charts / HealthKit / Spatial Coding draft B conceptual 2026-05-09 2026-05-09
ios
charts
healthkit
spatial
vibe-coding
language applicable_to
Swift
iOS
Swift Charts
HealthKit
ScreenCaptureKit
spatial audio
AVFoundation
health data

iOS Charts / HealthKit / Spatial

iOS 16+ Swift Charts (built-in), HealthKit (sensor data), Spatial Audio (immersive), ScreenCaptureKit (screen recording).

📖 핵심 개념

  • Swift Charts: declarative chart.
  • HealthKit: 사용자 health data (consent).
  • Spatial Audio: 3D positional sound.
  • ScreenCaptureKit: macOS / iOS 17+ screen capture.

💻 코드 패턴

Swift Charts

import Charts

struct SalesData: Identifiable {
    let id = UUID()
    let date: Date
    let amount: Double
}

struct SalesChart: View {
    let data: [SalesData]
    
    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Date", item.date),
                y: .value("Amount", item.amount)
            )
            .foregroundStyle(.blue)
            
            AreaMark(
                x: .value("Date", item.date),
                y: .value("Amount", item.amount)
            )
            .foregroundStyle(.blue.opacity(0.2))
        }
        .frame(height: 300)
    }
}

Bar / Pie / Scatter

Chart(data) {
    BarMark(x: .value("Cat", $0.category), y: .value("Sales", $0.sales))
}

Chart(data) {
    SectorMark(angle: .value("Sales", $0.sales))
}

Chart(data) {
    PointMark(x: .value("X", $0.x), y: .value("Y", $0.y))
}

Mixed

Chart {
    ForEach(data) { d in
        BarMark(x: .value("Day", d.day), y: .value("Sales", d.sales))
        LineMark(x: .value("Day", d.day), y: .value("Trend", d.trend))
            .foregroundStyle(.red)
    }
}

Interaction (iOS 17+)

@State var selectedDate: Date?

Chart(data) { d in
    LineMark(x: .value("Date", d.date), y: .value("Sales", d.sales))
}
.chartXSelection(value: $selectedDate)
.overlay {
    if let date = selectedDate, let item = data.first(where: { $0.date == date }) {
        Tooltip(item: item)
    }
}

Customization

Chart(data) {
    LineMark(...)
        .interpolationMethod(.catmullRom)
        .lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5]))
}
.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { value in
        AxisValueLabel(format: .dateTime.month(.abbreviated))
        AxisGridLine()
    }
}
.chartYAxis {
    AxisMarks(position: .leading)
}
.chartLegend(position: .top, alignment: .leading)

HealthKit setup

import HealthKit

class HealthManager: ObservableObject {
    let store = HKHealthStore()
    
    func request() async throws {
        let read: Set<HKObjectType> = [
            HKQuantityType(.stepCount),
            HKQuantityType(.heartRate),
            HKQuantityType(.activeEnergyBurned),
        ]
        try await store.requestAuthorization(toShare: [], read: read)
    }
    
    func steps(for date: Date) async throws -> Double {
        let type = HKQuantityType(.stepCount)
        let predicate = HKQuery.predicateForSamples(withStart: date.startOfDay, end: date.endOfDay)
        
        return try await withCheckedThrowingContinuation { cont in
            let query = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
                if let error { cont.resume(throwing: error); return }
                let steps = result?.sumQuantity()?.doubleValue(for: .count()) ?? 0
                cont.resume(returning: steps)
            }
            self.store.execute(query)
        }
    }
}
<!-- Info.plist -->
<key>NSHealthShareUsageDescription</key>
<string>We use your step count to show daily activity</string>

Live workout (HealthKit)

let session = try HKWorkoutSession(
    healthStore: store,
    configuration: HKWorkoutConfiguration().apply {
        $0.activityType = .running
        $0.locationType = .outdoor
    }
)
let builder = session.associatedWorkoutBuilder()

session.startActivity(with: Date())

// Live data
session.delegate = self
// → didChangeTo, didCollectDataOf

// End
session.end()
let workout = try await builder.finishWorkout()

Watch + iPhone sync

// Workout 가 watch 에서 시작, iPhone 에서 view.
// HKWorkoutSession 가 자동 sync.

Spatial Audio (AVFoundation)

import AVFoundation

let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let env = AVAudioEnvironmentNode()

env.position = AVAudio3DPoint(x: 0, y: 0, z: 0)  // listener

engine.attach(player)
engine.attach(env)

engine.connect(player, to: env, format: nil)
engine.connect(env, to: engine.outputNode, format: env.outputFormat(forBus: 0))

// Source position
player.position = AVAudio3DPoint(x: 5, y: 0, z: -10)  // 5m right, 10m forward

let file = try AVAudioFile(forReading: url)
player.scheduleFile(file, at: nil)

try engine.start()
player.play()

Music Spatial Audio

let asset = AVAsset(url: musicURL)

// Check spatial
let mix = AVAudioMix()
let params = AVMutableAudioMixInputParameters()
params.audioTimePitchAlgorithm = .spectral

// Apple Music API 가 spatial track 표시.

ScreenCaptureKit (iOS 17+ / macOS)

import ScreenCaptureKit

func startScreenRecording() async throws {
    let content = try await SCShareableContent.current
    guard let display = content.displays.first else { return }
    
    let filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: [])
    
    let config = SCStreamConfiguration()
    config.width = display.width * 2
    config.height = display.height * 2
    config.minimumFrameInterval = CMTime(value: 1, timescale: 60)
    
    let stream = SCStream(filter: filter, configuration: config, delegate: self)
    try stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .main)
    try await stream.startCapture()
}

// Output
extension ViewController: SCStreamOutput {
    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
        // Frame buffer — file 또는 stream
    }
}

→ macOS 가 가장 일반. iOS 17+ 도 지원.

App Intents (Charts + HealthKit)

struct DailyStepsIntent: AppIntent {
    static var title: LocalizedStringResource = "View Daily Steps"
    
    func perform() async throws -> some IntentResult & ProvidesDialog {
        let steps = try await HealthManager.shared.steps(for: Date())
        return .result(dialog: "You walked \(steps) steps today.")
    }
}

→ Siri / Shortcut 가 chart data 호출.

Widget + Chart

struct StepsWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "steps", provider: StepsProvider()) { entry in
            VStack {
                Text("Today")
                Chart(entry.data) {
                    BarMark(x: .value("Hour", $0.hour), y: .value("Steps", $0.steps))
                }
            }
        }
    }
}

Live Activity + Chart

struct WorkoutLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: WorkoutAttributes.self) { context in
            HStack {
                Text("\(context.state.heartRate) bpm")
                Chart(context.state.recentBpm) {
                    LineMark(x: .value("Time", $0.time), y: .value("BPM", $0.bpm))
                }
                .frame(height: 40)
            }
        }
    }
}

Vision OS Charts

// SwiftUI Charts 자동 호환 visionOS.
// 3D depth 추가 가능 (.padding3D, depth modifier).

Health Connect (Android 비교)

Android:
- Health Connect (Google) — 사용자 health data
- Background read / write
- 비슷 idea, 다른 API

iOS HealthKit + Android Health Connect = cross-platform health app.

CoreMotion (sensor)

import CoreMotion

let manager = CMMotionManager()
manager.deviceMotionUpdateInterval = 1.0 / 60  // 60 Hz

manager.startDeviceMotionUpdates(to: .main) { motion, error in
    guard let m = motion else { return }
    let heading = m.attitude.yaw
    let pitch = m.attitude.pitch
    let roll = m.attitude.roll
}

→ AR / motion-aware app.

1. Info.plist usage description.
2. Request 시 사용자 dialog.
3. 거부 가능 — graceful handle.
4. Background read = 추가 권한.
5. Apple Watch 가 별도.

Data export

let predicate = HKQuery.predicateForSamples(...)
let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: 0, sortDescriptors: nil) { _, samples, _ in
    // CSV / JSON export
}

→ User-controlled data export.

Anchor query (incremental)

let query = HKAnchoredObjectQuery(type: type, predicate: nil, anchor: lastAnchor, limit: HKObjectQueryNoLimit) { _, samples, _, anchor, _ in
    // 새 sample 만
    self.lastAnchor = anchor
}
query.updateHandler = { _, samples, _, anchor, _ in
    // Real-time updates
}
store.execute(query)

→ Live updates.

Apple Music API

import MusicKit

let request = MusicCatalogSearchRequest(term: query, types: [Song.self])
let response = try await request.response()
for song in response.songs {
    print(song.title, song.artistName)
}

→ Music app 통합.

Live Activities + Sensor

// 매 5초 업데이트
let timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
    Task {
        let heartRate = try await HealthManager.shared.currentHeartRate()
        let state = WorkoutAttributes.ContentState(heartRate: heartRate)
        await activity.update(.init(state: state, staleDate: nil))
    }
}

🤔 의사결정 기준

작업 추천
Chart Swift Charts
Health data HealthKit
Workout HKWorkoutSession
Screen record ScreenCaptureKit
Spatial audio AVAudioEngine env
Sensor CoreMotion
Music MusicKit

안티패턴

  • HealthKit 직접 raw save: validate + transform.
  • Background fetch 빈번: battery. throttle.
  • Privacy description 빈약: deny 자주.
  • Chart 실시간 큰 dataset: throttle / aggregate.
  • Spatial 단일 source 만: 의미 X. 환경 + 다중 source.
  • App store review 시 fake data: reject.

🤖 LLM 활용 힌트

  • Swift Charts = built-in. Type-safe.
  • HealthKit + watch + widget 통합.
  • ScreenCaptureKit = modern (iOS 17+).
  • Spatial audio = environment + position.

🔗 관련 문서