HealthKit์ผ๋ก '์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๋' ๊ฑธ์ ์ ์์ ฏ ๋ง๋ค๊ธฐ
iOS ์์ ฏ์์ ๊ฑธ์ ์๋ฅผ '์ค์๊ฐ ์ฒ๋ผ' ๋ณด์ฌ์ฃผ๋ ๊ฒ์ ์๊ฐ๋ณด๋ค ๊ฐ๋จํ์ง ์๋ค.
์์ ฏ์ ํญ์ ์ผ์ ธ ์๋ ๋ทฐ๊ฐ ์๋๋ผ, ์์คํ
์ด ํ์ํ ๋๋ง ๊นจ์์ ์ค๋
์ท์ ๊ทธ๋ฆฌ๋ ๊ตฌ์กฐ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
์ด๋ฒ ๊ธ์์๋ ์๋์ ๋ด์ฉ์ ์ค์ ์ผ๋ก ์ ๋ฆฌํ๋ ค๊ณ ํ๋ค.
โ ์์ ฏ์ด ์ง์ง ์ค์๊ฐ์ด ๋ ์ ์๋ ์ด์
โก HealthKit + WidgetKit ๊ตฌ์กฐ ์ค๊ณ
โข HealthKit์์ ๊ฑธ์ ์ ์์ง/ํฉ์ฐํ๊ธฐ
โฃ ์์ ฏ ํ์๋ผ์ธ ๊ตฌ์ฑํ๊ธฐ(์ค๋ ๊ฑธ์ ์ ํ์)
โค ObserverQuery + background delivery๋ก '์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๊ฒ' ๋ง๋ค๊ธฐ
์์ ฏ์ด ์ง์ง ์ค์๊ฐ์ด ๋ ์ ์๋ ์ด์
- ์์ ฏ์ ํญ์ ์คํ๋๋ ํ๋ก์ธ์ค๊ฐ ์๋
- ํ๋ฉด์ ๋ณด์ด๋ ๋์์๋, ๋ด๋ถ์ ์ผ๋ก๋ ํ ๋ฒ ๋ ๋๋ง ํ ํ,
์์คํ
์ด ๋ค์ ๊ฐฑ์ ์ ํ์ฉํ ๋๋ง ์๋ก ๊ทธ๋ฆด ์ ์์
- WidgetKit์ reloadTimelines, reloadAllTimelines() ๋ฑ์ ํตํด '๊ฐฑ์ ์์ฒญ'์ ํ ์ ์์ง๋ง,
์ธ์ ์ค์ ๋ก ์๋ก ๊ทธ๋ฆด์ง๋ ์์คํ
์ด ํด๋ฆฌ์คํฑ์ผ๋ก ๊ฒฐ์ ํจ
์ํคํ ์ฒ ์ค๊ณ: HealthKit + WidgetKit
1. ๊ณต์ฉ HealthKit Manager (App + Extension์์ ๊ฐ์ด ์ฌ์ฉ)
- ๊ฑธ์ ์ ๊ถํ ์์ฒญ
- ์ค๋ ๊ฑธ์ ์ ํฉ์ฐ
- ObserverQuery ๋ฑ๋ก ๋ฐ background delivery
2. ๋ฉ์ธ ์ฑ
- ์ต์ด ๊ถํ ์์ฒญ ํ๋ฉด
- HealthKit Manager ์ด๊ธฐํ
- HealthKit ๋ณ๊ฒฝ ์ด๋ฒคํธ ์์ ์ → WidgetKit ๊ฐฑ์ ์์ฒญ
3. Widget Extension
- TimelineProvider ์์ ์ค๋ ๊ฑธ์ ์ ์กฐํ
- TimelineEntry ์ ๊ฑธ์ ์ + ๋ ์ง/์๊ฐ ๋ด๊ธฐ
- View ์ ํ์
// Shared (App + Widget์์ ๊ณต์ฉ์ผ๋ก ์ธ ์ ์๋ ๋ชจ๋)
final class HealthKitStepService {
// ๊ถํ ์์ฒญ
func requestAuthorization() async throws { ... }
// ์ค๋ ๊ฑธ์ ์ ํฉ์ฐ
func fetchTodayStepCount() async throws -> Double { ... }
// ObserverQuery + background delivery ์ค์
func startObservingStepChanges(callback: @escaping () -> Void) { ... }
}
// App
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let healthService = HealthKitStepService()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
healthService.startObservingStepChanges {
// HealthKit ๋ฐ์ดํฐ ๋ณ๊ฒฝ ๊ฐ์ง ์
WidgetCenter.shared.reloadAllTimelines()
}
return true
}
}
// Widget Extension
struct StepWidgetProvider: TimelineProvider {
func getTimeline(
in context: Context,
completion: @escaping (Timeline<StepEntry>) -> Void
) {
Task {
let steps = try? await HealthKitStepService.shared.fetchTodayStepCount()
let entry = StepEntry(date: Date(), steps: Int(steps ?? 0))
let next = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
}
HealthKit์์ ๊ฑธ์ ์ ๊ฐ์ ธ์ค๊ธฐ
โถ ๊ถํ ์์ฒญ
(info.plist์ ์ถ๊ฐ)
NSHealthShareUsageDescription
NSHealthUpdateUsageDescription
import HealthKit
final class HealthKitStepService {
static let shared = HealthKitStepService()
private let store = HKHealthStore()
private init() {}
private var stepType: HKQuantityType {
HKObjectType.quantityType(forIdentifier: .stepCount)!
}
// MARK: - Authorization
func requestAuthorization() async throws {
guard HKHealthStore.isHealthDataAvailable() else { return }
let readTypes: Set = [stepType]
// ๊ฑธ์ ์๋ ์ผ๋ฐ์ ์ผ๋ก write๋ ํ์ ์์(ํ์ํ๋ฉด ์ถ๊ฐ)
let shareTypes: Set<HKSampleType> = []
try await store.requestAuthorization(
toShare: shareTypes,
read: readTypes
)
}
}
โถ '์ค๋' ๊ฑธ์ ์ ํฉ์ฐ ์ฟผ๋ฆฌ
Widget๊ณผ App ์์ชฝ์์ ๊ณตํต์ผ๋ก ์ฌ์ฉํ ํจ์
extension HealthKitStepService {
func fetchTodayStepCount() async throws -> Double {
let calendar = Calendar.current
let now = Date()
let startOfDay = calendar.startOfDay(for: now)
var components = DateComponents()
components.day = 1
components.second = -1
let endOfDay = calendar.date(byAdding: components, to: startOfDay)!
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: endOfDay,
options: .strictStartDate
)
let queryDescriptor = HKSampleQueryDescriptor(
predicates: [
.quantitySample(type: stepType, predicate: predicate)
],
sortDescriptors: []
)
let samples = try await queryDescriptor.result(for: store) as! [HKQuantitySample]
let total = samples.reduce(0.0) { partial, sample in
partial + sample.quantity.doubleValue(for: HKUnit.count())
}
return total
}
}
์ค์ ์ฑ์์๋ HKStatisticsQuery๋ฅผ ์ฌ์ฉํด ํฉ์ฐํ๋ ๋ฐฉ์๋ ๋ง์ด ์ฌ์ฉํจ.
ํต์ฌ์ ์ค๋ ์์ ~23:59:59๊น์ง ๋ฒ์๋ฅผ ๋ช
ํํ ์ ์ํ๋ ๊ฒ.
Widget ํ์๋ผ์ธ ๊ธฐ๋ณธ ๊ตฌํ
- Entry ์ ์
import WidgetKit
struct StepEntry: TimelineEntry {
let date: Date
let steps: Int
}
- Provider ๊ตฌํ
struct StepProvider: TimelineProvider {
func placeholder(in context: Context) -> StepEntry {
StepEntry(date: Date(), steps: 0)
}
func getSnapshot(in context: Context, completion: @escaping (StepEntry) -> Void) {
let entry = StepEntry(date: Date(), steps: 1234)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<StepEntry>) -> Void) {
Task {
let steps = (try? await HealthKitStepService.shared.fetchTodayStepCount()) ?? 0
let entry = StepEntry(date: Date(), steps: Int(steps))
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
}
์ฌ๊ธฐ๊น์ง๋ 15๋ถ๋ง๋ค ํ ๋ฒ ์ ๋ ์ค๋ ๊ฑธ์ ์๋ฅผ ํ์ํ๋ ์์ ฏ์ด๋ค.
์ด์ ๋ถํฐ '์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๊ฒ' ๋ง๋ค๊ธฐ ์ํด HealthKit์ Observer ๊ธฐ๋ฅ์ ๋ถ์ผ ์์ ์ด๋ค.
ObserverQuery + Background Delivery๋ก '์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๊ฒ' ๋ง๋ค๊ธฐ
- HealthKit ๋ณ๊ฒฝ ๊ฐ์ง
๊ฑธ์ ์๋ Health ์ฑ/์์น/๊ธฐ๊ธฐ์์ ๋์์์ด ์ถ๊ฐ๋๋ค.
HealthKit์ ์ด ๋ณํ๋ฅผ ๊ฐ์งํ ์ ์๋๋ก ObserverQuery์ background delivery๋ฅผ ์ ๊ณตํ๋ค.
extension HealthKitStepService {
func startObservingStepChanges(callback: @escaping () -> Void) {
let query = HKObserverQuery(sampleType: stepType, predicate: nil) { [weak self] _, completionHandler, error in
if let error {
print("ObserverQuery Error: \(error)")
completionHandler()
return
}
// ์ฌ๊ธฐ์ anchor ๊ธฐ๋ฐ AnchoredObjectQuery๋ฅผ ๊ฐ์ด ๋๋ ค๋ ๋จ
callback()
// ๋ฐ๋์ ํธ์ถํด์ผ background mode์์ ๋ค์ ๊นจ์ฐ๊ธฐ
completionHandler()
}
store.execute(query)
// Background delivery ์ค์
store.enableBackgroundDelivery(for: stepType, frequency: .immediate) { success, error in
if let error {
print("Background delivery error: \(error)")
} else {
print("Background delivery enabled: \(success)")
}
}
}
}
์์ ๊ฐ์ด ์ค์ ํด๋๋ฉด,
์์น/์์ดํฐ์ด ๊ฑธ์ ๋ฐ์ดํฐ๋ฅผ HealthKit์ ์์ ๋๋ง๋ค ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๊นจ์ฐ๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ์ ์๋ค.
์์ ฏ ๊ฐฑ์ ์์ฒญ ์ฐ๊ฒฐ
Observer ์ฝ๋ฐฑ์์ WidgetKit ๊ฐฑ์ ์์ฒญ์ ํธ์ถํ๋ค.
import WidgetKit
final class StepWidgetRefresher {
static let shared = StepWidgetRefresher()
func requestReload() {
WidgetCenter.shared.reloadAllTimelines()
}
}
์ฑ ๊ตฌ๋ ์,
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let healthService = HealthKitStepService()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
healthService.startObservingStepChanges {
StepWidgetRefresher.shared.requestReload()
}
return true
}
}
ํ๋ฆ์ ์๋์ ๊ฐ๋ค.
- ์์น/ํฐ์์ ์๋ก์ด ๊ฑธ์ ๋ฐ์ดํฐ๊ฐ HealthKit์ ๊ธฐ๋ก๋๋ค.
- HealthKit → ObserverQuery ์ฝ๋ฐฑ ํธ์ถ (๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐ๋ฅ)
- ์ฝ๋ฐฑ์์ WidgetCenter.shared.reloadAllTimelines() ํธ์ถ
- ์์คํ ์ด ํ์ฉํ๋ ์์ ์ ์์ ฏ์ด ๋ค์ ๊ทธ๋ ค์ง
- getTimeline ์ด ๋ค์ ์คํ๋๋ฉด์ fetchTodayStepCount()๋ฅผ ํธ์ถ
- ์๋ก ํฉ์ฐ๋ ๊ฑธ์ ์๊ฐ ์์ ฏ์ ๋ฐ์
์์ ฏ์ด 1์ด ๋จ์๋ก ๊ณ์ ๋ฐ๋์ง๋ ์์ง๋ง,
๋ฐ์ดํฐ๊ฐ ๊ฐฑ์ ๋ ๋๋ง๋ค ๊ฝค ๋น ๋ฅด๊ฒ ๋ฐ๋ผ๊ฐ๋ ํํ๋ก ๋ง๋ค ์ ์๋ค.
'์ค์๊ฐ์ฒ๋ผ' ๋ณด์ด๊ฒ ํ๊ธฐ ์ํ ํ๋ ํฌ์ธํธ
- ํ์๋ผ์ธ ์ ์ฑ
์ค๊ณ
getTimeline ์ policy ๋ฅผ ๋๋ฌด ๊ธธ๊ฒ ์ก์ผ๋ฉด,
Observer ์ชฝ์์ reload ์์ฒญ์ ํด๋ ๋ฐ์์ด ๋ฆ๊ฒ ๋๊ปด์ง ์ ์๋ฐ.
์ผ๋ฐ์ ์ธ ํจํด์
- ๊ธฐ๋ณธ ํ์๋ผ์ธ์ policy: .after(Date().addingTimeInterval(60*30)) ์ ๋๋ก ์ฌ์ ์๊ฒ
- ObserverQuery ์ด๋ฒคํธ ๋ฐ์ ์์๋ ์ฆ์ reload ์์ฒญ์ ์์กด
์ด๋ ๊ฒ ํ๋ฉด,
- ์ฌ์ฉ์๊ฐ ์์ ฏ์ ๊ฑฐ์ ์๋ณด๋ ์ํฉ์์๋ ์์คํ ์ด ์์์ ๋ ์์ฃผ ๊ฐฑ์ ํ๊ณ
- HealthKit ๋ฐ์ดํฐ๊ฐ ์์ฃผ ๋ฐ๋ ๋๋ Observer๋ฅผ ํตํด ์ข ๋ ์์ฃผ ๊ฐฑ์ ์์ฒญ์ ํ๊ฒ ๋๋ค.
- ์์ ฏ ํฌ๊ธฐ(๊ฐ๋ก/์ธ๋ก)๋ณ UI ์ค๊ณ
์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๊ฒ ํ๋ ค๋ฉด, ์ซ์๊ฐ ๋ฐ๋๋ ๋๋์ ๊ฐํํ๋ ๊ฒ๋ ์ค์ํ๋ค.
- small ์์ ฏ
- '์ค๋ ๊ฑธ์ ์' 1์ค + ํฐ ์ซ์
- ์ต๊ทผ ์ ๋ฐ์ดํธ ์๊ฐ(ex. ์ต๊ทผ 2๋ถ ์ ) ์์ ํ ์คํธ
- Medium/Large ์์ ฏ
- ์ค๋ ์ด ๊ฑธ์ ์
- ์๊ฐ๋๋ณ ๋ง๋ ๊ทธ๋ํ (๋ฏธ๋ ์ฐจํธ)
- '๋ชฉํ ๋๋น%' ๊ฒ์ด์ง
์ด๋ ๊ฒ ํ๋ฉด, ์ฌ์ฉ์ ์ ์ฅ์์๋ ์ซ์๊ฐ ์์ฃผ ๋ฐ๋๋ค๋ ์ธ์์ด ์๊ธฐ๋ฉด์ ์ค์๊ฐ ์ฒ๋ผ ๋ณด์ด๊ฒ ๋๋ค.
- ๊ถํ/์๋ฌ ์ํ ์ฒ๋ฆฌ
HealthKit ๊ธฐ๋ฐ ์์ ฏ์ ์๋์ ๊ฐ์ ์ํ๋ฅผ ๊ณ ๋ คํด์ผ ํ๋ค.
- ๊ถํ ๋ฏธ๋ถ์ฌ(์ฒ์ ์ค์น ํ)
- ์ค๋ ๋ฐ์ดํฐ ์์
- ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ๋ฌ ๋นํ์ฑํ(์ฌ์ฉ์๊ฐ ๋ ๊ฒฝ์ฐ)
- ์์น ๋ฏธ์ฐ๋/์์ดํฐ๋ง ๋ค๊ณ ์์
๊ฐ ์ํ์ ๋ํด ์์ ฏ์์
- Health ๋ฐ์ดํฐ ์ ๊ทผ ๊ถํ์ด ์์ต๋๋ค.
- ์์ง ๊ฑธ์ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค
๋ฑ์ ๋ฌธ์์ด์ ๋ณ๋๋ก ๋ ธ์ถํด์ฃผ๋ฉด UX ๊ฐ์ ์ด ๋ ๊ฒ ๊ฐ๋ค.
๋์ผ๋ก,
HealthKit ๊ธฐ๋ฐ์ ๊ฑธ์ ์ ์์ ฏ์ ๊ตฌ์กฐ์ ์ผ๋ก ์์ ํ ์ค์๊ฐ ๊ฐฑ์ ์ด ๋ถ๊ฐ๋ฅํ์ง๋ง,
ObserverQuery·background delivery·ํ์๋ผ์ธ ์ ์ฑ
์ ์ ์ ํ ์กฐํฉํ๋ฉด
์ฌ์ฉ์ ์
์ฅ์์๋ '์ค์๊ฐ์ฒ๋ผ ๋ฐ์ํ๋ ์์ ฏ'์ ์ถฉ๋ถํ ๊ตฌํํ ์ ์์๋ค.
๊ฒฐ๊ตญ ํต์ฌ์,
- HealthKit์ ๋ณํ๋ฅผ ๋น ๋ฅด๊ฒ ๊ฐ์งํ๊ณ
- ๊ฐ๋ฅํ ํ ์ฆ์ WidgetKit ๊ฐฑ์ ์ ์์ฒญํ๋ฉฐ
- ์์ ฏ UI์ ํ์๋ผ์ธ์ “์ค์๊ฐ์ ๊ฐ๊น๊ฒ ๋ณด์ด๋๋ก” ์ค๊ณํ๋ ๊ฒ
์ด๋ฒ ๊ธ์์๋ ์ด๋ฌํ ๊ตฌ์กฐ๋ฅผ ์ค์ ์ฝ๋ ๊ธฐ๋ฐ์ผ๋ก ํ๋์ฉ ๊ตฌํํด๋ณด๋ฉฐ
'์ค์๊ฐ์ฒ๋ผ ๋ณด์ด๋ ๊ฑธ์ ์ ์์ ฏ'์ด ์ด๋ค ๋ฐฉ์์ผ๋ก ์์ฑ๋๋์ง ํ๋ฆ์ ์ ๋ฆฌํด๋ณด์๋ค.
HealthKit๊ณผ WidgetKit์ ํน์ฑ์ ์ดํดํ๊ณ ๋๋ฉด,
์์ ฏ์ ๋จ์ํ ํ์์ฉ ์์๊ฐ ์๋
๊ธฐ๊ธฐ ๋ฐ์ดํฐ์ ์์ฐ์ค๋ฝ๊ฒ ์ฐ๋๋๋ ํ๋์ UI ์ปดํฌ๋ํธ๋ก ๋ฐ๋ผ๋ณผ ์ ์์ ๊ฒ์ด๋ค.
๊ธฐํ๊ฐ ๋๋ค๋ฉด, ๊ฑธ์ ์ ๊ทธ๋ํ, ๋ชฉํ ๋๋น UI, ๋ค์ํ ์์ ฏ ํฌ๊ธฐ ๋์ ๋ฑ
UIโ๋ฐ์ดํฐ ํํ ํ์ฅ์ ๋ํ ๋ด์ฉ๋ ๋ณ๋๋ก ์ ๋ฆฌํด๋ณผ ์๊ฐ์ด๋ค.
'๐' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [Swift] iOS WidgetKit์ ๊ฐ๋ฅ ๋ฒ์์ ํ๊ณ: ์ค์๊ฐ์ฑ, ๋ฐ์ดํฐ ๊ฐฑ์ ์ ๋ต (0) | 2025.12.14 |
|---|---|
| [Swift] D-Day ์์ ฏ ๋ง๋ค๊ธฐ (2) (0) | 2025.12.01 |
| [Swift] D-Day ์์ ฏ ๋ง๋ค๊ธฐ (1) (0) | 2025.11.28 |
| [iOS] WKWebView์์์ ์คํฌ๋กคโBounceโSafeArea ๋์ (0) | 2025.11.17 |
| [Swift] Swift Concurrency ์์ ์ ๋ณต (0) | 2025.11.11 |