← 모든 글

iOS 26의 HealthKit + SwiftUI: 권한 요청, 샘플 타입, 그리고 두 개의 앱을 출시하며 얻은 크로스 플랫폼 패턴

HealthKit은 SwiftUI 앱에서 제대로 출시하기가 까다로운 Apple 프레임워크 중 하나입니다. 권한 흐름에는 일시적 실패로 인해 사용자가 영구적으로 잠겨버릴 수 있는 함정이 있습니다. 샘플 타입은 정량적 데이터(HKQuantitySample로 물 섭취량, 걸음 수, 칼로리)와 범주형 데이터(HKCategorySample로 마음챙김 세션, 수면, 월경 흐름)로 어색하게 나뉩니다. async API 표면은 콜백 기반의 HealthKit 호출을 withCheckedThrowingContinuation으로 래핑해야 합니다. 그리고 watchOS에서는 패턴이 또 달라집니다.

저는 두 개의 프로덕션 앱에서 HealthKit을 출시했습니다. Water(물 섭취량 추적, 약 192줄의 HealthKitService)1와 Return(마음챙김 세션 기록, 약 171줄의 HealthKitManager와 155줄의 HealthKitPermissionSheet)입니다.2 두 앱은 함께 HealthKit의 두 가지 주요 샘플 형태, 양방향(읽기 + 쓰기 vs. 쓰기 전용), 그리고 단일 플랫폼과 크로스 플랫폼 배포를 모두 다룹니다.

이 글에서는 프로덕션에서 살아남은 패턴들을 다룹니다. 사전 권한 UX, SwiftUI의 .healthDataAccessRequest 모디파이어 vs. 레거시 requestAuthorization API, 샘플 쿼리를 위한 async 래퍼, 그리고 watchOS 특유의 차이점들입니다.

TL;DR

  • 권한 상태 보고는 비대칭적입니다. Apple의 API는 “공유 승인됨”은 안정적으로 알려주지만, 프라이버시상의 이유로 “읽기 거부됨”은 알려주지 않습니다. 이 글은 읽기 상태를 추론하지 않고 “사용자가 요청받았지만 승인하지 않은” 상태를 감지하는 방법을 다룹니다.
  • 정량적 데이터는 HKQuantitySampleHKQuantityTypeHKUnit과 함께 사용합니다(Water는 물 섭취량에 .literUnit(with: .milli)를 사용). 범주형 데이터는 HKCategorySampleHKCategoryType과 함께 사용합니다(Return은 .mindfulSession을 사용).
  • 사전 권한 시트는 가장 많이 건너뛰는 패턴입니다. Apple의 시스템 권한 다이얼로그는 무미건조합니다. 먼저 보여주는 커스텀 View는 가치를 설명하고 승인율을 극적으로 높입니다.
  • watchOS HealthKit은 별도의 HKHealthStore 인스턴스가 필요하고, 권한 재요청 규칙이 더 엄격하며, 사전 권한 UX를 위한 SwiftUI 시트를 표시할 수 없습니다.
  • Return의 recordAuthorizationAttempt() 패턴은 일시적인 표시 실패가 영구적인 거부로 처리되는 것을 방지합니다.

두 가지 샘플 형태

HealthKit은 작성하는 모든 코드 라인에 영향을 미치는 축을 따라 샘플 모델을 분할합니다.3

샘플 형태 일반적인 사용 API
HKQuantitySample 물, 걸음 수, 칼로리, 체중, 심박수 HKQuantityType + HKUnit + HKQuantity
HKCategorySample 마음챙김 세션, 수면, 월경 흐름, 성행위 HKCategoryType + HKCategoryValue
HKWorkout 구조화된 운동(달리기, 수영) HKWorkoutBuilder, HKWorkoutSession

물은 정량입니다. HealthKitService.swift의 실제 프로덕션 코드입니다.1

import HealthKit

@Observable
final class HealthKitService {
    static let shared = HealthKitService()

