위젯 동기화 크래시 수정: - DataStore: CloudKit 미러링을 메인 앱 프로세스 전담으로 분리. 위젯 등 앱 확장(.appex)은 CloudKit 엔타이틀먼트가 없고 메모리·수명 제약으로 미러링 설정이 크래시/행을 유발하므로, 확장 프로세스는 항상 같은 App Group 스토어 파일을 로컬 전용(cloudKitDatabase: .none)으로 연다. 확장이 쓴 변경은 메인 앱이 히스토리로 집어 CloudKit에 내보냄 - CloudSyncMonitor 신설: 메인 앱이 NSPersistentStoreRemoteChange (다른 기기의 CloudKit 가져오기·위젯 인텐트의 쓰기)를 디바운스로 받아 라이브 액티비티 → 위젯 타임라인 → 워치 스냅숏 순으로 즉시 갱신 - 인터랙티브 위젯(AppIntent)의 컨테이너 매핑 점검: IntentStore.commit이 저장 직후 reloadAllTimelines를 이미 보장함을 확인·문서화 모음 탭 목표 진행 현황 카드 개선: - '다짐별로 보기'의 상위 3개 제한 제거 — 모든 다짐 확인 가능 - 화면 균형을 위해 접힘/펼침 UX 적용: iPhone은 3개, iPad는 6개까지 접힌 상태로 보여주고 '다짐 N개 더 보기' 버튼으로 카드 안에서 펼침 (카드 탭 이동과 충돌하지 않는 독립 버튼, 스냅 애니메이션) - DebugSeed: 접힘/펼침 검증용 4번째 다짐 추가, -expandGoalCard 인자 추가 검증: iPhone 17 Pro·iPad Pro 11 시뮬레이터에서 접힘/펼침 렌더링과 사이드바 레이아웃 확인, 전체 타깃(iOS·워치·위젯) 빌드 성공 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
50 lines
1.9 KiB
Swift
50 lines
1.9 KiB
Swift
//
|
|
// CloudSyncMonitor.swift
|
|
// Haru_Danim
|
|
//
|
|
// CloudKit 원격 변경 반영 (메인 앱 전용)
|
|
// - SwiftData의 CloudKit 스토어는 원격 가져오기·다른 프로세스(위젯 인텐트)의 쓰기 때
|
|
// NSPersistentStoreRemoteChange 알림을 올린다. 이를 받아 위젯 타임라인·워치 스냅숏·
|
|
// 라이브 액티비티를 갱신해, 다른 기기에서 바뀐 데이터가 위젯에 바로 반영되게 한다.
|
|
// - 가져오기 중에는 알림이 짧은 간격으로 여러 번 오므로 디바운스로 한 번만 갱신한다.
|
|
//
|
|
|
|
import CoreData
|
|
import SwiftData
|
|
import WidgetKit
|
|
|
|
@MainActor
|
|
final class CloudSyncMonitor {
|
|
static let shared = CloudSyncMonitor()
|
|
|
|
private var observer: (any NSObjectProtocol)?
|
|
private var refreshTask: Task<Void, Never>?
|
|
|
|
/// 앱 시작 시 1회 호출. 이후 원격 변경이 올 때마다 디바운스 갱신을 예약한다.
|
|
func start(container: ModelContainer) {
|
|
guard observer == nil else { return }
|
|
observer = NotificationCenter.default.addObserver(
|
|
forName: .NSPersistentStoreRemoteChange,
|
|
object: nil,
|
|
queue: nil
|
|
) { _ in
|
|
Task { @MainActor in
|
|
CloudSyncMonitor.shared.scheduleRefresh(container: container)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func scheduleRefresh(container: ModelContainer) {
|
|
refreshTask?.cancel()
|
|
refreshTask = Task { @MainActor in
|
|
// CloudKit 가져오기 묶음이 끝나길 기다렸다가 한 번만 갱신
|
|
try? await Task.sleep(for: .seconds(2))
|
|
guard !Task.isCancelled else { return }
|
|
// 다른 기기에서 세션이 시작/종료됐을 수 있으므로 라이브 액티비티부터 맞춘다
|
|
LiveActivityManager.sync(context: container.mainContext)
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
WatchSyncManager.shared.pushSnapshot()
|
|
}
|
|
}
|
|
}
|