HealthKit (2)
HealthKit Permission 요청하기
HealthKit GuideLine을 한번 읽어보도록 하자
디자인은 생략.
한가지 중요한 점이 있다면. 해당 앱을 사용하는 Device가 HealthKit을 지원하는지 반드시 체크해야한다.
When you enable the HealthKit capabilities on an iOS app, Xcode adds HealthKit to the list of required device capabilities, which prevents users from purchasing or installing the app on devices that don’t support HealthKit.
Docs에 해당 관련 문구가 있으니 읽어볼 것.
HealthKit 추가
우선 HealthKit을 사용하기 위해
이렇게 추가를 해준다.
Info.Plist에 추가
Docs에 나와있다.
이걸 기반으로 추가해본다.
이전에 App Store에 앱을 출시할때 카메라나 지도 이런부분에 있어 유저의 동의를 얻는것처럼 HealthKit역시 마찬가지, 이걸하지 않으면 Reject은 100% 라고 봐도된다.
왜냐 이것도 일종의 개인정보이기에 반드시 동의를 받아야 하기 때문.
HealthKitManager 기본구현
새롭게 파일을 만들어준다.
1
2
3
4
5
6
7
import HealthKit
import Observation
@Observable class HealthKitManager {
let store = HKHealthStore()
}
이때 특이점은 바로 Observation을 import 한것인데,
이걸 사용함으로써 클래스 자체를 관찰 가능(Observable)한 상태로 만든다.
- @Observable로 선언하면 클래스 내부의 속성 변경이 SwiftUI 뷰에 실시간으로 반영된다.
- 코드 간결성과 효율성을 높여 기존 @Published 속성과 ObservableObject를 사용하는 방식을 대체한다.
- @Observable로 선언된 클래스는 내부의 모든 속성이 자동으로 관찰 가능하다. 이를 통해 상태 변화가 앱 전반에 걸쳐 쉽게 반영된다.
다만, Observation은 iOS 17 이상에서만 지원된다.
HealthKitManager는 앱의 글로벌 상태 관리 객체로 사용된다. 이를 @Environment를 통해 SwiftUI 뷰 계층에 주입하여 어디서든 쉽게 접근할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
@main
struct Step_TrackerApp: App {
let hkManager = HealthKitManager()
var body: some Scene {
WindowGroup {
DashboardView()
.environment(hkManager)
}
}
}
이렇게 앱자체애서 사용하게 한다 이떄 environment 내부 파라미터는 반드시 Observable이어야한다. 그래서 위의 클래스를 저렇게 표현한것.
@Environment
로 hkManager를 전달하여 SwiftUI 뷰 계층에서 공유 가능.- DashboardView와 그 하위 뷰에서
@Environment
를 통해 쉽게 접근할 수 있다. @Observable
덕분에 hkManager의 상태 변화는 UI에 자동으로 반영된다
HealthKitPermissionPrimingView에서 유저의 동의 얻기
먼저 HealthKitUI
를 import!
주의하자 HealthKit
이 아니다.
이렇게 Vstack의 Modifier로 만들건데, 우리는 Reading/Writing을 전부 다하므로 사진에 있는 저것을 선택해서 만들것이다.
그리고 parameter로 값을 넣기전 위에서 environment로 만든 hkManager를 사용한다.
@Environment(HealthKitManager.self) private var hkManager
이렇게.
store에는 우리가 처음에 위에서 만들어둔 manager의 store를 그대로 적용 하면되고,
shareTypes
- 정의: 앱이 사용자 데이터를 HealthKit에 저장(쓰기)할 때 필요한 데이터 유형.
- 예시: 사용자가 앱을 통해 입력한 몸무게를 HealthKit에 저장하려는 경우
HKQuantityType(.bodyMass)
를shareTypes
에 포함. - 특징: 사용자가 데이터를 수정하거나 업데이트할 수 있는 권한을 요청.
- Docs: Docs
readTypes
- 정의: 앱이 HealthKit으로부터 사용자 데이터를 읽어올 때 필요한 데이터 유형.
- 예시: 앱에서 사용자의 걸음 수 데이터를 읽으려면
HKQuantityType(.stepCount)
를readTypes
에 포함. - 특징: 사용자의 동의 하에 데이터를 읽어와 분석 및 표시 가능.
- Docs: Docs
trigger: alert와 유사, bool값이 true일때 동의를 얻는 화면이 작동
@State private var isShowingHealthKitPermission = false
이런식으로 만들어서 사용한다.
HealthKit Type 만들기
여기서 쓰이는 Type이 바로 위에 언급한 shareTypes와 readTypes에 들어간다.
1
let types: Set = [HKQuantityType(.stepCount), HKQuantityType(.bodyMass)]
우린 걸음걸이와 몸무게를 사용하기에 두개를 Set의 배열로 담기로 했다.
- 의미:
HKQuantityType
을 사용해 공유(shareTypes
) 및 읽기(readTypes
)가 필요한 데이터 유형을 정의.
HKQuantityType(.stepCount)
- 사용자가 하루 동안 걸은 총 걸음 수를 나타내는 데이터 유형.
HKQuantityType(.bodyMass)
- 사용자의 몸무게 정보를 나타내는 데이터 유형.
Set
사용 이유
- HealthKit 데이터 타입은 중복되지 않으므로
Set
자료구조를 사용하여 각 데이터 타입을 고유하게 관리. Set
은 삽입 순서와 상관없이 빠른 검색과 중복 제거가 가능.
HKQuantityType에는 상당히 많은게 있으므로 나중에 한번 읽어보자.
코드 작성
이때 화면의 창을 내리기위해서
@Environment(\.dismiss) private var dismiss
를 사용.
1
2
3
4
5
6
7
8
9
10
11
.healthDataAccessRequest(store: hkManager.store,
shareTypes: hkManager.types,
readTypes: hkManager.types,
trigger: isShowingHealthKitPermission) { result in
switch result {
case .success(let success):
dismiss()
case .failure(let failure):
dismiss()
}
}
지금은 result에 상관없이 모두 dismiss하게 되어있다.
앱 시작 시 동의 얻기
앱이 실행될 때 HealthKit 사용에 대한 동의를 얻는 과정을 구현한다. 주요 포인트는 @AppStorage
를 사용하여 사용자 동의 상태를 저장하고, onAppear
에서 이를 트리거로 활용하는 것이다.
Docs는 여기
@AppStorage
로 사용자 동의 상태를 저장한다.
@AppStorage
란?
- 사용자 설정을
UserDefaults
에 저장하고 읽을 수 있도록 해주는 SwiftUI Property Wrapper이다. - 간단하게 키-값 형태로 데이터를 저장하고 UI와 동기화 가능.
@AppStorage("hasSeenPermissionPriming") private var hasSeenPermissionPriming = false
이렇게 Key값을 괄호 안에 넣어준다.
그리고 HealthKit 사용 허용에 관한 내용이 모달 형식으로 올라오는 상태를 알려줄
@State private var isShowingPermissionPrimingSheet = false
변수도 하나 만들어 준다.
그다음 DashBoardView의 ScollView의 Modifer로
1
2
3
4
5
.sheet(isPresented: $isShowingPermissionPrimingSheet, onDismiss: {
// fetch health data
}, content: {
HealthKitPermissionPrimingView()
})
이렇게 코드를 작성 즉 트리거 되었을때 primingView를 띄우겠다는것. 아직 onDismiss에는 내용을 담지는 않았다.
PrimingView 수정
PermissionPrimingView의 경우 앱을 설치후 실행할때 최초 1회만 나오는 화면이므로
PrimingView에서
@Binding var hasSeen: Bool
봤는지 안봤는지의 여부를 판단할 변수를 만들어 준다.
그리고 onAppear를 통해 한번 봤으면 끝이기에
1
.onAppear { hasSeen = true }
이렇게 true로 돌려준다.
DashBoardView 수정
그리고 다시 DashboardView로 와서
1
2
3
content: {
HealthKitPermissionPrimingView(hasSeen: $hasSeenPermissionPriming)
})
바인딩한 값을 전달하는데 초기에는 hasSeenPermissionPriming = false가 넘어가서 hasSeen이 false이므로 화면이 나오게 될것이다.
그리고 DashBoardView에서 onAppear를 통해 화면을 트리거할것인데,
1
2
3
.onAppear {
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
hasSeenPermissionPriming이 false라면 사용자가 아직 동의를 하지 않은 상태.
!hasSeenPermissionPriming으로 값이 반전되어 isShowingPermissionPrimingSheet가 true로 설정.
그러면 화면이 나오게 될것이다.
PrimingView 드래그로 화면 내려가지않게 수정
해당 화면은 사용자가 필수 작업(HealthKit 동의)을 완료하지 않은 상태에서 해당 화면을 닫는 일이 발생하면 안된다.
그래서 PrimingView VStack의 Modifier로
.interactiveDismissDisabled()
을 추가해주면 된다.
화면 작동의 기본적인 매커니즘 - Priming View와 @AppStorage
의 연동
1. @AppStorage
로 사용자 동의 상태 저장
1
@AppStorage("hasSeenPermissionPriming") private var hasSeenPermissionPriming = false
@AppStorage("hasSeenPermissionPriming")
를 통해UserDefaults
에 사용자 동의 상태를 저장한다.- 초기값은
false
로 설정되어 있다 - 따라서 앱이 처음 실행될 때는 hasSeenPermissionPriming 값이 false로 반환된다.
- 사용자가 Priming View를 본 이후
true
로 변경된다.
2. onAppear
로 Priming View 트리거
1
2
3
.onAppear {
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
DashboardView
의onAppear
에서hasSeenPermissionPriming
값을 읽는다.hasSeenPermissionPriming
이false
인 경우 Priming View를 보여주는 Sheet가 트리거된다.- 이로 인해 앱이 처음 실행될 때만 Priming View가 나타난다.
3. Priming View에서 상태 변경
1
2
3
4
5
@Binding var hasSeen: Bool
.onAppear {
hasSeen = true
}
- Priming View가 표시될 때
onAppear
를 사용하여hasSeen
값을true
로 변경한다. hasSeen
은DashboardView
의hasSeenPermissionPriming
과 바인딩되어 있으므로, 해당 값이true
로 업데이트된다.- 바인딩 되어있기에, 부모 뷰(DashboardView)의 hasSeenPermissionPriming과 값을 공유한다.
4. 이후 동작
1
2
3
.sheet(isPresented: $isShowingPermissionPrimingSheet, content: {
HealthKitPermissionPrimingView(hasSeen: $hasSeenPermissionPriming)
})
- Priming View가 닫힌 후,
hasSeenPermissionPriming
값이true
로 유지된다.- 바인딩이 되어있기 때문
- @AppStorage를 통해 true로 변경된 값이 UserDefaults에 저장된다.
- 앱을 재실행하거나 다시 대시보드로 돌아와도
hasSeenPermissionPriming
이true
이기 때문에 더 이상 Priming View가 나타나지 않는다.
실행화면
다른 강의 수강생들의 여러 의견
강의를 보고 댓글을 보다 흥미로운 점들이 좀 있어서 정리를 해본다.
1. sheet 대신 fullScreenCover 사용
현재 DashboardView에서 처음에 앱을 설치하면 sheet를 통해 PrimingView가 나타나게 된다.
그래서 드래그를 방지하기위해 우리는 PrimingView에 .interactiveDismissDisabled()
을 추가해주었다.
다른방법으로 추천을한건 똑같이 아래에서 모달로 올라오되 애초에 드래그가 불가능한 전체화면으로 보여주는 것이다.
이건 선택지이니 취향껏 하면 될듯.
2. AppStorage 사용 ❌
이건 내용이 좀 길다.
현재 코드의 문제점
- 권한 상태와 동기화되지 않음:
- 사용자가 iOS 설정에서 권한을 재설정하면 앱의 @AppStorage 값과 실제 권한 상태가 불일치할 수 있다.
- 이로 인해 권한 요청 플로우가 오작동하거나 앱이 비정상적으로 작동할 수 있다.
- 사용자가 권한을 재설정했을 때의 처리 부족:
- 사용자가 위치 및 개인정보 보호 재설정을 통해 권한 상태를 초기화하면 앱은 이를 알지 못하고 잘못된 상태를 유지할 수 있다. - 예: 사용자가 HealthKit 권한 요청 화면에서 앱을 종료하거나, 설정 > 개인정보 보호를 초기화하면 앱의 상태와 실제 권한 상태가 동기화되지 않을 수 있다. - 최악의 경우, 앱 재설치로도 문제를 해결할 수 없게 되어 사용자가 앱을 포기할 가능성이 높아진다. - 권장하지 않는 방식:
- Keychain에 요청 상태를 저장.
- 수동적으로 요청 상태를 기록하는 플래그 사용.
1
@AppStorage("hasSeenPermissionPriming") private var hasSeenPermissionPriming = false
- @AppStorage를 사용하여 UserDefaults에 hasSeenPermissionPriming 키를 저장하고 있다.
- 이 값은 사용자가 HealthKit 권한 요청 화면(Priming View)을 본 적이 있는지를 나타낸다.
- 초깃값은 false이며, 사용자가 Priming View를 본 후 true로 업데이트된다.
1
2
3
.onAppear {
isShowingPermissionPrimingSheet = !hasSeenPermissionPriming
}
- hasSeenPermissionPriming 값이 false일 경우, Priming View가 표시된다.
- Priming View가 표시된 이후, 해당 값을 true로 설정하여 다시 표시되지 않도록 한다.
대안 (getRequestStatusForAuthorization API) ✅
Apple은 HealthKit 권한 상태를 확인하는 전용 API를 제공한다:
- 이 API는 권한 요청 상태를 자동으로 관리하며, 아래 세 가지 상태 중 하나를 반환한다.
- .unknown: 오류 상황 발생.
- .shouldRequest: 최소 하나 이상의 데이터 유형에 대해 권한 요청이 필요함.
- .unnecessary: 사용자가 이미 권한 요청을 완료했으며 추가 요청이 불필요함.
이 API를 사용하면 권한 요청이 필요한 경우에만 정확하게 트리거할 수 있다.
- .shouldRequest 상태일 때 요청을 보내면 시스템에서 권한 요청 화면을 표시.
- .unnecessary 상태일 때 요청을 보내면 화면을 표시하지 않고 종료.
- 관련 문서: Docs, 권한 요청
이부분은 나중에 한번 수정을 해보는걸로…
Github: Step-Tracker Repository