    private let healthStore = HKHealthStore()
    private let waterType = HKQuantityType(.dietaryWater)

    func logWater(amount: Double, date: Date = .now) async throws -> UUID {
        guard isAuthorized else { throw HealthKitError.notAuthorized }

        let quantity = HKQuantity(unit: .literUnit(with: .milli), doubleValue: amount)
        let sample = HKQuantitySample(
            type: waterType,
            quantity: quantity,
            start: date,
            end: date,
            metadata: [HKMetadataKeyWasUserEntered: true]
        )

        try await healthStore.save(sample)
        return sample.uuid
    }
}

프로덕션에서 얻은 세 가지 세부 사항입니다.

  1. .literUnit(with: .milli)는 물을 밀리리터로 표현하는 정규 단위입니다. HealthKit은 어떤 단위(미국 액량 온스, 리터)도 받아들이지만, 생성자가 중요한 이유는 Apple이 내부적으로 모든 것을 리터로 정규화하기 때문입니다. .milli를 선택하면 분수 리터(0.240, 0.500) 대신 정수 값(240, 500)을 저장할 수 있습니다.
  2. HKMetadataKeyWasUserEntered: true는 샘플이 측정된 것이 아니라 수동으로 입력되었음을 표시합니다. Health 앱은 이런 샘플에 작은 “수동 입력” 표시를 보여줍니다. 사용자는 수동으로 입력된 데이터를 저울로 측정한 데이터와는 다르게 신뢰합니다.
  3. startend는 즉각적인 샘플의 경우 같은 Date입니다. 정량은 [start, end] 윈도우에 걸쳐 누적되지만, “방금 240ml를 마셨다”의 경우 윈도우는 한 점으로 축소됩니다.

마음챙김 세션은 범주형입니다. Return의 HealthKitManager.swift의 실제 프로덕션 코드입니다.2

import HealthKit

@MainActor
class HealthKitManager {
    static let shared = HealthKitManager()
    let healthStore = HKHealthStore()
    let mindfulType = HKCategoryType(.mindfulSession)

    func saveMindfulSession(start: Date, end: Date) async -> Bool {
        guard isAvailable else { return false }
        guard end > start else { return false }

        let sample = HKCategorySample(
            type: mindfulType,
            value: HKCategoryValue.notApplicable.rawValue,
            start: start,
            end: end
        )
        // ... save via healthStore.save(sample) ...
    }
}

두 가지 세부 사항입니다.

  1. HKCategoryValue.notApplicable.rawValue는 핵심적인 센티넬 값입니다. 마음챙김 세션은 의미 있는 “값”이 없으므로(지속 시간 마커입니다), HealthKit은 타입 시스템을 만족시키기 위해 필드에 센티넬 카테고리 값(Int(0))을 요구합니다. 다른 카테고리 샘플들은 더 풍부한 값을 가집니다. 수면은 HKCategoryValueSleepAnalysis.asleep을 사용하는 식입니다.
  2. start/end 윈도우는 카테고리 샘플의 경우 실제 의미가 있습니다. 10분짜리 마음챙김 세션은 startend가 10분 떨어져 있고, HealthKit의 일일 마음챙김 분 합계는 이러한 지속 시간을 합산합니다.

권한 흐름(그리고 그 함정)

HealthKit 권한은 의도적으로 비대칭적입니다. authorizationStatus(for:)는 공유/쓰기 상태를 정직하게 보고하지만(.sharingAuthorized, .sharingDenied, .notDetermined), 읽기 승인이나 거부는 해당 API를 통해 직접 관찰할 수 없습니다. Apple은 앱이 사용자의 Health 프로필에 어떤 데이터가 존재하는지 추론하는 것을 막기 위해 의도적으로 읽기 상태를 숨깁니다.4 읽기 결정은 간접적으로 알게 됩니다. 사용자가 읽기를 승인했다면 쿼리는 데이터를 반환하고, 거부했다면 빈 결과를 반환합니다. 코드가 분기할 수 있는 “읽기가 거부됨” 신호는 없습니다.

