feat(complications): 워치 페이스 컴플리케이션 3종 (CLAUDE.md §9.2)
- 현재 현황: 측정 중 대표 행동 + 실시간 타이머 + 추가 측정 +N(없으면 생략), 미측정 시 '실행 중 아님' - 목표 달성률: 목표·기간(하루/주간/월간) 선택, 원형 게이지/진행바/인라인 - 다짐 달성률: rectangular는 세 기간 모두 표기, '이하 유지(한도형)' 다짐은 게이지 대신 한도 안/초과 상태로 별도 표현 - accessoryCircular/Rectangular/Inline/Corner 지원 + 페이스 갤러리 추천(recommendations) - 워치 앱이 캐시한 스냅숏 사용, 갱신 시 reloadTimelines로 실시간 리프레시 - pbxproj: WatchShared 3개 타깃 연결, 위젯·컴플리케이션 뷰를 앱 타깃에도 포함 (DEBUG 미리보기용, @main 제외), WATCHOS_DEPLOYMENT_TARGET 26.0 - DEBUG 미리보기: -complicationPreview YES, -complicationScroll YES Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
f9f1898d15
commit
1758cc3a25
@ -7,10 +7,28 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
7263D0EA3000433400EA47FC /* Haru_DanimWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 7263D0E03000433300EA47FC /* Haru_DanimWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7263D0F7300044E900EA47FC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7263D0F6300044E900EA47FC /* WidgetKit.framework */; };
|
||||
7263D0F9300044E900EA47FC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7263D0F8300044E900EA47FC /* SwiftUI.framework */; };
|
||||
7263D108300044EA00EA47FC /* Haru_DanimWatchWidgetsExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7263D0F4300044E900EA47FC /* Haru_DanimWatchWidgetsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
AA000000000000000000000E /* Haru_DanimWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000004 /* Haru_DanimWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
7263D0E83000433400EA47FC /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 725A9E7C2FFCC65000846FFB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 7263D0DF3000433300EA47FC;
|
||||
remoteInfo = "Haru_DanimWatch Watch App";
|
||||
};
|
||||
7263D106300044EA00EA47FC /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 725A9E7C2FFCC65000846FFB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 7263D0F3300044E900EA47FC;
|
||||
remoteInfo = Haru_DanimWatchWidgetsExtension;
|
||||
};
|
||||
AA000000000000000000000B /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 725A9E7C2FFCC65000846FFB /* Project object */;
|
||||
@ -21,6 +39,28 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
7263D0EE3000433400EA47FC /* Embed Watch Content */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
|
||||
dstSubfolderSpec = 16;
|
||||
files = (
|
||||
7263D0EA3000433400EA47FC /* Haru_DanimWatch Watch App.app in Embed Watch Content */,
|
||||
);
|
||||
name = "Embed Watch Content";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D10D300044EA00EA47FC /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
7263D108300044EA00EA47FC /* Haru_DanimWatchWidgetsExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA000000000000000000000D /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -36,10 +76,24 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
725A9E842FFCC65000846FFB /* Haru_Danim.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Haru_Danim.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7263D0DA3000415100EA47FC /* Haru_DanimWidgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Haru_DanimWidgets.entitlements; sourceTree = "<group>"; };
|
||||
7263D0DB3000417000EA47FC /* Haru_Danim.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Haru_Danim.entitlements; sourceTree = "<group>"; };
|
||||
7263D0E03000433300EA47FC /* Haru_DanimWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Haru_DanimWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7263D0F4300044E900EA47FC /* Haru_DanimWatchWidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Haru_DanimWatchWidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
7263D0F6300044E900EA47FC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
7263D0F8300044E900EA47FC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
7263D10E300044F100EA47FC /* Haru_DanimWatchWidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Haru_DanimWatchWidgetsExtension.entitlements; sourceTree = "<group>"; };
|
||||
AA0000000000000000000004 /* Haru_DanimWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Haru_DanimWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
7263D109300044EA00EA47FC /* Exceptions for "Haru_DanimWatchWidgets" folder in "Haru_DanimWatchWidgetsExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 7263D0F3300044E900EA47FC /* Haru_DanimWatchWidgetsExtension */;
|
||||
};
|
||||
AA000000000000000000000F /* Exceptions for "Widgets" folder in "Haru_DanimWidgets" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@ -47,6 +101,23 @@
|
||||
);
|
||||
target = AA0000000000000000000003 /* Haru_DanimWidgets */;
|
||||
};
|
||||
AA0000000000000000000010 /* Exceptions for "Widgets" folder in "Haru_Danim" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
HaruDanimWidgets.swift,
|
||||
Info.plist,
|
||||
);
|
||||
target = 725A9E832FFCC65000846FFB /* Haru_Danim */;
|
||||
};
|
||||
AA0000000000000000000012 /* Exceptions for "Haru_DanimWatchWidgets" folder in "Haru_DanimWatch Watch App" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Assets.xcassets,
|
||||
Haru_DanimWatchWidgetsBundle.swift,
|
||||
Info.plist,
|
||||
);
|
||||
target = 7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
@ -55,9 +126,24 @@
|
||||
path = IOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7263D0E13000433300EA47FC /* Haru_DanimWatch Watch App */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "Haru_DanimWatch Watch App";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7263D0FA300044E900EA47FC /* Haru_DanimWatchWidgets */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
AA0000000000000000000012 /* Exceptions for "Haru_DanimWatchWidgets" folder in "Haru_DanimWatch Watch App" target */,
|
||||
7263D109300044EA00EA47FC /* Exceptions for "Haru_DanimWatchWidgets" folder in "Haru_DanimWatchWidgetsExtension" target */,
|
||||
);
|
||||
path = Haru_DanimWatchWidgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA0000000000000000000001 /* Widgets */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
AA0000000000000000000010 /* Exceptions for "Widgets" folder in "Haru_Danim" target */,
|
||||
AA000000000000000000000F /* Exceptions for "Widgets" folder in "Haru_DanimWidgets" target */,
|
||||
);
|
||||
path = Widgets;
|
||||
@ -68,6 +154,11 @@
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA0000000000000000000011 /* WatchShared */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = WatchShared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -78,6 +169,22 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0DD3000433300EA47FC /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0F1300044E900EA47FC /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7263D0F9300044E900EA47FC /* SwiftUI.framework in Frameworks */,
|
||||
7263D0F7300044E900EA47FC /* WidgetKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000006 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -91,9 +198,16 @@
|
||||
725A9E7B2FFCC65000846FFB = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7263D10E300044F100EA47FC /* Haru_DanimWatchWidgetsExtension.entitlements */,
|
||||
7263D0DB3000417000EA47FC /* Haru_Danim.entitlements */,
|
||||
7263D0DA3000415100EA47FC /* Haru_DanimWidgets.entitlements */,
|
||||
725A9E862FFCC65000846FFB /* IOS */,
|
||||
AA0000000000000000000002 /* Shared */,
|
||||
AA0000000000000000000011 /* WatchShared */,
|
||||
AA0000000000000000000001 /* Widgets */,
|
||||
7263D0E13000433300EA47FC /* Haru_DanimWatch Watch App */,
|
||||
7263D0FA300044E900EA47FC /* Haru_DanimWatchWidgets */,
|
||||
7263D0F5300044E900EA47FC /* Frameworks */,
|
||||
725A9E852FFCC65000846FFB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@ -103,10 +217,21 @@
|
||||
children = (
|
||||
725A9E842FFCC65000846FFB /* Haru_Danim.app */,
|
||||
AA0000000000000000000004 /* Haru_DanimWidgets.appex */,
|
||||
7263D0E03000433300EA47FC /* Haru_DanimWatch Watch App.app */,
|
||||
7263D0F4300044E900EA47FC /* Haru_DanimWatchWidgetsExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7263D0F5300044E900EA47FC /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7263D0F6300044E900EA47FC /* WidgetKit.framework */,
|
||||
7263D0F8300044E900EA47FC /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -118,15 +243,19 @@
|
||||
725A9E812FFCC65000846FFB /* Frameworks */,
|
||||
725A9E822FFCC65000846FFB /* Resources */,
|
||||
AA000000000000000000000D /* Embed Foundation Extensions */,
|
||||
7263D0EE3000433400EA47FC /* Embed Watch Content */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
AA000000000000000000000C /* PBXTargetDependency */,
|
||||
7263D0E93000433400EA47FC /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
725A9E862FFCC65000846FFB /* IOS */,
|
||||
AA0000000000000000000001 /* Widgets */,
|
||||
AA0000000000000000000002 /* Shared */,
|
||||
AA0000000000000000000011 /* WatchShared */,
|
||||
);
|
||||
name = Haru_Danim;
|
||||
packageProductDependencies = (
|
||||
@ -135,6 +264,55 @@
|
||||
productReference = 725A9E842FFCC65000846FFB /* Haru_Danim.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7263D0EB3000433400EA47FC /* Build configuration list for PBXNativeTarget "Haru_DanimWatch Watch App" */;
|
||||
buildPhases = (
|
||||
7263D0DC3000433300EA47FC /* Sources */,
|
||||
7263D0DD3000433300EA47FC /* Frameworks */,
|
||||
7263D0DE3000433300EA47FC /* Resources */,
|
||||
7263D10D300044EA00EA47FC /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
7263D107300044EA00EA47FC /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
7263D0E13000433300EA47FC /* Haru_DanimWatch Watch App */,
|
||||
7263D0FA300044E900EA47FC /* Haru_DanimWatchWidgets */,
|
||||
AA0000000000000000000011 /* WatchShared */,
|
||||
);
|
||||
name = "Haru_DanimWatch Watch App";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "Haru_DanimWatch Watch App";
|
||||
productReference = 7263D0E03000433300EA47FC /* Haru_DanimWatch Watch App.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
7263D0F3300044E900EA47FC /* Haru_DanimWatchWidgetsExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7263D10A300044EA00EA47FC /* Build configuration list for PBXNativeTarget "Haru_DanimWatchWidgetsExtension" */;
|
||||
buildPhases = (
|
||||
7263D0F0300044E900EA47FC /* Sources */,
|
||||
7263D0F1300044E900EA47FC /* Frameworks */,
|
||||
7263D0F2300044E900EA47FC /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
7263D0FA300044E900EA47FC /* Haru_DanimWatchWidgets */,
|
||||
AA0000000000000000000011 /* WatchShared */,
|
||||
);
|
||||
name = Haru_DanimWatchWidgetsExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Haru_DanimWatchWidgetsExtension;
|
||||
productReference = 7263D0F4300044E900EA47FC /* Haru_DanimWatchWidgetsExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
AA0000000000000000000003 /* Haru_DanimWidgets */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Haru_DanimWidgets" */;
|
||||
@ -171,6 +349,12 @@
|
||||
725A9E832FFCC65000846FFB = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
7263D0DF3000433300EA47FC = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
7263D0F3300044E900EA47FC = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
AA0000000000000000000003 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
@ -194,6 +378,8 @@
|
||||
targets = (
|
||||
725A9E832FFCC65000846FFB /* Haru_Danim */,
|
||||
AA0000000000000000000003 /* Haru_DanimWidgets */,
|
||||
7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */,
|
||||
7263D0F3300044E900EA47FC /* Haru_DanimWatchWidgetsExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -206,6 +392,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0DE3000433300EA47FC /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0F2300044E900EA47FC /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000007 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -223,6 +423,20 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0DC3000433300EA47FC /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
7263D0F0300044E900EA47FC /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000005 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -233,6 +447,16 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
7263D0E93000433400EA47FC /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 7263D0DF3000433300EA47FC /* Haru_DanimWatch Watch App */;
|
||||
targetProxy = 7263D0E83000433400EA47FC /* PBXContainerItemProxy */;
|
||||
};
|
||||
7263D107300044EA00EA47FC /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 7263D0F3300044E900EA47FC /* Haru_DanimWatchWidgetsExtension */;
|
||||
targetProxy = 7263D106300044EA00EA47FC /* PBXContainerItemProxy */;
|
||||
};
|
||||
AA000000000000000000000C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = AA0000000000000000000003 /* Haru_DanimWidgets */;
|
||||
@ -365,6 +589,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_Danim.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
@ -400,6 +625,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_Danim.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
@ -430,9 +656,146 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7263D0EC3000433400EA47FC /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatch;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.yechan.HaruDanim;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.HaruDanim.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7263D0ED3000433400EA47FC /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatch;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.yechan.HaruDanim;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.yechan.HaruDanim.watchkitapp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
7263D10B300044EA00EA47FC /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgetsExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatchWidgets;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.HaruDanim.watchkitapp.Haru-DanimWatchWidgets";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
7263D10C300044EA00EA47FC /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgetsExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Haru_DanimWatchWidgets;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.HaruDanim.watchkitapp.Haru-DanimWatchWidgets";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = watchos;
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 26.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
AA0000000000000000000008 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
@ -458,6 +821,7 @@
|
||||
AA0000000000000000000009 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
@ -501,6 +865,24 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7263D0EB3000433400EA47FC /* Build configuration list for PBXNativeTarget "Haru_DanimWatch Watch App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7263D0EC3000433400EA47FC /* Debug */,
|
||||
7263D0ED3000433400EA47FC /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
7263D10A300044EA00EA47FC /* Build configuration list for PBXNativeTarget "Haru_DanimWatchWidgetsExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
7263D10B300044EA00EA47FC /* Debug */,
|
||||
7263D10C300044EA00EA47FC /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Haru_DanimWidgets" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
//
|
||||
// ComplicationPreviewScreen.swift
|
||||
// Haru_DanimWatch Watch App
|
||||
//
|
||||
// DEBUG 전용: 컴플리케이션 뷰를 워치 앱 안에서 렌더링해 검증하는 화면.
|
||||
// 런치 인자 `-complicationPreview YES`로 진입.
|
||||
// (워치 페이스에 컴플리케이션을 자동으로 추가할 방법이 없어 만든 검증 도구)
|
||||
//
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct ComplicationPreviewScreen: View {
|
||||
@State private var status: StatusEntry?
|
||||
@State private var goalRate: GoalRateEntry?
|
||||
@State private var questRate: QuestRateEntry?
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let status {
|
||||
label("현재 현황")
|
||||
HStack(spacing: 8) {
|
||||
circularBox { StatusComplicationView(previewFamily: .accessoryCircular, entry: status) }
|
||||
rectBox { StatusComplicationView(previewFamily: .accessoryRectangular, entry: status) }
|
||||
}
|
||||
}
|
||||
if let goalRate {
|
||||
label("목표 달성률")
|
||||
HStack(spacing: 8) {
|
||||
circularBox { GoalRateComplicationView(previewFamily: .accessoryCircular, entry: goalRate) }
|
||||
rectBox { GoalRateComplicationView(previewFamily: .accessoryRectangular, entry: goalRate) }
|
||||
}
|
||||
}
|
||||
if let questRate {
|
||||
label("다짐 달성률")
|
||||
HStack(spacing: 8) {
|
||||
circularBox { QuestRateComplicationView(previewFamily: .accessoryCircular, entry: questRate) }
|
||||
rectBox { QuestRateComplicationView(previewFamily: .accessoryRectangular, entry: questRate) }
|
||||
}
|
||||
.id("quest")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
status = StatusProvider.makeEntry()
|
||||
goalRate = GoalRateProvider.makeEntry(GoalRateConfigIntent())
|
||||
questRate = QuestRateProvider.makeEntry(QuestRateConfigIntent())
|
||||
// -complicationScroll YES → 다짐 섹션까지 자동 스크롤
|
||||
if UserDefaults.standard.bool(forKey: "complicationScroll") {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
||||
withAnimation { proxy.scrollTo("quest", anchor: .bottom) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func label(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
private func circularBox(@ViewBuilder content: () -> some View) -> some View {
|
||||
content()
|
||||
.frame(width: 50, height: 50)
|
||||
.background(.white.opacity(0.12), in: Circle())
|
||||
}
|
||||
|
||||
private func rectBox(@ViewBuilder content: () -> some View) -> some View {
|
||||
content()
|
||||
.padding(4)
|
||||
.frame(width: 120, height: 54)
|
||||
.background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"platform" : "watchos",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
125
myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift
Normal file
125
myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift
Normal file
@ -0,0 +1,125 @@
|
||||
//
|
||||
// ComplicationSupport.swift
|
||||
// Haru_DanimWatchWidgetsExtension
|
||||
//
|
||||
// 컴플리케이션 공용 (CLAUDE.md §9.2)
|
||||
// - 워치 앱이 App Group defaults에 캐시한 최신 스냅숏을 읽는다
|
||||
// - 목표/다짐 선택 엔티티와 기간 옵션 정의
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
// MARK: - 스냅숏 로드
|
||||
|
||||
nonisolated enum ComplicationStore {
|
||||
static var defaults: UserDefaults {
|
||||
UserDefaults(suiteName: "group.com.yechan.HaruDanim") ?? .standard
|
||||
}
|
||||
|
||||
static func snapshot() -> WatchSnapshot? {
|
||||
guard let data = defaults.data(forKey: WatchSync.cachedSnapshotKey) else { return nil }
|
||||
return WatchSnapshot.decode(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 기간 옵션
|
||||
|
||||
enum WatchSpanOption: String, AppEnum {
|
||||
case day, week, month
|
||||
|
||||
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "기간")
|
||||
static let caseDisplayRepresentations: [WatchSpanOption: DisplayRepresentation] = [
|
||||
.day: "하루",
|
||||
.week: "주간",
|
||||
.month: "월간",
|
||||
]
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .day: return "하루"
|
||||
case .week: return "주간"
|
||||
case .month: return "월간"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 목표/다짐 엔티티 (캐시된 스냅숏 기반)
|
||||
|
||||
struct WatchGoalEntity: AppEntity {
|
||||
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "목표")
|
||||
static let defaultQuery = WatchGoalEntityQuery()
|
||||
|
||||
let id: UUID
|
||||
let title: String
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(title)")
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchGoalEntityQuery: EntityQuery {
|
||||
func entities(for identifiers: [UUID]) async throws -> [WatchGoalEntity] {
|
||||
(ComplicationStore.snapshot()?.goals ?? [])
|
||||
.filter { identifiers.contains($0.id) }
|
||||
.map { WatchGoalEntity(id: $0.id, title: $0.title) }
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [WatchGoalEntity] {
|
||||
(ComplicationStore.snapshot()?.goals ?? [])
|
||||
.map { WatchGoalEntity(id: $0.id, title: $0.title) }
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchQuestEntity: AppEntity {
|
||||
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "다짐")
|
||||
static let defaultQuery = WatchQuestEntityQuery()
|
||||
|
||||
let id: UUID
|
||||
let title: String
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(title: "\(title)")
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchQuestEntityQuery: EntityQuery {
|
||||
private func all() -> [WatchQuestEntity] {
|
||||
(ComplicationStore.snapshot()?.goals ?? []).flatMap { goal in
|
||||
goal.quests.map { WatchQuestEntity(id: $0.id, title: "\(goal.title) · \($0.name)") }
|
||||
}
|
||||
}
|
||||
|
||||
func entities(for identifiers: [UUID]) async throws -> [WatchQuestEntity] {
|
||||
all().filter { identifiers.contains($0.id) }
|
||||
}
|
||||
|
||||
func suggestedEntities() async throws -> [WatchQuestEntity] {
|
||||
all()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 공용 뷰
|
||||
|
||||
struct ComplicationLockedView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "crown.fill")
|
||||
Text("프리미엄")
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComplicationEmptyView: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
Text(message)
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
//
|
||||
// Haru_DanimWatchWidgetsBundle.swift
|
||||
// Haru_DanimWatchWidgetsExtension
|
||||
//
|
||||
// 워치 페이스 컴플리케이션 번들 (CLAUDE.md §9.2)
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@main
|
||||
struct Haru_DanimWatchWidgetsBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
StatusComplication()
|
||||
GoalRateComplication()
|
||||
QuestRateComplication()
|
||||
}
|
||||
}
|
||||
11
myApp/HaruDanim/Haru_DanimWatchWidgets/Info.plist
Normal file
11
myApp/HaruDanim/Haru_DanimWatchWidgets/Info.plist
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
315
myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift
Normal file
315
myApp/HaruDanim/Haru_DanimWatchWidgets/RateComplications.swift
Normal file
@ -0,0 +1,315 @@
|
||||
//
|
||||
// RateComplications.swift
|
||||
// Haru_DanimWatchWidgetsExtension
|
||||
//
|
||||
// 목표/다짐 달성률 컴플리케이션 (CLAUDE.md §9.2-2, §9.2-3)
|
||||
// - 목표: 선택한 목표의 하루/주간/월간 달성률
|
||||
// - 다짐: 선택한 다짐의 달성률. rectangular는 세 기간을 모두 표기.
|
||||
// '이하 유지(넘기면 안 되는)' 다짐은 한도 대비 상태로 다르게 표현
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
// MARK: - 목표 달성률
|
||||
|
||||
struct GoalRateConfigIntent: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "목표 달성률"
|
||||
static let description = IntentDescription("표시할 목표와 기간을 선택하세요.")
|
||||
|
||||
@Parameter(title: "목표")
|
||||
var goal: WatchGoalEntity?
|
||||
|
||||
@Parameter(title: "기간", default: .day)
|
||||
var span: WatchSpanOption
|
||||
}
|
||||
|
||||
struct GoalRateEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let locked: Bool
|
||||
let title: String?
|
||||
let symbolName: String
|
||||
let spanLabel: String
|
||||
let ratio: Double
|
||||
}
|
||||
|
||||
struct GoalRateProvider: AppIntentTimelineProvider {
|
||||
static func makeEntry(_ configuration: GoalRateConfigIntent) -> GoalRateEntry {
|
||||
guard let snapshot = ComplicationStore.snapshot() else {
|
||||
return GoalRateEntry(date: .now, locked: false, title: nil, symbolName: "flag.checkered",
|
||||
spanLabel: configuration.span.label, ratio: 0)
|
||||
}
|
||||
guard snapshot.isPremium else {
|
||||
return GoalRateEntry(date: .now, locked: true, title: nil, symbolName: "flag.checkered",
|
||||
spanLabel: "", ratio: 0)
|
||||
}
|
||||
let goal = snapshot.goals.first { $0.id == configuration.goal?.id } ?? snapshot.goals.first
|
||||
return GoalRateEntry(
|
||||
date: .now,
|
||||
locked: false,
|
||||
title: goal?.title,
|
||||
symbolName: goal?.symbolName ?? "flag.checkered",
|
||||
spanLabel: configuration.span.label,
|
||||
ratio: goal?.ratio(forSpanRaw: configuration.span.rawValue) ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
func placeholder(in context: Context) -> GoalRateEntry {
|
||||
GoalRateEntry(date: .now, locked: false, title: "목표", symbolName: "flag.checkered",
|
||||
spanLabel: "하루", ratio: 0.6)
|
||||
}
|
||||
|
||||
func snapshot(for configuration: GoalRateConfigIntent, in context: Context) async -> GoalRateEntry {
|
||||
Self.makeEntry(configuration)
|
||||
}
|
||||
|
||||
func timeline(for configuration: GoalRateConfigIntent, in context: Context) async -> Timeline<GoalRateEntry> {
|
||||
Timeline(entries: [Self.makeEntry(configuration)], policy: .after(.now.addingTimeInterval(15 * 60)))
|
||||
}
|
||||
|
||||
/// 워치 페이스 갤러리에 노출할 추천 구성 (하루/주간/월간)
|
||||
func recommendations() -> [AppIntentRecommendation<GoalRateConfigIntent>] {
|
||||
WatchSpanOption.allCases.map { span in
|
||||
let intent = GoalRateConfigIntent()
|
||||
intent.span = span
|
||||
return AppIntentRecommendation(intent: intent, description: "목표 달성률 (\(span.label))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GoalRateComplicationView: View {
|
||||
@Environment(\.widgetFamily) private var envFamily
|
||||
/// 워치 앱 내 DEBUG 미리보기에서 패밀리를 강제할 때 사용
|
||||
var previewFamily: WidgetFamily? = nil
|
||||
private var family: WidgetFamily { previewFamily ?? envFamily }
|
||||
let entry: GoalRateEntry
|
||||
|
||||
private var percent: Int { Int((entry.ratio * 100).rounded()) }
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if entry.locked {
|
||||
ComplicationLockedView()
|
||||
} else if let title = entry.title {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
Text("\(title) \(percent)%")
|
||||
case .accessoryRectangular:
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: entry.symbolName)
|
||||
.font(.system(size: 11))
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Gauge(value: min(max(entry.ratio, 0), 1)) { EmptyView() }
|
||||
.gaugeStyle(.accessoryLinearCapacity)
|
||||
Text("\(entry.spanLabel) \(percent)%")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
default:
|
||||
Gauge(value: min(max(entry.ratio, 0), 1)) {
|
||||
Image(systemName: entry.symbolName)
|
||||
} currentValueLabel: {
|
||||
Text("\(percent)")
|
||||
.font(.system(size: 13, weight: .bold).monospacedDigit())
|
||||
}
|
||||
.gaugeStyle(.accessoryCircular)
|
||||
}
|
||||
} else {
|
||||
ComplicationEmptyView(message: "목표 없음")
|
||||
}
|
||||
}
|
||||
.containerBackground(.clear, for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
struct GoalRateComplication: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(
|
||||
kind: "HaruWatchGoalRate",
|
||||
intent: GoalRateConfigIntent.self,
|
||||
provider: GoalRateProvider()
|
||||
) { entry in
|
||||
GoalRateComplicationView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("목표 달성률")
|
||||
.description("선택한 목표의 하루/주간/월간 달성률을 표시해요. (프리미엄)")
|
||||
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 다짐 달성률
|
||||
|
||||
struct QuestRateConfigIntent: WidgetConfigurationIntent {
|
||||
static let title: LocalizedStringResource = "다짐 달성률"
|
||||
static let description = IntentDescription("표시할 다짐과 기간을 선택하세요.")
|
||||
|
||||
@Parameter(title: "다짐")
|
||||
var quest: WatchQuestEntity?
|
||||
|
||||
@Parameter(title: "기간", default: .day)
|
||||
var span: WatchSpanOption
|
||||
}
|
||||
|
||||
struct QuestRateEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let locked: Bool
|
||||
let quest: WatchQuestInfo?
|
||||
let spanRaw: String
|
||||
let spanLabel: String
|
||||
}
|
||||
|
||||
struct QuestRateProvider: AppIntentTimelineProvider {
|
||||
static func makeEntry(_ configuration: QuestRateConfigIntent) -> QuestRateEntry {
|
||||
guard let snapshot = ComplicationStore.snapshot() else {
|
||||
return QuestRateEntry(date: .now, locked: false, quest: nil,
|
||||
spanRaw: configuration.span.rawValue, spanLabel: configuration.span.label)
|
||||
}
|
||||
guard snapshot.isPremium else {
|
||||
return QuestRateEntry(date: .now, locked: true, quest: nil,
|
||||
spanRaw: configuration.span.rawValue, spanLabel: "")
|
||||
}
|
||||
let quests = snapshot.goals.flatMap(\.quests)
|
||||
let quest = quests.first { $0.id == configuration.quest?.id } ?? quests.first
|
||||
return QuestRateEntry(
|
||||
date: .now,
|
||||
locked: false,
|
||||
quest: quest,
|
||||
spanRaw: configuration.span.rawValue,
|
||||
spanLabel: configuration.span.label
|
||||
)
|
||||
}
|
||||
|
||||
func placeholder(in context: Context) -> QuestRateEntry {
|
||||
QuestRateEntry(date: .now, locked: false, quest: nil, spanRaw: "day", spanLabel: "하루")
|
||||
}
|
||||
|
||||
func snapshot(for configuration: QuestRateConfigIntent, in context: Context) async -> QuestRateEntry {
|
||||
Self.makeEntry(configuration)
|
||||
}
|
||||
|
||||
func timeline(for configuration: QuestRateConfigIntent, in context: Context) async -> Timeline<QuestRateEntry> {
|
||||
Timeline(entries: [Self.makeEntry(configuration)], policy: .after(.now.addingTimeInterval(15 * 60)))
|
||||
}
|
||||
|
||||
/// 워치 페이스 갤러리에 노출할 추천 구성 (하루/주간/월간)
|
||||
func recommendations() -> [AppIntentRecommendation<QuestRateConfigIntent>] {
|
||||
WatchSpanOption.allCases.map { span in
|
||||
let intent = QuestRateConfigIntent()
|
||||
intent.span = span
|
||||
return AppIntentRecommendation(intent: intent, description: "다짐 달성률 (\(span.label))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QuestRateComplicationView: View {
|
||||
@Environment(\.widgetFamily) private var envFamily
|
||||
/// 워치 앱 내 DEBUG 미리보기에서 패밀리를 강제할 때 사용
|
||||
var previewFamily: WidgetFamily? = nil
|
||||
private var family: WidgetFamily { previewFamily ?? envFamily }
|
||||
let entry: QuestRateEntry
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if entry.locked {
|
||||
ComplicationLockedView()
|
||||
} else if let quest = entry.quest {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
Text("\(quest.name) \(inlineText(quest))")
|
||||
case .accessoryRectangular:
|
||||
// 크기가 큰 패밀리는 하루/주간/월간 세 개를 모두 표기 (스펙 §9.2-3)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: quest.symbolName)
|
||||
.font(.system(size: 11))
|
||||
Text(quest.name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
if quest.isAtMost {
|
||||
Text("한도")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.padding(.horizontal, 3)
|
||||
.background(.secondary.opacity(0.3), in: Capsule())
|
||||
}
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
spanGauge(quest, raw: "day", label: "하루")
|
||||
spanGauge(quest, raw: "week", label: "주간")
|
||||
spanGauge(quest, raw: "month", label: "월간")
|
||||
}
|
||||
}
|
||||
default:
|
||||
circular(quest)
|
||||
}
|
||||
} else {
|
||||
ComplicationEmptyView(message: "다짐 없음")
|
||||
}
|
||||
}
|
||||
.containerBackground(.clear, for: .widget)
|
||||
}
|
||||
|
||||
/// '이하 유지' 다짐은 게이지 대신 한도 상태로 표현 (스펙 §9.2-3)
|
||||
@ViewBuilder
|
||||
private func circular(_ quest: WatchQuestInfo) -> some View {
|
||||
if quest.isAtMost {
|
||||
VStack(spacing: 1) {
|
||||
Image(systemName: quest.achieved(forSpanRaw: entry.spanRaw)
|
||||
? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 16))
|
||||
Text(quest.achieved(forSpanRaw: entry.spanRaw) ? "한도 안" : "초과")
|
||||
.font(.system(size: 9))
|
||||
}
|
||||
} else {
|
||||
Gauge(value: min(max(quest.ratio(forSpanRaw: entry.spanRaw), 0), 1)) {
|
||||
Image(systemName: quest.symbolName)
|
||||
} currentValueLabel: {
|
||||
Text("\(Int((quest.ratio(forSpanRaw: entry.spanRaw) * 100).rounded()))")
|
||||
.font(.system(size: 13, weight: .bold).monospacedDigit())
|
||||
}
|
||||
.gaugeStyle(.accessoryCircular)
|
||||
}
|
||||
}
|
||||
|
||||
private func inlineText(_ quest: WatchQuestInfo) -> String {
|
||||
if quest.isAtMost {
|
||||
return quest.achieved(forSpanRaw: entry.spanRaw) ? "한도 안" : "한도 초과"
|
||||
}
|
||||
return "\(Int((quest.ratio(forSpanRaw: entry.spanRaw) * 100).rounded()))%"
|
||||
}
|
||||
|
||||
private func spanGauge(_ quest: WatchQuestInfo, raw: String, label: String) -> some View {
|
||||
VStack(spacing: 1) {
|
||||
Text(label)
|
||||
.font(.system(size: 8))
|
||||
.foregroundStyle(.secondary)
|
||||
if quest.isAtMost {
|
||||
Image(systemName: quest.achieved(forSpanRaw: raw) ? "checkmark.circle.fill" : "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 12))
|
||||
} else {
|
||||
Text("\(Int((quest.ratio(forSpanRaw: raw) * 100).rounded()))%")
|
||||
.font(.system(size: 11, weight: .semibold).monospacedDigit())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct QuestRateComplication: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
AppIntentConfiguration(
|
||||
kind: "HaruWatchQuestRate",
|
||||
intent: QuestRateConfigIntent.self,
|
||||
provider: QuestRateProvider()
|
||||
) { entry in
|
||||
QuestRateComplicationView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("다짐 달성률")
|
||||
.description("선택한 다짐의 달성률을 표시해요. 한도형 다짐은 상태로 표시돼요. (프리미엄)")
|
||||
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner])
|
||||
}
|
||||
}
|
||||
150
myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift
Normal file
150
myApp/HaruDanim/Haru_DanimWatchWidgets/StatusComplication.swift
Normal file
@ -0,0 +1,150 @@
|
||||
//
|
||||
// StatusComplication.swift
|
||||
// Haru_DanimWatchWidgetsExtension
|
||||
//
|
||||
// 현재 현황 컴플리케이션 (CLAUDE.md §9.2-1)
|
||||
// - 측정 중인 대표 행동(다이나믹 아일랜드와 동일 기준) + 추가 측정 개수
|
||||
// - 실행 중인 것이 없으면 "실행 중 아님"
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct StatusEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let locked: Bool
|
||||
let runningName: String?
|
||||
let runningSymbol: String?
|
||||
let runningStartAt: Date?
|
||||
let extraCount: Int
|
||||
}
|
||||
|
||||
struct StatusProvider: TimelineProvider {
|
||||
static func makeEntry() -> StatusEntry {
|
||||
guard let snapshot = ComplicationStore.snapshot() else {
|
||||
return StatusEntry(date: .now, locked: false, runningName: nil, runningSymbol: nil,
|
||||
runningStartAt: nil, extraCount: 0)
|
||||
}
|
||||
guard snapshot.isPremium else {
|
||||
return StatusEntry(date: .now, locked: true, runningName: nil, runningSymbol: nil,
|
||||
runningStartAt: nil, extraCount: 0)
|
||||
}
|
||||
return StatusEntry(
|
||||
date: .now,
|
||||
locked: false,
|
||||
runningName: snapshot.runningName,
|
||||
runningSymbol: snapshot.runningSymbol,
|
||||
runningStartAt: snapshot.runningStartAt,
|
||||
extraCount: snapshot.extraRunningCount
|
||||
)
|
||||
}
|
||||
|
||||
func placeholder(in context: Context) -> StatusEntry {
|
||||
StatusEntry(date: .now, locked: false, runningName: "독서", runningSymbol: "book.fill",
|
||||
runningStartAt: .now, extraCount: 0)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (StatusEntry) -> Void) {
|
||||
completion(Self.makeEntry())
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<StatusEntry>) -> Void) {
|
||||
completion(Timeline(entries: [Self.makeEntry()], policy: .after(.now.addingTimeInterval(15 * 60))))
|
||||
}
|
||||
}
|
||||
|
||||
struct StatusComplicationView: View {
|
||||
@Environment(\.widgetFamily) private var envFamily
|
||||
/// 워치 앱 내 DEBUG 미리보기에서 패밀리를 강제할 때 사용
|
||||
var previewFamily: WidgetFamily? = nil
|
||||
private var family: WidgetFamily { previewFamily ?? envFamily }
|
||||
let entry: StatusEntry
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if entry.locked {
|
||||
ComplicationLockedView()
|
||||
} else if let name = entry.runningName {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
if entry.extraCount > 0 {
|
||||
Text("\(name) +\(entry.extraCount)")
|
||||
} else {
|
||||
Text(name)
|
||||
}
|
||||
case .accessoryRectangular:
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: entry.runningSymbol ?? "record.circle")
|
||||
.font(.system(size: 11))
|
||||
Text(name)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
// 추가 측정이 있을 때만 개수 표시 (스펙 §9.2-1)
|
||||
if entry.extraCount > 0 {
|
||||
Text("+\(entry.extraCount)")
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let start = entry.runningStartAt {
|
||||
Text(start, style: .timer)
|
||||
.font(.system(.body, design: .rounded).monospacedDigit())
|
||||
}
|
||||
Text("측정 중")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
default:
|
||||
VStack(spacing: 1) {
|
||||
Image(systemName: entry.runningSymbol ?? "record.circle")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
if let start = entry.runningStartAt {
|
||||
Text(start, style: .timer)
|
||||
.font(.system(size: 11).monospacedDigit())
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
if entry.extraCount > 0 {
|
||||
Text("+\(entry.extraCount)")
|
||||
.font(.system(size: 9, weight: .bold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch family {
|
||||
case .accessoryInline:
|
||||
Text("실행 중 아님")
|
||||
case .accessoryRectangular:
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text("하루다님")
|
||||
.font(.headline)
|
||||
Text("실행 중 아님")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
default:
|
||||
VStack(spacing: 2) {
|
||||
Image(systemName: "pause.circle")
|
||||
.font(.system(size: 16))
|
||||
Text("없음")
|
||||
.font(.system(size: 9))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.containerBackground(.clear, for: .widget)
|
||||
}
|
||||
}
|
||||
|
||||
struct StatusComplication: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: "HaruWatchStatus", provider: StatusProvider()) { entry in
|
||||
StatusComplicationView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("현재 현황")
|
||||
.description("측정 중인 행동과 경과 시간을 표시해요. (프리미엄)")
|
||||
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline, .accessoryCorner])
|
||||
}
|
||||
}
|
||||
10
myApp/HaruDanim/Haru_DanimWatchWidgetsExtension.entitlements
Normal file
10
myApp/HaruDanim/Haru_DanimWatchWidgetsExtension.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.yechan.HaruDanim</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Loading…
x
Reference in New Issue
Block a user