From c0c59af0acb2cb543c3e2e6e813937cbb9b44fb2 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Fri, 10 Jul 2026 15:42:37 +0900 Subject: [PATCH] =?UTF-8?q?fix(watch):=20=EC=BB=B4=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=AF=B8=EB=85=B8=EC=B6=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20+=20=EC=9B=8C=EC=B9=98=20=EC=95=B1=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=8B=A4=EA=B5=AD=EC=96=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 컴플리케이션이 페이스 편집 목록에 아예 안 나오던 원인: 추천 구성 (recommendations)의 description에 문자열 보간을 사용해 WidgetKit이 "Formatted text for AppIntentRecommendation is not supported" assertion으로 익스텐션을 즉사시킴 → 시스템이 위젯 목록 조회 자체를 실패 → Text(verbatim:)으로 교체. 시뮬레이터 chronod 로그로 3종 kind (HaruWatchStatus/GoalRate/QuestRate) 정상 등록 확인 (크래시 0) - 추천 구성을 캐시된 목표/다짐 기반으로 개선 (모든 파라미터 채움) - 워치 앱·워치 위젯 익스텐션 표시 이름 현지화: InfoPlist.xcstrings 추가 (ko 하루다님 / en Haru Danim / ja ハルダニム) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7 --- .../Haru_Danim.xcodeproj/project.pbxproj | 9 ++-- .../InfoPlist.xcstrings | 30 ++++++++++++ .../InfoPlist.xcstrings | 30 ++++++++++++ .../RateComplications.swift | 48 +++++++++++++++---- 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 myApp/HaruDanim/Haru_DanimWatch Watch App/InfoPlist.xcstrings create mode 100644 myApp/HaruDanim/Haru_DanimWatchWidgets/InfoPlist.xcstrings diff --git a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj index d4dfacf..3153f7d 100644 --- a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj +++ b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ Assets.xcassets, Haru_DanimWatchWidgetsBundle.swift, Info.plist, + InfoPlist.xcstrings, Localizable.xcstrings, ); target = 7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */; @@ -670,7 +671,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatch; + INFOPLIST_KEY_CFBundleDisplayName = "하루다님"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.yechan.HaruDanim; LD_RUNPATH_SEARCH_PATHS = ( @@ -706,7 +707,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatch; + INFOPLIST_KEY_CFBundleDisplayName = "하루다님"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.yechan.HaruDanim; LD_RUNPATH_SEARCH_PATHS = ( @@ -742,7 +743,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatchWidgets; + INFOPLIST_KEY_CFBundleDisplayName = "하루다님"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -778,7 +779,7 @@ DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatchWidgets; + INFOPLIST_KEY_CFBundleDisplayName = "하루다님"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/InfoPlist.xcstrings b/myApp/HaruDanim/Haru_DanimWatch Watch App/InfoPlist.xcstrings new file mode 100644 index 0000000..553ca4b --- /dev/null +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/InfoPlist.xcstrings @@ -0,0 +1,30 @@ +{ + "sourceLanguage": "ko", + "strings": { + "CFBundleDisplayName": { + "comment": "Bundle display name", + "extractionState": "manual", + "localizations": { + "ko": { + "stringUnit": { + "state": "translated", + "value": "하루다님" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Haru Danim" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ハルダニム" + } + } + } + } + }, + "version": "1.0" +} diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/InfoPlist.xcstrings b/myApp/HaruDanim/Haru_DanimWatchWidgets/InfoPlist.xcstrings new file mode 100644 index 0000000..553ca4b --- /dev/null +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/InfoPlist.xcstrings @@ -0,0 +1,30 @@ +{ + "sourceLanguage": "ko", + "strings": { + "CFBundleDisplayName": { + "comment": "Bundle display name", + "extractionState": "manual", + "localizations": { + "ko": { + "stringUnit": { + "state": "translated", + "value": "하루다님" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Haru Danim" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ハルダニム" + } + } + } + } + }, + "version": "1.0" +} diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift b/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift index 3a7db7d..20de515 100644 --- a/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift @@ -68,12 +68,27 @@ struct GoalRateProvider: AppIntentTimelineProvider { Timeline(entries: [Self.makeEntry(configuration)], policy: .after(.now.addingTimeInterval(15 * 60))) } - /// 워치 페이스 갤러리에 노출할 추천 구성 (하루/주간/월간) + /// 워치 페이스 갤러리에 노출할 추천 구성 (목표 × 하루/주간/월간) + /// 주의: 추천 description에 문자열 보간(포맷 텍스트)을 쓰면 WidgetKit assertion으로 + /// 익스텐션이 즉사해 컴플리케이션이 페이스 편집 목록에 아예 나타나지 않는다. + /// 반드시 Text(verbatim:)처럼 포맷 없는 텍스트를 넘길 것. func recommendations() -> [AppIntentRecommendation] { - WatchSpanOption.allCases.map { span in - let intent = GoalRateConfigIntent() - intent.span = span - return AppIntentRecommendation(intent: intent, description: "목표 달성률 (\(span.label))") + let cached = (ComplicationStore.snapshot()?.goals ?? []).prefix(2) + .map { WatchGoalEntity(id: $0.id, title: $0.title) } + // 스냅숏이 아직 없으면 자리 표시용 엔티티로 추천 (표시 시점엔 첫 목표로 대체됨) + let goals = cached.isEmpty + ? [WatchGoalEntity(id: UUID(), title: String(localized: "목표"))] + : cached + return goals.flatMap { goal in + WatchSpanOption.allCases.map { span in + let intent = GoalRateConfigIntent() + intent.goal = goal + intent.span = span + return AppIntentRecommendation( + intent: intent, + description: Text(verbatim: "\(goal.title) · \(span.label)") + ) + } } } } @@ -196,12 +211,25 @@ struct QuestRateProvider: AppIntentTimelineProvider { Timeline(entries: [Self.makeEntry(configuration)], policy: .after(.now.addingTimeInterval(15 * 60))) } - /// 워치 페이스 갤러리에 노출할 추천 구성 (하루/주간/월간) + /// 워치 페이스 갤러리에 노출할 추천 구성 (다짐 × 하루/주간/월간) + /// 주의: description에 문자열 보간 금지 — Text(verbatim:) 사용 (GoalRate 쪽 주석 참고) func recommendations() -> [AppIntentRecommendation] { - WatchSpanOption.allCases.map { span in - let intent = QuestRateConfigIntent() - intent.span = span - return AppIntentRecommendation(intent: intent, description: "다짐 달성률 (\(span.label))") + let cached = (ComplicationStore.snapshot()?.goals ?? []) + .flatMap { goal in goal.quests.map { WatchQuestEntity(id: $0.id, title: "\(goal.title) · \($0.name)") } } + .prefix(2) + let quests = cached.isEmpty + ? [WatchQuestEntity(id: UUID(), title: String(localized: "다짐"))] + : Array(cached) + return quests.flatMap { quest in + WatchSpanOption.allCases.map { span in + let intent = QuestRateConfigIntent() + intent.quest = quest + intent.span = span + return AppIntentRecommendation( + intent: intent, + description: Text(verbatim: "\(quest.title) · \(span.label)") + ) + } } } }