두 앱은 이를 다르게 우회합니다.

Water는 자체 샘플을 읽습니다. Water는 물 항목을 쓰고 읽기 때문에(“오늘의 기록”을 채우기 위해), 권한 흐름이 읽기 거부가 보이지 않는 경우를 처리해야 합니다.1

private var typesToShare: Set<HKSampleType> { [waterType] }
private var typesToRead: Set<HKSampleType> { [waterType] }

func requestAuthorization() async throws {
    guard isHealthDataAvailable else { throw HealthKitError.notAvailable }

    try await healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)

    await MainActor.run { checkAuthorizationStatus() }
}

func checkAuthorizationStatus() {
    authorizationStatus = healthStore.authorizationStatus(for: waterType)
    isAuthorized = authorizationStatus == .sharingAuthorized
}

checkAuthorizationStatus가 하지 않는 것에 주목하세요. 읽기 권한 상태를 묻지 않습니다. Water는 단지 공유 상태만 확인합니다. 사용자가 공유는 승인했지만 읽기를 거부한 경우, Water의 UI는 “항목 없음”을 표시합니다(명시적 오류 때문이 아니라, 읽기가 빈 결과를 반환하기 때문입니다). Water는 공유 결정을 신뢰하고 데이터의 부재가 스스로 말하도록 둡니다. 사용자가 신경 쓴다면 설정에서 수정할 수 있습니다.

Return은 쓰기만 합니다. Return은 마음챙김 세션을 기록하지만 다시 읽지는 않습니다. 보여주는 세션 목록은 HealthKit이 아닌 자체 NSUbiquitousKeyValueStore에서 가져옵니다. 따라서 Return의 권한 요청은 쓰기 전용입니다.2

func requestAuthorization() async -> Bool {
    guard isAvailable else { return false }

    do {
        try await healthStore.requestAuthorization(
            toShare: [mindfulType],
            read: []  // We only need write access
        )
        hasRequestedHealthKit = authorizationStatus != .notDetermined
        return isAuthorizedToWrite()
    } catch {
        hasRequestedHealthKit = authorizationStatus != .notDetermined
        return false
    }
}

read: [] 빈 집합은 의도적입니다. 필요하지 않은 읽기 권한을 요청하면 App Review에서 정당화해야 할 권한 범위가 확장되고, “Return이 마음챙김 세션을 읽으려고 합니다”를 보는 사용자들을 혼란스럽게 합니다. 앱이 명백히 그럴 필요가 없는데도 말입니다.

함정. 두 앱은 같은 방어적 패턴으로 수렴했습니다. 상태가 실제로 .notDetermined를 넘어 움직였을 때만 권한 시도를 “완료됨”으로 표시합니다. 순진한 코드는 이렇습니다.

hasRequestedHealthKit = true  // ❌ wrong: treats transient failures as denial

Return의 올바른 패턴입니다.2

hasRequestedHealthKit = authorizationStatus != .notDetermined  // ✓ checks real state

왜 중요한지: SwiftUI의 .healthDataAccessRequest 모디파이어와 그 기반의 requestAuthorization API는 특정 조건(시트 충돌, 뷰 라이프사이클 중단, 일시적인 OS 상태)에서 시스템 다이얼로그를 표시하지 못할 수 있습니다. 실제 상태를 확인하기 전에 시도를 “완료됨”으로 표시하면, 앱은 사용자가 거부했다고 생각하지만 실제로는 시스템 다이얼로그가 나타나지 않은 상태에 사용자를 가둬버립니다. 사용자는 설정 → 개인정보 보호 → 건강으로 가서 수동으로 승인하지 않는 한 돌아갈 길이 없는데, 프롬프트를 본 적이 없으니 그렇게 할 생각조차 못합니다. Return의 recordAuthorizationAttempt()는 정확히 이 경우를 위해 존재합니다.

사전 권한 시트

