Conversation
|
Note
|
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | |
| Description check | ❓ Inconclusive | Description includes the related issue (#1174) and explains the implementation approach, though the main work content and key changes sections are incomplete per the template. |
Please complete the 작업 내용 (Work Content) and 주요 변경사항 (Key Changes) sections following the template structure for clarity. |
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Title check | ✅ Passed | Title clearly describes the main change: iOS HealthKit integration. It directly relates to the primary objective of connecting the iOS Health app. |
| Linked Issues check | ✅ Passed | The PR implements iOS HealthKit integration with automatic calorie update notifications, matching issue #1174's objective to enable server requests from auto-invoked Swift functions. |
| Out of Scope Changes check | ✅ Passed | All changes align with iOS HealthKit integration: HealthKitManager implementation, entitlements setup, UI components, and KoinHelper for server communication are all in scope. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
- 📝 Generate docstrings (stacked PR)
- 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
feat/1174
📝 Coding Plan
- Generate coding plan for human review comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
Tip
CodeRabbit can generate a title for your PR based on the changes with custom instructions.
Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (6)
mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/setting/cups/component/CupsTopAppBar.kt (1)
50-55:CenterAlignedTopAppBar와topAppBarColors()간의 API 불일치현재 컴포넌트는
CenterAlignedTopAppBar(Line 32)를 사용하고 있지만, 색상 설정에는TopAppBarDefaults.topAppBarColors()를 사용하고 있습니다.Material 3 API 설계상:
CenterAlignedTopAppBar→TopAppBarDefaults.centerAlignedTopAppBarColors()TopAppBar(standard) →TopAppBarDefaults.topAppBarColors()두 함수 모두
TopAppBarColors타입을 반환하므로 현재 동작에는 문제가 없습니다. 그러나 이러한 불일치는 다음과 같은 잠재적 문제를 야기할 수 있습니다:
- 코드 가독성 저하: 다른 개발자가 코드를 읽을 때 의도를 파악하기 어려움
- 향후 호환성: Material 3 라이브러리 업데이트 시 두 함수의 기본값이 달라질 경우 예상치 못한 UI 변경 발생 가능
의도적으로 표준 TopAppBar 색상을 적용하려는 것이 아니라면, 원래의
centerAlignedTopAppBarColors()를 유지하는 것이 권장됩니다.♻️ API 일치를 위한 수정 제안
colors = - TopAppBarDefaults.topAppBarColors( + TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = White, titleContentColor = Gray400, navigationIconContentColor = Gray400, ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/setting/cups/component/CupsTopAppBar.kt` around lines 50 - 55, The component uses CenterAlignedTopAppBar but sets colors via TopAppBarDefaults.topAppBarColors; change the colors call to TopAppBarDefaults.centerAlignedTopAppBarColors(...) so the API intent matches (keep the same parameters: containerColor = White, titleContentColor = Gray400, navigationIconContentColor = Gray400) and update any import if needed; locate the usage in CupsTopAppBar.kt where CenterAlignedTopAppBar is defined and replace topAppBarColors with centerAlignedTopAppBarColors.mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/component/HealthSection.ios.kt (1)
41-49: 클릭 영역에 접근성 역할(role) 명시를 권장합니다.현재
noRippleClickable이 접근성 semantics를 충분히 전달하지 않으면 VoiceOver에서 버튼으로 읽히지 않을 수 있습니다.
대안:
Modifier.clickable(role = Role.Button, ...)사용: 기본 접근성 지원이 명확함(장점), 기존 커스텀 효과와 동작 차이가 날 수 있음(단점).noRippleClickable유지 +semantics { role = Role.Button }추가: 기존 UX 유지(장점), 확장 함수에 의존해 일관성 관리 필요(단점).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/component/HealthSection.ios.kt` around lines 41 - 49, The Row currently uses Modifier.noRippleClickable(onClick = { onClick() }) which may not expose a button role to VoiceOver; update the modifier to explicitly declare accessibility role by either replacing noRippleClickable with Modifier.clickable(role = Role.Button, onClick = { onClick() }) or keep noRippleClickable and append semantics { role = Role.Button } to the Modifier chain so the Row (and its onClick handler) is announced as a button to assistive technologies.mobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.kt (1)
16-23:startKoin재호출 방어를 추가하는 편이 안전합니다.Line 20의
startKoin은 앱 재초기화 경로(예: 프리뷰/특정 재진입)에서 중복 호출 시 예외로 이어질 수 있습니다.
GlobalContext.getOrNull()체크 후 미초기화일 때만startKoin하는 방식이 안정적입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.kt` around lines 16 - 23, The initKoin function currently calls startKoin unconditionally; to prevent exceptions on re-initialization, check GlobalContext.getOrNull() and only call startKoin when it returns null. Update the initKoin implementation (function initKoin) to guard the startKoin call using GlobalContext.getOrNull(), preserving the modules(iosSharedModule(baseUrl, isDebug)) behavior when actually initializing.mobile/iosApp/iosApp/Health/HealthKitManager.swift (1)
48-64: ObserverQuery 중복 등록 방어를 권장합니다.
requestAuthorization이 다시 호출되면 Line 63에서 query가 중첩 실행될 수 있습니다.
isObserving플래그 또는 query 참조를 보관해 중복 실행을 막으면, 중복 콜백/중복 서버 호출 리스크를 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift` around lines 48 - 64, startObserverQuery currently creates and executes a new HKObserverQuery every time it runs, which can cause duplicate observer callbacks when requestAuthorization is called again; add a guard using an isObserving Bool or store the HKObserverQuery instance (e.g., a private var observerQuery: HKObserverQuery?) and check it before creating/executing a new query in startObserverQuery, set isObserving = true (or assign observerQuery) after successfully executing via healthStore.execute(query), and clear/reset these when you stop observing or on deinit to prevent duplicate registrations.mobile/iosApp/iosApp/ContentView.swift (1)
33-35: 매번 새로운KoinHelper()인스턴스를 생성하는 것은 비효율적입니다.
onBurnedCalorieUpdated콜백은 HealthKit 백그라운드 업데이트가 발생할 때마다 호출될 수 있습니다. 현재 구현에서는 매 호출 시 새로운KoinHelper인스턴스를 생성하고 있는데, 이는 불필요한 객체 할당을 유발합니다.개선 방안:
KoinHelper인스턴스를ComposeView프로퍼티로 캐싱하거나,KoinHelper를 Kotlin 측에서object(싱글톤)로 변경하는 것을 권장합니다.♻️ 개선 예시
struct ComposeView: UIViewControllerRepresentable { let loginPlatform = LoginPlatform() let appVersion = (Bundle.main.infoDictionary?["MARKETING_VERSION"] as? String) ?? "" + let koinHelper = KoinHelper() func makeUIViewController(context: Context) -> UIViewController { // ... onBurnedCalorieUpdated: { kcal in - KoinHelper().postActiveCaloriesBurned(kcal: kcal) + self.koinHelper.postActiveCaloriesBurned(kcal: kcal) },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/iosApp/iosApp/ContentView.swift` around lines 33 - 35, 현재 onBurnedCalorieUpdated 콜백에서 매번 새로운 KoinHelper()를 생성하고 있어 불필요한 객체 할당이 발생합니다; 해결책은 KoinHelper 인스턴스를 ComposeView(또는 ContentView) 프로퍼티로 캐싱하거나 KoinHelper를 Kotlin 측에서 싱글톤(object)으로 변경하는 것입니다; 구체적으로 ContentView.swift의 onBurnedCalorieUpdated 콜백 호출부를 찾아 KoinHelper() 호출을 제거하고 미리 생성해 둔 프로퍼티(예: private let koinHelper = KoinHelper())를 사용하거나, Kotlin에서 object KoinHelper로 변경해 호출을 KoinHelper.postActiveCaloriesBurned(kcal:)로 바꿔 적용하세요.mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/HealthKitPlatform.kt (1)
23-25: Health 앱을 열 수 없을 때 알림 설정으로 이동하는 것은 사용자 경험상 혼란을 줄 수 있습니다.
canOpenURL(healthUrl)이false를 반환하는 경우는 주로 Health 앱이 기기에서 사용 불가능한 상황(예: 시뮬레이터, 특정 기기)입니다. 이때 알림 설정 화면으로 이동하는 것은 사용자가 기대하는 동작이 아닐 수 있습니다.대안을 고려해 보세요:
- 아무 동작도 하지 않고 사용자에게 안내 메시지 표시 - 가장 명확한 사용자 경험을 제공하지만, 현재 구조에서 UI 피드백 전달 방법이 필요합니다.
- 시스템 설정의 Health 섹션으로 이동 -
App-Prefs:root=HEALTHURL 스킴 사용 (단, 비공개 API라 App Store 심사에서 문제될 수 있음).- 현재 방식 유지 - 만약 이 fallback이 의도적으로 "권한 관련 설정으로 안내"하는 목적이라면 주석으로 의도를 명시해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/HealthKitPlatform.kt` around lines 23 - 25, In HealthKitPlatform, when canOpenURL(healthUrl) returns false, don't silently fall back to openAppNotificationSettings(); instead implement a clear, explicit fallback: either call a new helper like showHealthUnavailableAlert() to present a user-facing message (or propagate an error callback) indicating Health app is unavailable, or if you intend the notification-settings fallback to be a deliberate permission-related guide, leave the fallback but add a clarifying comment above openAppNotificationSettings() describing that intent; update references in HealthKitPlatform (the canOpenURL(healthUrl) branch) accordingly and add any small helper (e.g., showHealthUnavailableAlert or an error callback) so the UI can show feedback rather than unexpectedly opening notification settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift`:
- Around line 16-37: Add a pre-check for HealthKit availability before calling
healthStore.requestAuthorization by using HKHealthStore.isHealthDataAvailable()
in requestAuthorization() (returning completion(false) immediately if
unavailable) so you don’t prompt unsupported devices; also update
enableBackgroundDelivery() to handle its completion callback (surface/log errors
and consider retry behavior) instead of ignoring result/error so background
delivery failures are observable; locate these changes around the
requestAuthorization(), enableBackgroundDelivery(), and startObserverQuery()
calls to implement the availability guard and proper error handling.
- Around line 40-45: enableBackgroundDelivery의
enableBackgroundDelivery(completion:) 콜백에서 success와 error를 무시하고 있어 실패를 알 수 없으니,
healthStore.enableBackgroundDelivery(for:activeEnergyType, frequency: .hourly,
withCompletion:)의 completion 파라미터에서 success와 error를 검사하도록 변경하고 실패 시(1) OSLog 또는
분석 서비스로 실패 원인(error.localizedDescription 포함)을 기록하고, (2) 등록이 실패했으면 바로
startObserverQuery()를 호출하지 않도록 분기 처리하며, 필요하면 사용자에게 설정 확인을 유도하거나(알림/alert) 재시도
로직을 트리거하도록 구현하세요; 대상 식별자는 enableBackgroundDelivery(), startObserverQuery(),
healthStore.enableBackgroundDelivery(...)입니다.
In `@mobile/iosApp/iosApp/iosApp.entitlements`:
- Around line 9-10: The entitlements currently only include
com.apple.developer.healthkit which enables basic HealthKit access but not
background delivery; update the entitlements to also include
com.apple.developer.healthkit.background-delivery (set to true) so
HKObserverQuery and enableBackgroundDelivery() will work, or enable "Background
Delivery" under Signing & Capabilities → HealthKit in Xcode to have Xcode add
the com.apple.developer.healthkit.background-delivery entitlement automatically.
In `@mobile/iosApp/iosApp/iosAppDebug.entitlements`:
- Around line 9-10: Add the HealthKit background delivery entitlement to the
debug entitlements so HKHealthStore.enableBackgroundDelivery() can succeed:
update iosAppDebug.entitlements to include the key
com.apple.developer.healthkit.background-delivery with a true value so when
HealthKitManager.enableBackgroundDelivery() is called immediately after
permission request it won’t fail at runtime; then rebuild the debug target to
ensure the entitlement is applied.
In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.kt`:
- Around line 25-28: The current postActiveCaloriesBurned(kcal: Double) creates
a new CoroutineScope(Dispatchers.IO) per call which prevents proper lifecycle
and error handling; instead either (A) add a class-level val scope =
CoroutineScope(SupervisorJob() + Dispatchers.IO) and use scope.launch {
notificationRepository.postActiveCaloriesBurned(kcal) } so cancellations/errors
are managed and you can cancel scope when the host is destroyed, or (B) make fun
postActiveCaloriesBurned a suspend function and delegate suspension to the
caller (Swift side) so callers control coroutine lifecycle; locate the function
postActiveCaloriesBurned and the usage of CoroutineScope(Dispatchers.IO) and
implement one of these approaches, ensuring
notificationRepository.postActiveCaloriesBurned is called from that managed
scope or from the suspend function.
- Line 6: Remove the invalid import "kotlinx.coroutines.IO" (it isn't a
top-level symbol) and rely on the already imported
kotlinx.coroutines.Dispatchers; ensure usages reference Dispatchers.IO (e.g.,
wherever Dispatchers.IO is used in Helper.kt) and keep only the Dispatchers
import to avoid the "Unresolved reference" compile error.
---
Nitpick comments:
In `@mobile/iosApp/iosApp/ContentView.swift`:
- Around line 33-35: 현재 onBurnedCalorieUpdated 콜백에서 매번 새로운 KoinHelper()를 생성하고 있어
불필요한 객체 할당이 발생합니다; 해결책은 KoinHelper 인스턴스를 ComposeView(또는 ContentView) 프로퍼티로 캐싱하거나
KoinHelper를 Kotlin 측에서 싱글톤(object)으로 변경하는 것입니다; 구체적으로 ContentView.swift의
onBurnedCalorieUpdated 콜백 호출부를 찾아 KoinHelper() 호출을 제거하고 미리 생성해 둔 프로퍼티(예: private
let koinHelper = KoinHelper())를 사용하거나, Kotlin에서 object KoinHelper로 변경해 호출을
KoinHelper.postActiveCaloriesBurned(kcal:)로 바꿔 적용하세요.
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift`:
- Around line 48-64: startObserverQuery currently creates and executes a new
HKObserverQuery every time it runs, which can cause duplicate observer callbacks
when requestAuthorization is called again; add a guard using an isObserving Bool
or store the HKObserverQuery instance (e.g., a private var observerQuery:
HKObserverQuery?) and check it before creating/executing a new query in
startObserverQuery, set isObserving = true (or assign observerQuery) after
successfully executing via healthStore.execute(query), and clear/reset these
when you stop observing or on deinit to prevent duplicate registrations.
In
`@mobile/shared/src/commonMain/kotlin/com/mulkkam/ui/setting/cups/component/CupsTopAppBar.kt`:
- Around line 50-55: The component uses CenterAlignedTopAppBar but sets colors
via TopAppBarDefaults.topAppBarColors; change the colors call to
TopAppBarDefaults.centerAlignedTopAppBarColors(...) so the API intent matches
(keep the same parameters: containerColor = White, titleContentColor = Gray400,
navigationIconContentColor = Gray400) and update any import if needed; locate
the usage in CupsTopAppBar.kt where CenterAlignedTopAppBar is defined and
replace topAppBarColors with centerAlignedTopAppBarColors.
In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.kt`:
- Around line 16-23: The initKoin function currently calls startKoin
unconditionally; to prevent exceptions on re-initialization, check
GlobalContext.getOrNull() and only call startKoin when it returns null. Update
the initKoin implementation (function initKoin) to guard the startKoin call
using GlobalContext.getOrNull(), preserving the modules(iosSharedModule(baseUrl,
isDebug)) behavior when actually initializing.
In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/component/HealthSection.ios.kt`:
- Around line 41-49: The Row currently uses Modifier.noRippleClickable(onClick =
{ onClick() }) which may not expose a button role to VoiceOver; update the
modifier to explicitly declare accessibility role by either replacing
noRippleClickable with Modifier.clickable(role = Role.Button, onClick = {
onClick() }) or keep noRippleClickable and append semantics { role = Role.Button
} to the Modifier chain so the Row (and its onClick handler) is announced as a
button to assistive technologies.
In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/HealthKitPlatform.kt`:
- Around line 23-25: In HealthKitPlatform, when canOpenURL(healthUrl) returns
false, don't silently fall back to openAppNotificationSettings(); instead
implement a clear, explicit fallback: either call a new helper like
showHealthUnavailableAlert() to present a user-facing message (or propagate an
error callback) indicating Health app is unavailable, or if you intend the
notification-settings fallback to be a deliberate permission-related guide,
leave the fallback but add a clarifying comment above
openAppNotificationSettings() describing that intent; update references in
HealthKitPlatform (the canOpenURL(healthUrl) branch) accordingly and add any
small helper (e.g., showHealthUnavailableAlert or an error callback) so the UI
can show feedback rather than unexpectedly opening notification settings.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
mobile/iosApp/iosApp/ContentView.swiftmobile/iosApp/iosApp/Health/HealthKitManager.swiftmobile/iosApp/iosApp/Info.plistmobile/iosApp/iosApp/MulKkamApp.swiftmobile/iosApp/iosApp/iosApp.entitlementsmobile/iosApp/iosApp/iosAppDebug.entitlementsmobile/shared/src/commonMain/composeResources/values/strings.xmlmobile/shared/src/commonMain/kotlin/com/mulkkam/ui/home/home/HomeViewModel.ktmobile/shared/src/commonMain/kotlin/com/mulkkam/ui/setting/cups/component/CupsTopAppBar.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/data/checker/CalorieCheckerImpl.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/data/repository/HealthRepositoryImpl.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/di/CheckerModule.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/di/IosSharedModule.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/di/RepositoryModule.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/HealthKitPlatform.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/component/HealthSection.ios.kt
💤 Files with no reviewable changes (5)
- mobile/shared/src/iosMain/kotlin/com/mulkkam/di/IosSharedModule.kt
- mobile/shared/src/iosMain/kotlin/com/mulkkam/di/CheckerModule.kt
- mobile/shared/src/iosMain/kotlin/com/mulkkam/data/checker/CalorieCheckerImpl.kt
- mobile/shared/src/iosMain/kotlin/com/mulkkam/data/repository/HealthRepositoryImpl.kt
- mobile/shared/src/iosMain/kotlin/com/mulkkam/di/RepositoryModule.kt
| func requestAuthorization( | ||
| onBurnedCalorieUpdated: @escaping (Double) -> Void, | ||
| completion: @escaping (Bool) -> Void | ||
| ) { | ||
| self.onBurnedCalorieUpdated = onBurnedCalorieUpdated | ||
|
|
||
| healthStore.requestAuthorization( | ||
| toShare: nil, | ||
| read: read | ||
| ) { success, error in | ||
| DispatchQueue.main.async { | ||
| if success { | ||
| self.enableBackgroundDelivery() | ||
| self.startObserverQuery() | ||
| } else { | ||
| if let url = URL(string: UIApplication.openSettingsURLString) { | ||
| UIApplication.shared.open(url) | ||
| } | ||
| } | ||
| completion(success) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "HealthKitManager.swift" -type fRepository: woowacourse-teams/2025-mul-kkam
Length of output: 127
🏁 Script executed:
cat -n ./mobile/iosApp/iosApp/Health/HealthKitManager.swiftRepository: woowacourse-teams/2025-mul-kkam
Length of output: 3623
HealthKit 가용성 체크가 필요합니다.
requestAuthorization() 호출 전에 HKHealthStore.isHealthDataAvailable() 가드가 없어서, HealthKit을 지원하지 않는 기기(예: 일부 iPad)에서 문제가 발생할 수 있습니다. 현재 코드는 인증 실패 시 설정 화면으로 유도하는데, 기기 미지원과 사용자 거부를 구분하지 못해 사용자 경험이 좋지 않습니다.
두 가지 개선 방안을 제안합니다:
-
사전 가드 방식 (권장): 16번 줄 앞에
if !HKHealthStore.isHealthDataAvailable() { completion(false); return }추가- 장점: 지원하지 않는 기기인지 명확히 판단, 불필요한 권한 요청 생략
- 단점: 분기 로직이 한 단계 증가
-
현재 유지: 추가 코드 없이 진행
- 장점: 구현이 단순함
- 단점: 실패 원인 구분 어려움, 지원하지 않는 기기에서도 설정 화면 유도
더불어 28번 줄의 enableBackgroundDelivery() 메서드(40-45줄)에서 완료 핸들러가 결과와 에러를 모두 무시하고 있습니다. 백그라운드 전달이 실패해도 알 수 없어 조용히 실패할 위험이 있으니, 최소한 에러 로깅이나 재시도 로직을 고려해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift` around lines 16 - 37, Add
a pre-check for HealthKit availability before calling
healthStore.requestAuthorization by using HKHealthStore.isHealthDataAvailable()
in requestAuthorization() (returning completion(false) immediately if
unavailable) so you don’t prompt unsupported devices; also update
enableBackgroundDelivery() to handle its completion callback (surface/log errors
and consider retry behavior) instead of ignoring result/error so background
delivery failures are observable; locate these changes around the
requestAuthorization(), enableBackgroundDelivery(), and startObserverQuery()
calls to implement the availability guard and proper error handling.
| private func enableBackgroundDelivery() { | ||
| healthStore.enableBackgroundDelivery( | ||
| for: activeEnergyType, | ||
| frequency: .hourly, | ||
| withCompletion: { _, _ in } | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "HealthKitManager.swift" | head -5Repository: woowacourse-teams/2025-mul-kkam
Length of output: 127
🏁 Script executed:
cat -n ./mobile/iosApp/iosApp/Health/HealthKitManager.swiftRepository: woowacourse-teams/2025-mul-kkam
Length of output: 3623
🌐 Web query:
HealthKit enableBackgroundDelivery error handling Swift
💡 Result:
What enableBackgroundDelivery’s completion means
HKHealthStore.enableBackgroundDelivery(...)’s completion handler only tells you whether registration succeeded (and returns an Error?). It does not deliver updates; updates come through an HKObserverQuery you’ve set up and executed. (stackoverflow.com)
Swift pattern: register + log errors (and don’t assume it worked)
let store = HKHealthStore()
let type = HKObjectType.quantityType(forIdentifier: .stepCount)! // example
store.enableBackgroundDelivery(for: type, frequency: .hourly) { success, error in
if success {
// registration succeeded
} else {
// IMPORTANT: log this (device settings often explain failures)
NSLog("enableBackgroundDelivery failed: \(error as Any)")
}
}A surprisingly common “error handling” improvement is simply persisting success + error somewhere (OSLog, analytics, your backend) so you can see what happens on real devices. (reddit.com)
In your observer query: always call completionHandler (even on error)
If your observer update handler has an error path that returns without calling completionHandler, HealthKit can eventually stop sending background updates. Use defer to make this foolproof. (developer.apple.com)
let query = HKObserverQuery(sampleType: type, predicate: nil) { _, completion, error in
defer { completion() } // ensures it’s called on every path
if let error {
NSLog("Observer error: \(error)")
return
}
// Kick off your real fetch here (often an HKAnchoredObjectQuery),
// keeping work short.
}
store.execute(query)Practical “it didn’t fire” cases to handle (not code bugs)
- Background App Refresh disabled can prevent/derail delivery; users may need to enable it in Settings. (stackoverflow.com)
- Device locked can block HealthKit access/delivery for privacy (expect delivery when the device is unlocked again). (stackoverflow.com)
- Some types (notably Step Count) are effectively throttled (often ~hourly) even if you request
.immediate, so “no callback yet” isn’t necessarily an error. (stackoverflow.com)
If you paste the exact error you’re seeing (domain/code/userInfo), I can suggest the specific handling/recovery path for that case.
Citations:
- 1: https://stackoverflow.com/questions/48670418/enablebackgrounddeliveryforfrequencywithcompletion-healthkit?utm_source=openai
- 2: https://www.reddit.com/r/swift/comments/1ceu3ac?utm_source=openai
- 3: https://developer.apple.com/forums/thread/801627?utm_source=openai
- 4: https://stackoverflow.com/questions/52292100/healthkit-set-up-enablebackgrounddelivery-but-ios-is-not-at-all-waking-up-the?utm_source=openai
- 5: https://stackoverflow.com/questions/74701509/hkhealthstore-enablebackgrounddelivery-when-screen-is-locked-never-works?utm_source=openai
- 6: https://stackoverflow.com/questions/26375767/healthkit-background-delivery-when-app-is-not-running?utm_source=openai
백그라운드 전달 등록 실패를 무시하고 있어서 동기화 실패를 추적할 수 없습니다.
44줄의 completion 핸들러가 success와 error를 버리면, 실제로 background delivery 등록이 실패한 상황을 감지하지 못합니다. 이후 바로 startObserverQuery()를 호출하지만, 등록이 실패했다면 백그라운드에서 갱신 콜백이 도착하지 않을 것입니다. 건강 데이터 누락으로 이어질 수 있는 침묵의 실패 경로입니다.
코드의 다른 부분들(5255줄, 8285줄)에서는 에러를 확인하는데, 이 메서드만 건너뛰는 것도 불일치합니다. 최소한 다음 중 하나를 권장합니다:
- 로깅만: 실패 여부와 이유를 OSLog나 분석 서비스에 기록하면, 실제 기기에서 어떤 상황(Background App Refresh 비활성화, 프라이버시 설정 등)으로 실패하는지 추적할 수 있습니다.
- 사용자 알림: 등록 실패 시 사용자에게 설정 확인을 유도하는 방식으로, 데이터 누락을 사전에 방지할 수 있습니다.
- 재시도 로직: 초기 등록 실패 시 주기적으로 재시도하는 방식입니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift` around lines 40 - 45,
enableBackgroundDelivery의 enableBackgroundDelivery(completion:) 콜백에서 success와
error를 무시하고 있어 실패를 알 수 없으니,
healthStore.enableBackgroundDelivery(for:activeEnergyType, frequency: .hourly,
withCompletion:)의 completion 파라미터에서 success와 error를 검사하도록 변경하고 실패 시(1) OSLog 또는
분석 서비스로 실패 원인(error.localizedDescription 포함)을 기록하고, (2) 등록이 실패했으면 바로
startObserverQuery()를 호출하지 않도록 분기 처리하며, 필요하면 사용자에게 설정 확인을 유도하거나(알림/alert) 재시도
로직을 트리거하도록 구현하세요; 대상 식별자는 enableBackgroundDelivery(), startObserverQuery(),
healthStore.enableBackgroundDelivery(...)입니다.
| fun postActiveCaloriesBurned(kcal: Double) { | ||
| CoroutineScope(Dispatchers.IO).launch { | ||
| notificationRepository.postActiveCaloriesBurned(kcal) | ||
| } |
There was a problem hiding this comment.
요청마다 새 CoroutineScope 생성은 수명 관리/에러 전파에 취약합니다.
Line 26 패턴은 호출 누적 시 취소 제어가 어렵고, 실패가 조용히 유실됩니다.
대안:
- 클래스 단일
CoroutineScope(SupervisorJob() + Dispatchers.IO)보유: 수명 제어/일관성 좋음(장점), 정리 지점 필요(단점). postActiveCaloriesBurned를suspend로 노출해 Swift 측에서 호출 제어: 호출부 책임이 명확(장점), Swift 연동 복잡도 증가(단점).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/Helper.kt` around lines 25 -
28, The current postActiveCaloriesBurned(kcal: Double) creates a new
CoroutineScope(Dispatchers.IO) per call which prevents proper lifecycle and
error handling; instead either (A) add a class-level val scope =
CoroutineScope(SupervisorJob() + Dispatchers.IO) and use scope.launch {
notificationRepository.postActiveCaloriesBurned(kcal) } so cancellations/errors
are managed and you can cancel scope when the host is destroyed, or (B) make fun
postActiveCaloriesBurned a suspend function and delegate suspension to the
caller (Swift side) so callers control coroutine lifecycle; locate the function
postActiveCaloriesBurned and the usage of CoroutineScope(Dispatchers.IO) and
implement one of these approaches, ensuring
notificationRepository.postActiveCaloriesBurned is called from that managed
scope or from the suspend function.
|
ci 터졌어요 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
mobile/shared/src/iosMain/kotlin/com/mulkkam/domain/model/bio/HealthKitPlatform.kt (1)
11-11: 메서드명이 Android 플랫폼 용어를 사용하고 있습니다.
navigateToHealthConnect라는 이름은 Android의 Health Connect API를 연상시킵니다. iOS에서는 HealthKit을 사용하므로navigateToHealthApp또는navigateToHealthKit과 같은 플랫폼 중립적이거나 iOS에 맞는 이름이 더 적절할 수 있습니다.물론 인터페이스(
HealthPlatform)에서 정의된 이름을 따라야 한다면 인터페이스 수준에서 이름을 재검토해보는 것도 좋겠습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/domain/model/bio/HealthKitPlatform.kt` at line 11, The method name navigateToHealthConnect uses Android-specific terminology; rename it to a platform-appropriate name (e.g., navigateToHealthApp or navigateToHealthKit) in this implementation (HealthKitPlatform) and update the HealthPlatform interface and all other implementations/usages to match the new name so signatures remain consistent; ensure you refactor references to navigateToHealthConnect across the codebase (calls, overrides, and tests) to the chosen new name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift`:
- Around line 51-67: The observer query is being re-registered each time
requestAuthorization() runs, causing duplicate HKObserverQuery handlers; modify
HealthKitManager to keep a stored reference (e.g., activeObserverQuery:
HKObserverQuery?) and before creating a new HKObserverQuery in
startObserverQuery() stop and/or remove the existing one via
healthStore.stop(_:) (or call existingQuery.stop()) and replace it with the new
query, or alternatively set a Bool flag (e.g., isObserverQueryStarted) to no-op
if already registered; update startObserverQuery() to use the stored symbol
(activeObserverQuery or isObserverQueryStarted) so only one active observer
exists and ensure the previous query is properly stopped before assigning a new
one.
In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/KoinHelper.kt`:
- Around line 16-23: initKoin can throw KoinAppAlreadyStartedException if called
more than once; protect it by adding a companion-object boolean guard (e.g.,
isKoinStarted) checked at the start of initKoin and set to true after successful
startKoin, so subsequent calls return early; make the guard thread-safe by
wrapping the check/set in a synchronized block (or other mutex) to avoid races
when initKoin is called from multiple threads.
In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/domain/model/bio/HealthKitPlatform.kt`:
- Line 3: 도메인 레이어(com.mulkkam.domain.model.bio의 HealthKitPlatform.kt)가 UI 유틸인
openAppNotificationSettings를 직접 임포트하고 있어 의존성 방향이 역전되어 있으니, 이 호출을 도메인에서 제거하세요;
해결책으로는 1) openAppNotificationSettings를 common/ios 공통 유틸 모듈로 이동하여 도메인에서 UI 의존성을
없애거나 2) HealthPlatform(또는 HealthKitPlatform) 인터페이스에 openSettings 같은 추상 메서드를 추가하고
도메인에서는 이 추상 메서드만 호출한 뒤 실제 플랫폼별 구현체(앱 모듈)에서 openAppNotificationSettings를 호출하도록
변경해 주세요 so that HealthKitPlatform/HealthPlatform contains no UI imports and the
platform-specific implementation in the UI layer performs the actual settings
opening.
---
Nitpick comments:
In
`@mobile/shared/src/iosMain/kotlin/com/mulkkam/domain/model/bio/HealthKitPlatform.kt`:
- Line 11: The method name navigateToHealthConnect uses Android-specific
terminology; rename it to a platform-appropriate name (e.g., navigateToHealthApp
or navigateToHealthKit) in this implementation (HealthKitPlatform) and update
the HealthPlatform interface and all other implementations/usages to match the
new name so signatures remain consistent; ensure you refactor references to
navigateToHealthConnect across the codebase (calls, overrides, and tests) to the
chosen new name.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 853c4f93-df26-4db0-94e5-a88ca2681e46
📒 Files selected for processing (4)
mobile/iosApp/iosApp/Health/HealthKitManager.swiftmobile/shared/src/iosMain/kotlin/com/mulkkam/di/HealthPlatformModule.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/di/KoinHelper.ktmobile/shared/src/iosMain/kotlin/com/mulkkam/domain/model/bio/HealthKitPlatform.kt
| private func startObserverQuery() { | ||
| let query = HKObserverQuery( | ||
| sampleType: activeEnergyType, | ||
| predicate: nil | ||
| ) { [weak self] _, completionHandler, error in | ||
| if error != nil { | ||
| completionHandler() | ||
| return | ||
| } | ||
|
|
||
| self?.fetchBurnedCalories { | ||
| completionHandler() | ||
| } | ||
| } | ||
|
|
||
| healthStore.execute(query) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the HealthKitManager file and examine its full content
fd -t f "HealthKitManager.swift" mobile/iosAppRepository: woowacourse-teams/2025-mul-kkam
Length of output: 125
🏁 Script executed:
#!/bin/bash
# Read the HealthKitManager file to understand the full context
cat -n mobile/iosApp/iosApp/Health/HealthKitManager.swiftRepository: woowacourse-teams/2025-mul-kkam
Length of output: 3643
🏁 Script executed:
#!/bin/bash
# Search for calls to startObserverQuery to understand the call patterns
rg -n "startObserverQuery|requestAuthorization|enableBackgroundDelivery" mobile/iosApp/iosApp/Health/ -A 2 -B 2Repository: woowacourse-teams/2025-mul-kkam
Length of output: 2461
🏁 Script executed:
#!/bin/bash
# Check if requestAuthorization is called elsewhere or if there are other lifecycle entry points
rg -n "requestAuthorization|HealthKitManager" mobile/iosApp --type swift -B 1 -A 1 | head -40Repository: woowacourse-teams/2025-mul-kkam
Length of output: 1948
🏁 Script executed:
#!/bin/bash
# Verify HKObserverQuery behavior patterns and typical iOS HealthKit usage
# Check if there's any cleanup or deinitialization in the class
rg -n "deinit|stop|invalidate" mobile/iosApp/iosApp/Health/HealthKitManager.swiftRepository: woowacourse-teams/2025-mul-kkam
Length of output: 57
Observer 쿼리가 누적 등록될 수 있습니다.
requestAuthorization()이 여러 번 호출될 때마다 새로운 HKObserverQuery를 실행하지만, 이전 쿼리를 중지하지 않습니다. HealthKitManager는 싱글톤으로 앱 생명주기 동안 유지되고, 저장된 쿼리 참조도 없어서 이전 인스턴스를 제어할 수 없습니다. 이렇게 되면 HealthKit 업데이트 1건이 등록된 쿼리 개수만큼 fetchBurnedCalories()를 반복 호출하고, 결국 onBurnedCalorieUpdated 콜백이 중복 실행되어 서버 전송도 여러 번 일어납니다.
해결 방안 — 두 가지 접근 모두 실용적입니다.
-
쿼리 교체 방식: 기존 쿼리를
stop()으로 정리한 뒤 새 쿼리를 등록합니다. 재권한 요청이나 재설정 상황에도 안전하며, 항상 정확히 1개의 활성 쿼리만 유지합니다. -
이미 등록 여부 확인:
startObserverQuery()를 처음 호출했는지 플래그로 추적해서, 이미 등록되었으면 no-op 처리합니다. 상태 관리가 더 단순하지만, 설정 변경 시나리오에서는 쿼리를 갱신할 수 없습니다.
현재 구조에서는 초기화가 한 번 더 발생하는 것만으로 중복 동작이 쉽게 누적되므로, 쿼리 생명주기를 여기서 명확히 관리하는 편이 안전합니다. 다음은 교체 방식의 예시입니다.
🔧 쿼리 교체 방식 예시
final class HealthKitManager {
static let shared = HealthKitManager()
static let exerciseDetectionThreshold: Double = 100.0
private init() {}
+ private var observerQuery: HKObserverQuery?
private let activeEnergyType =
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!
private let healthStore = HKHealthStore()
private let read = Set([
HKSampleType.quantityType(forIdentifier: .activeEnergyBurned)!
])
private var onBurnedCalorieUpdated: ((Double) -> Void)?
func requestAuthorization(
onBurnedCalorieUpdated: `@escaping` (Double) -> Void,
completion: `@escaping` (Bool) -> Void
) {
self.onBurnedCalorieUpdated = onBurnedCalorieUpdated
healthStore.requestAuthorization(
toShare: nil,
read: read
) { success, error in
DispatchQueue.main.async {
if success {
self.enableBackgroundDelivery()
self.startObserverQuery()
} else {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
completion(success)
}
}
}
private func startObserverQuery() {
+ if let observerQuery {
+ healthStore.stop(observerQuery)
+ }
+
let query = HKObserverQuery(
sampleType: activeEnergyType,
predicate: nil
) { [weak self] _, completionHandler, error in
if error != nil {
completionHandler()
return
}
self?.fetchBurnedCalories {
completionHandler()
}
}
+ observerQuery = query
healthStore.execute(query)
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mobile/iosApp/iosApp/Health/HealthKitManager.swift` around lines 51 - 67, The
observer query is being re-registered each time requestAuthorization() runs,
causing duplicate HKObserverQuery handlers; modify HealthKitManager to keep a
stored reference (e.g., activeObserverQuery: HKObserverQuery?) and before
creating a new HKObserverQuery in startObserverQuery() stop and/or remove the
existing one via healthStore.stop(_:) (or call existingQuery.stop()) and replace
it with the new query, or alternatively set a Bool flag (e.g.,
isObserverQueryStarted) to no-op if already registered; update
startObserverQuery() to use the stored symbol (activeObserverQuery or
isObserverQueryStarted) so only one active observer exists and ensure the
previous query is properly stopped before assigning a new one.
| fun initKoin( | ||
| baseUrl: String, | ||
| isDebug: Boolean, | ||
| ) { | ||
| startKoin { | ||
| modules(iosSharedModule(baseUrl, isDebug)) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Koin startKoin multiple calls re-initialization behavior
💡 Result:
startKoin { … }creates a global Koin container by registering it inGlobalContext. Because it’s global, it’s intended to be called once at the app entry point. [1]- If you call
startKoinagain while Koin is already started, Koin considers that an error (“can’t be called more than once”) and you typically get aKoinAppAlreadyStartedException. [2] - If your goal is to “reinitialize” Koin, the supported reset is to stop the global context via
stopKoin()(then you canstartKoinagain). This is also the recommended approach between tests to reset state. [1] [2] - If your goal is not a full re-init, but just adding/removing definitions after startup, don’t call
startKoinagain—useloadKoinModules(...)/unloadKoinModules(...). [2] - If you truly need multiple independent Koin containers (SDK/library isolation, multiple instances, some testing setups), don’t use
startKoin; usekoinApplication { … }to create an isolated instance that does not touchGlobalContext. [1] [3]
Sources: Starting Koin (latest docs) [1], Start Koin (core reference; notes “can’t be called more than once”, load/unload modules, stopKoin) [2], Context Isolation (koinApplication for isolated contexts) [3].
🏁 Script executed:
# Search for usage of KoinHelper and initKoin in the codebase
find . -type f -name "*.kt" | head -20 | xargs grep -l "KoinHelper\|initKoin" 2>/dev/null | head -10Repository: woowacourse-teams/2025-mul-kkam
Length of output: 57
🏁 Script executed:
# Look at the KoinHelper.kt file to understand its full context
cat -n mobile/shared/src/iosMain/kotlin/com/mulkkam/di/KoinHelper.ktRepository: woowacourse-teams/2025-mul-kkam
Length of output: 1067
🏁 Script executed:
# Search for Koin imports and usage patterns in the iOS main source
grep -r "startKoin\|initKoin\|KoinHelper" mobile/shared/src/iosMain/ --include="*.kt" 2>/dev/nullRepository: woowacourse-teams/2025-mul-kkam
Length of output: 418
initKoin()은 중복 호출 시 런타임 예외를 방지하도록 보호해야 합니다.
Koin의 startKoin은 전역 컨테이너를 일회성으로 초기화하는 API입니다. 공식 문서에 따르면 두 번째 호출 시 KoinAppAlreadyStartedException이 발생합니다. 현재 구현에는 재호출을 막는 장치가 없어서, Swift 쪽에서 초기화 로직이 반복되거나 KoinHelper 인스턴스가 재생성되면 런타임 크래시로 이어질 수 있습니다.
이를 해결하는 두 가지 방법을 고려해볼 수 있습니다.
1) 헬퍼 내부에 초기화 가드를 두는 방법 (최소 변경)
- 장점: 코드 변경이 작고, 여러 호출 지점에서도 안전하게 동작합니다.
- 단점: 초기화 책임이 헬퍼에 분산되어 있어 의도가 명확하지 않을 수 있습니다.
2) 앱 진입점(App delegate)에서만 초기화하도록 제한하는 방법 (구조적 개선)
- 장점: 초기화 시점과 책임이 명확해서 유지보수가 쉽습니다.
- 단점: 호출 구조 변경이 필요합니다.
현재 상황에서는 방법 1을 권장합니다. 아래와 같이 companion object의 가드 플래그를 추가하면 충분합니다:
🔧 구현 예시
class KoinHelper {
+ companion object {
+ private var isStarted = false
+ }
+
fun initKoin(
baseUrl: String,
isDebug: Boolean,
) {
+ if (isStarted) return
startKoin {
modules(iosSharedModule(baseUrl, isDebug))
}
+ isStarted = true
}
}메인 스레드 외 경로에서도 호출될 가능성이 있다면, 이 가드에 synchronized 블록을 감싸는 것도 좋습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mobile/shared/src/iosMain/kotlin/com/mulkkam/di/KoinHelper.kt` around lines
16 - 23, initKoin can throw KoinAppAlreadyStartedException if called more than
once; protect it by adding a companion-object boolean guard (e.g.,
isKoinStarted) checked at the start of initKoin and set to true after successful
startKoin, so subsequent calls return early; make the guard thread-safe by
wrapping the check/set in a synchronized block (or other mutex) to avoid races
when initKoin is called from multiple threads.
mobile/shared/src/iosMain/kotlin/com/mulkkam/ui/setting/bioinfo/HealthKitPlatform.kt
Show resolved
Hide resolved
junseo511
left a comment
There was a problem hiding this comment.
QA를 해드리고 싶었으나...! 로그인이 안되는군요 😭
궁금한 점이 있어 코멘트를 남겼습니다 :)
고생하셨어요 헬스!!!
| static let shared = HealthKitManager() | ||
| static let exerciseDetectionThreshold: Double = 100.0 | ||
|
|
||
| private init() {} |
There was a problem hiding this comment.
p3) 해당 함수가 하는 역할이 없어보입니다! 🙌
There was a problem hiding this comment.
private init 이 없으면 외부에서 인스턴스 생성이 가능해서, 싱글톤 패턴에서는 필수라고 하더군요?
그래서 살짝 넣어 봤습니다
| val healthUrl = NSURL.Companion.URLWithString("x-apple-health://") ?: return | ||
|
|
||
| val app = UIApplication.Companion.sharedApplication |
There was a problem hiding this comment.
p3) 여기 Companion들 지우셔도 될 것 같아요!
There was a problem hiding this comment.
| onBurnedCalorieUpdated: @escaping (Double) -> Void, | ||
| completion: @escaping (Bool) -> Void | ||
| ) { | ||
| self.onBurnedCalorieUpdated = onBurnedCalorieUpdated |
There was a problem hiding this comment.
p3) 궁금한 점이 있습니다! 혹시 100칼로리 이상 넘어가서 101, 102, 103 매번 보내지는 않는지요? onXxx 라는 함수명 때문에 우려가 되어서 혹시혹시 물어봐요!!
There was a problem hiding this comment.
🔗 관련 이슈
안드로이드처럼 WorkManager를 사용하는 것이 불가능해서, Calorie의 변동 사항이 있으면 자동으로 함수가 호출되는 로직입니다...
따라서 swift 내 자동으로 호출되는 함수에서 직접 서버 요청을 할 수 있도록
KoinHelper를 만들어 봤어요📸 스크린샷 (Optional)
ScreenRecording_02-27-2026.17-35-27_1.mov
🔗 관련 이슈
📝 작업 내용
iOS 기기의 건강 앱에서 활동 에너지(칼로리) 변화를 감지하면 자동 호출되는 Swift 함수가 서버에 요청할 수 있도록 iOS HealthKit 통합을 구현했습니다. Android의 WorkManager와 달리 iOS에서는 자동 트리거 함수가 직접 서버 통신을 수행해야 하므로, Kotlin/Swift 상호운영성을 지원하는 KoinHelper를 도입하여 Swift 코드에서 Kotlin의 저장소(Repository) 로직에 접근할 수 있게 했습니다.
주요 변경사항
iOS HealthKit 통합 계층 신규 구현
HealthKitManager.swift: HKHealthStore를 활용해 활동 에너지 권한 요청, 시간 단위 배경 업데이트 활성화, 데이터 변화 감지 옵저버 구현Kotlin/Swift 상호운영성 레이어 재설계
HelperKt.doInitKoin()의 정적 함수 호출에서KoinHelper클래스의 인스턴스 메서드 방식으로 변경postActiveCaloriesBurned(kcal: Double)메서드 추가: Swift에서 호출하면 Dispatchers.IO 스레드에서 비동기로 서버 요청 수행iOS 기존 Placeholder 구현 완전 제거
CalorieCheckerImpl.kt: TODO 주석만 있던 iOS 전용 스텁 삭제HealthRepositoryImpl.kt: 더미 반환값(0.0, false)만 제공하던 iOS 저장소 구현 삭제RepositoryModule,CheckerModule: 위 구현들의 DI 바인딩 제거iOS 앱 설정 및 권한 구성
Info.plist: NSHealthShareUsageDescription(한글 설명) 추가, UIBackgroundModes에 healthkit 추가iosApp.entitlements,iosAppDebug.entitlements: HealthKit 관련 entitlement 추가 (com.apple.developer.healthkit, com.apple.developer.healthkit.background-delivery)UI 계층 지원
HealthKitPlatform.kt: isAvailable()을 HKHealthStore.isHealthDataAvailable()로 구현, navigateToHealthConnect()에서 x-apple-health:// 스키마로 건강 앱 연결 시도HealthSection.ios.kt: iOS용 UI 완전 구현 (TODO 제거, 클릭 가능한 Row 컴포넌트 추가)strings.xml: "헬스 키트" 관련 리소스 문자열 추가부수적 개선
HomeViewModel.kt: isFirstLaunch 타입을 SharedFlow에서 StateFlow으로 변경하여 상태 흐름 강화CupsTopAppBar.kt: 컬러 설정을 centerAlignedTopAppBarColors에서 topAppBarColors로 통일📊 변경 효과 요약