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:
songyc macbook 2026-07-10 09:00:15 +09:00
parent f9f1898d15
commit 1758cc3a25
12 changed files with 1132 additions and 0 deletions

View File

@ -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 = (

View File

@ -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

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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)
}
}

View File

@ -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()
}
}

View 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>

View 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])
}
}

View 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])
}
}

View 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>