Apple의 HealthKit 권한 다이얼로그는 정확하지만 매력이 없습니다. 앱이 공유하거나 읽으려는 타입 목록을 토글과 함께 표시할 뿐입니다. 앱이 그 데이터를 원하는지에 대한 맥락도, 혜택 설명도, 앱 브랜드도 없습니다. 프롬프트가 맥락 없이 표시되기 때문에 사용자는 “허용 안 함”을 누릅니다.

Return은 시스템 다이얼로그 전에 나타나는 HealthKitPermissionSheet을 제공합니다. 이 시트는 Return 앱 아이콘을 Apple Health 아이콘 옆에 표시하고(HIG에 맞게: Apple Health 아이콘은 잘리거나 그림자가 들어가서는 안 됩니다),5 혜택을 명시하며(“연습 추적” / “명상 세션을 Apple Health에 마음챙김 분으로 저장”), 세 가지 혜택 행을 나열하고(“Apple Health에서 연습 확인”, “모든 기기에 동기화”, “다른 웰니스 앱과 작동”), 시스템 요청을 트리거하는 단일 전진 Continue 버튼으로 끝납니다. HealthKitPermissionSheet.swift의 실제 구조입니다.6

struct HealthKitPermissionSheet: View {
    var onEnableRequested: () -> Void
    var theme: Theme

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 16) {
                Image("ReturnAppIcon")
                    .resizable().scaledToFit().frame(width: 80, height: 80)
                    .clipShape(RoundedRectangle(cornerRadius: 18))

                Text("+").font(.title)

                Image("AppleHealthIcon")
                    .resizable().scaledToFit().frame(width: 80, height: 80)
                    // No clipShape; Apple HIG forbids altering the Health icon
            }

            // Title + subtitle + benefits list

            // (See discussion below for the production comment.)
            Button { onEnableRequested() } label: {
                Text("Continue")
            }
        }
        .interactiveDismissDisabled(true)
    }
}

interactiveDismissDisabled(true)와 취소 버튼이 없는 점은 의도적인 설계 선택입니다. Apple의 App Review 가이드라인 5.1.1은 앱이 사용자의 개인정보 보호 설정을 존중해야 하며 동의를 조작하거나 속이거나 강요해서는 안 된다고 요구합니다.10 프로덕션 코드의 Continue 버튼 위 주석은 이렇게 되어 있습니다.

Single forward action: the system HealthKit dialog owns the real yes/no choice. Apple Guideline 5.1.1(iv): the priming screen may not include any exit/dismiss path that bypasses the system permission request.

이러한 표현은 문자 그대로의 가이드라인 텍스트보다 더 엄격합니다(가이드라인은 동의를 존중하고 강요하지 않는다고 말하지만, 프라이밍 화면 UX에 대해 그런 말로 규정하지는 않습니다). 이는 Return의 해석으로, 소스 코드에 명문화되어 있습니다. 의도는 이렇습니다. 거부하는 사용자는 Return의 화면이 아니라 Apple의 화면에서 그렇게 해야 합니다. 결과는 단일 전진 동작과 탈출구가 없는 시트입니다. 카피와 혜택 행이 탭을 얻어내야 합니다. “나중에 생각해볼게요” 분기는 없습니다.

흐름은 다음과 같습니다.

  1. 사용자가 Return의 설정에서 “Connect Health”를 탭합니다.
  2. 사전 권한 시트가 나타납니다. 사용자가 설명을 읽습니다.
  3. 사용자가 Continue를 탭합니다. requestAuthorization이 실행되고 시스템 다이얼로그가 나타나는 동안 시트는 마운트된 상태로 유지됩니다.
  4. 사용자가 시스템 다이얼로그에서 수락(또는 거부)합니다. Return은 결과를 캡처하기 위해 recordAuthorizationAttempt()를 호출하고 시트가 닫힙니다.

왜 굳이 그렇게 할까요. Apple은 사전 권한 시트로 인한 승인율 상승에 대한 공식 수치를 발표하지 않았지만, A/B 테스트를 진행하고 그것을 끈질기게 수행한 모든 iOS 개발자들은 같은 방향을 보고했습니다. 사전 권한 시트는 공유 권한 승인율을 극적으로 높입니다. 이 패턴은 이제 Apple 자체 템플릿(사진, 카메라, 위치)도 점점 자체 사전 권한 UX를 포함할 정도로 일반적입니다.

watchOS 변형

watchOS HealthKit은 iOS HealthKit과 동일한 API 표면(HKHealthStore, 샘플 타입, 권한)을 공유하지만, 세 가지 구조적 차이가 있습니다.

  1. 각 watch 앱마다 새로운 HKHealthStore. watch 앱과 페어링된 iPhone 앱은 각각 자체 HKHealthStore 인스턴스를 가집니다. 둘 다 사용자의 HealthKit 데이터베이스에 쓸 수 있고, 둘 다 자신의 샘플을 읽을 수 있습니다. 스토어는 페어 간에 공유되지 않습니다.
  2. 사전 권한을 위한 SwiftUI 시트 없음. watchOS 뷰 계층은 iOS처럼 시트를 지원하지 않습니다. 사전 권한 UX는 전체 화면이어야 합니다.
  3. 더 엄격한 재요청 규칙. watchOS의 requestAuthorization 호출은 사용자가 이전에 거부한 경우 시스템 다이얼로그를 다시 표시하는 것에 대해 더 보수적입니다. watch 자체에는 설정 → 개인정보 보호 → 건강 UI가 없으므로, 사용자에게 iPhone의 Watch 앱으로 가서 설정을 변경하도록 안내해야 할 수도 있습니다.

WatchHealthKitManager.swift의 실제 프로덕션 코드입니다.7

@MainActor
class WatchHealthKitManager {
    static let shared = WatchHealthKitManager()
    let healthStore = HKHealthStore()
    let mindfulType = HKCategoryType(.mindfulSession)

    private init() {}

    var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }

    /// Returns true if the system request completed (success or already-authorized).
    /// Caller checks isAuthorizedToWrite() separately for the actual share state.
    func requestAuthorization() async -> Bool {
        guard isAvailable else { return false }
        do {
            try await healthStore.requestAuthorization(toShare: [mindfulType], read: [])
            return true
        } catch {
            return false
        }
    }

    func isAuthorizedToWrite() -> Bool {
        guard isAvailable else { return false }
        return healthStore.authorizationStatus(for: mindfulType) == .sharingAuthorized
    }
    // ... saveMindfulSession identical to iOS variant ...
}

분리는 의도적입니다. requestAuthorization시스템 호출이 성공했는지 여부를 보고하고, isAuthorizedToWrite는 사용자가 선택한 결과를 보고합니다. watch에서 이를 분리하면 코드를 추론하기가 더 쉬워집니다. iOS에서는 동등한 분리가 HealthKitManagerrecordAuthorizationAttempt() 더하기 isAuthorizedToWrite() 쌍으로 나타납니다.

Watch 클래스는 다음이 필요하지 않으므로 iOS 클래스의 약 절반 크기입니다.

  • hasRequestedHealthKit UserDefaults 플래그(watch UX는 지원해야 할 두 번째 시도 경로가 더 적음).
  • HealthKitPermissionSheet 배관(watchOS에는 시트 UI가 없음).
  • recordAuthorizationAttempt() 메서드(watch의 더 좁은 흐름은 일시적 실패 엣지 케이스가 더 적음).

트레이드오프는 시스템 다이얼로그를 거부하고 iPhone의 Watch 앱에서 수정할 수 있다는 것을 모르는 사용자들의 경우 watchOS 앱이 때때로 의지할 곳 없는 거부 상태에 빠진다는 것입니다. Return은 이 경우 다시 요청을 시도하는 대신 작은 인앱 안내(“iPhone의 Watch 앱 열기 → 내 시계 → 개인정보 보호 → 건강”)를 보여줍니다.

다르게 만들었을 것들

두 개의 프로덕션 앱에서 HealthKit을 출시하며 얻은 세 가지 교훈입니다.

두 API 모두 작동합니다. 선택은 표시 맥락에 따라 다릅니다. SwiftUI의 .healthDataAccessRequest 모디파이어는 레거시 API를 더 선언적인 형태로 래핑하고 iPadOS에서 표시 맥락을 올바르게 처리합니다. Return은 모디파이어를 사용하며, SwiftUI 부모 뷰가 연결할 수 있도록 HealthKitManager의 공개 mindfulShareTypes 속성을 통해 공유 타입을 노출합니다. Water는 권한 흐름이 비-View 컨텍스트(@Observable 서비스)에서 실행되기 때문에 레거시 healthStore.requestAuthorization을 직접 사용합니다. 이 분할은 유용한 패턴입니다. 요청이 SwiftUI 라이프사이클 이벤트(시트 안의 버튼 탭)에서 시작될 때는 모디파이어를 선호하고, 요청이 서비스에서 시작될 때는 레거시 API로 폴백합니다.

사전 권한 시트는 iOS에서는 엔지니어링 비용을 들일 가치가 있지만 watchOS에서는 그렇지 않습니다. 사전 권한 시트는 약 4시간 정도의 작업을 추가합니다(시트 뷰, 카피, 테마, 통합). iOS에서의 승인율 상승은 4시간이 명백한 투자가 될 만큼 큽니다. watchOS에서는 동등한 전체 화면 사전 권한이 더 침습적이고(시트가 아니라 전체 시계 화면을 차지함), 사용자가 작은 화면에서 긴 카피를 읽을 가능성이 적으며, watch UX 흐름은 사용자가 HealthKit이 필요한 기능을 요청하는 진입점이 더 적습니다. 저는 Return을 watchOS에서 사전 권한 시트 없이 출시했으며 후회한 적이 없습니다.

hasRequestedHealthKit을 실시간 권한 상태와 별도로 추적하세요. HealthKit API는 현재 권한 상태를 알려주지만, 지금까지 한 번이라도 요청했는지는 알려주지 않습니다. 이 구별이 중요한 이유는 올바른 두 번째 탭 동작이 그것에 따라 달라지기 때문입니다. 첫 번째 탭은 requestAuthorization을 호출해야 합니다. 두 번째 탭에서 사용자가 이전에 거부했다면, API를 다시 호출(거부된 상태에서 조용히 아무 동작도 하지 않음)하기보다 “설정 → 개인정보 보호 → 건강” 알림을 표시해야 합니다. hasRequestedHealthKit UserDefaults 플래그가 두 번째 탭을 유용하게 만드는 요소입니다.

HealthKit을 사용하지 말아야 할 때

거절도 설계의 일부입니다.

할 수 있다는 이유만으로 HealthKit에 쓰지 마세요. 집중 타이머 앱은 운동을 쓸 필요가 없습니다. 메모 앱은 마음챙김을 기록할 필요가 없습니다. “공짜 통합”이라는 이유로 HealthKit을 추가하면 사용자에게 실질적인 가치를 주지 않으면서 프라이버시 풋프린트, 권한 마찰, App Review 설문 범위를 확장합니다.

피할 수 있다면 HealthKit을 읽지 마세요. 읽기 권한은 App Review에서 정당화하기 더 어렵고 사용자에게 설명하기 더 어렵습니다. HealthKit을 읽는 많은 앱들은 쓰기만 하고 사용자가 Apple Health에서 데이터를 볼 수 있도록 할 수 있습니다. 읽기 흐름은 미미한 UX 이득을 위해 표면적을 두 배로 늘립니다.

크로스 디바이스 전용 데이터를 위해 Mac에서 HealthKit을 사용하지 마세요. macOS는 macOS 13부터 HealthKit을 지원하지만, 대부분의 Health 데이터는 iPhone이나 Apple Watch에서 시작됩니다. Mac 앱에 같은 데이터가 필요하다면, iPhone의 HealthKit에 쓰고 Apple의 크로스 디바이스 동기화가 Mac에 표시되도록 두세요. Mac에서의 직접 HealthKit 쓰기는 유효하지만 실제로는 드뭅니다.

거부 후 철회 경로를 테스트하지 않고 출시하지 마세요. 사용자는 권한을 부여한 다음 설정 → 개인정보 보호 → 건강에서 철회합니다. 앱은 철회를 우아하게 처리해야 하며, 보통 다음에 기능을 사용하려고 할 때 설정 딥링크 안내를 표시합니다. Water와 Return 모두 이 경로를 출시했지만, 둘 다 처음에는 제대로 하지 못했습니다.

iOS 26+에서 HealthKit을 출시하는 SwiftUI 앱에 의미하는 것

세 가지 시사점입니다.

  1. UI를 설계하기 전에 쓰기 전용 vs. 읽기+쓰기를 결정하세요. 쓰기 전용은 더 작은 표면이고 더 빠른 App Review입니다. 읽기+쓰기는 더 유연하지만 읽기 거부가 보이지 않는 비대칭성을 추가합니다.
  2. iOS에서 사전 권한 시트를 출시하세요. 승인율 상승은 실제입니다. Apple의 아이콘을 올바르게 사용하고(잘림 없음, 그림자 없음), 혜택을 명시한 다음, 시스템 API를 호출하세요.
  3. watchOS HealthKit을 iOS 패턴의 더 작고 단순한 변형으로 다루세요. 의식이 적고, 시트가 없으며, 단일 목적 권한입니다. 재승인을 위해 사용자를 iPhone의 Watch 앱으로 안내하세요.

이 글을 같은 앱 군에 대한 이전 글들과 함께 읽어보세요. Apple Intelligence를 위한 타입 지정 App Intents, 크로스 LLM 에이전트를 위한 MCP 서버, 비주얼 레이어를 위한 Liquid Glass 패턴, 크로스 디바이스 도달을 위한 멀티 플랫폼 출시. HealthKit은 비주얼 레이어와 통합 표면 아래에 위치한 데이터 소스 레이어입니다.8

FAQ

iOS 앱과 watchOS 익스텐션 간에 권한을 공유할 수 있나요?

아니요. iOS의 HKHealthStore와 watchOS의 HKHealthStore는 독립적입니다. 사용자는 별도의 시스템 다이얼로그를 통해 각 플랫폼에서 권한을 별도로 부여합니다. 각 측의 코드는 자체 권한 상태를 확인합니다. watch에서 iOS의 상태를 읽거나 그 반대로 할 수는 없습니다.

사용자가 쓰기 권한을 철회하면 내 샘플은 어떻게 되나요?

기존 샘플은 HealthKit에 남아 있습니다. 사용자가 원한다면 Health 앱에서 수동으로 삭제할 수 있지만, 앱의 권한을 철회하는 것은 새로운 쓰기만 멈춥니다. 앱은 더 이상 샘플을 저장하거나 수정할 수 없지만, 사용자의 과거 데이터는 보존됩니다.

.healthDataAccessRequest SwiftUI 모디파이어는 프로덕션에서 사용하기 안전한가요?

iOS 17.4+에서 작동하며 후속 릴리스에서 개선되어 왔습니다. Return은 모디파이어를 사용합니다(SwiftUI가 사용할 수 있도록 HealthKitManagermindfulShareTypes를 노출합니다). Water는 요청이 비-View @Observable 서비스에서 시작되기 때문에 레거시 healthStore.requestAuthorization을 직접 사용합니다. 요청이 시작되는 위치를 기준으로 선택하세요. SwiftUI 라이프사이클 이벤트 → 모디파이어; 서비스 또는 비-View 컨텍스트 → 레거시 API.

공유 권한을 요청한 적도 없는데 왜 HealthKit 권한 상태가 .sharingAuthorized라고 나오나요?

그렇지 않습니다. 상태는 HKObjectType별로 다릅니다. authorizationStatus(for: heartRateType)를 확인했는데 심박수를 요청한 적이 없다면 .notDetermined를 받게 됩니다. 상태는 해당 특정 타입에 대한 권한 부여가 성공한 후에만 .sharingAuthorized로 갑니다.

HealthKit을 위해 프라이버시 매니페스트가 필요한가요?

네. HealthKit을 다루는 앱은 PrivacyInfo.xcprivacy(프라이버시 매니페스트)와 App Store Connect 프라이버시 설문에 사용을 선언해야 합니다. 관련 항목은 Info.plistNSHealthShareUsageDescriptionNSHealthUpdateUsageDescription, 그리고 프라이버시 매니페스트의 해당 선언입니다.9

References


  1. Production code in Water/Water/Services/HealthKitService.swift (192 lines). Author’s Water, a SwiftUI water-tracking app available on iOS, iPadOS, macOS, watchOS, and visionOS. Uses HKQuantitySample with HKQuantityType(.dietaryWater) and the HKMetadataKeyWasUserEntered flag. 

  2. Production code in Return/Return/HealthKitManager.swift (171 lines). Author’s Return, a SwiftUI meditation-timer app available on iOS, iPadOS, macOS, watchOS, and tvOS. Uses HKCategorySample with HKCategoryType(.mindfulSession) and HKCategoryValue.notApplicable.rawValue

  3. Apple Developer, “HealthKit framework”. The framework’s two main sample types are HKQuantitySample (for measurable quantities like water, steps, calories) and HKCategorySample (for non-quantitative events like mindful sessions, sleep, menstrual flow). HKWorkout covers structured exercise. 

  4. Apple Developer, “Authorizing access to health data”. Apple intentionally hides read-denial state to prevent apps from inferring what data exists in a user’s Health profile. The authorizationStatus(for:) method only returns honest results for share access. 

  5. Apple Developer, “Apple Health icon usage” Human Interface Guidelines. Apple’s Health icon must not be modified, clipped, shadowed, or recolored; reproduce it at 1:1 fidelity in promotional and pre-permission UIs. 

  6. Production code in Return/Return/HealthKitPermissionSheet.swift (155 lines). Pre-permission View shown before triggering the system HealthKit dialog. Pairs the Return app icon with the Apple Health icon, explains the benefit, and triggers authorization via a callback closure on user tap. 

  7. Production code in Return/ReturnWatch Watch App/WatchHealthKitManager.swift (86 lines). The watchOS variant of HealthKitManager. Independent HKHealthStore, no hasRequestedHealthKit flag, no permission sheet plumbing. 

  8. Author’s analysis: HealthKit is the data-source layer of an iOS app, App Intents are the system-AI surface, MCP is the cross-LLM agent surface, Liquid Glass is the visual surface. The four layers compose into a single shipped product across all five Apple platforms. 

  9. Apple Developer, “Privacy manifest files”. HealthKit usage must be declared in PrivacyInfo.xcprivacy plus NSHealthShareUsageDescription and NSHealthUpdateUsageDescription keys in Info.plist

  10. Apple Developer, “App Review Guideline 5.1.1”. Apps must respect user privacy settings, request consent before collecting personal data, and cannot manipulate, trick, or force consent. The exact phrasing about priming-screen exit paths in this article reflects Return’s interpretation rather than the literal guideline text. 

관련 게시물

Liquid Glass in SwiftUI: Three Patterns From Shipping Return on iOS 26

Apple's Liquid Glass is a one-line SwiftUI API. Three patterns from Return go beyond .glassEffect(): glass on text via C…

17 분 소요

Five Apple Platforms, Three Shared Files: How Return Actually Ships Cross-Platform SwiftUI

Return runs on iPhone, iPad, Mac, Apple Watch, and Apple TV. Three Swift files are shared across all five targets out of…

18 분 소요

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 분 소요