Complete free features and prototype; testing to follow
This commit is contained in:
parent
c854c8f265
commit
3000ce6713
@ -6,16 +6,68 @@
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
AA000000000000000000000E /* Haru_DanimWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000004 /* Haru_DanimWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
AA000000000000000000000B /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 725A9E7C2FFCC65000846FFB /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = AA0000000000000000000003;
|
||||
remoteInfo = Haru_DanimWidgets;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
AA000000000000000000000D /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
AA000000000000000000000E /* Haru_DanimWidgets.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
725A9E842FFCC65000846FFB /* Haru_Danim.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Haru_Danim.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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 */
|
||||
AA000000000000000000000F /* Exceptions for "Widgets" folder in "Haru_DanimWidgets" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = AA0000000000000000000003 /* Haru_DanimWidgets */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
725A9E862FFCC65000846FFB /* IOS */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = IOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA0000000000000000000001 /* Widgets */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
AA000000000000000000000F /* Exceptions for "Widgets" folder in "Haru_DanimWidgets" target */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AA0000000000000000000002 /* Shared */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -26,6 +78,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000006 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@ -33,6 +92,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
725A9E862FFCC65000846FFB /* IOS */,
|
||||
AA0000000000000000000002 /* Shared */,
|
||||
AA0000000000000000000001 /* Widgets */,
|
||||
725A9E852FFCC65000846FFB /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@ -41,6 +102,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
725A9E842FFCC65000846FFB /* Haru_Danim.app */,
|
||||
AA0000000000000000000004 /* Haru_DanimWidgets.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -55,13 +117,16 @@
|
||||
725A9E802FFCC65000846FFB /* Sources */,
|
||||
725A9E812FFCC65000846FFB /* Frameworks */,
|
||||
725A9E822FFCC65000846FFB /* Resources */,
|
||||
AA000000000000000000000D /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
AA000000000000000000000C /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
725A9E862FFCC65000846FFB /* IOS */,
|
||||
AA0000000000000000000002 /* Shared */,
|
||||
);
|
||||
name = Haru_Danim;
|
||||
packageProductDependencies = (
|
||||
@ -70,6 +135,29 @@
|
||||
productReference = 725A9E842FFCC65000846FFB /* Haru_Danim.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
AA0000000000000000000003 /* Haru_DanimWidgets */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Haru_DanimWidgets" */;
|
||||
buildPhases = (
|
||||
AA0000000000000000000005 /* Sources */,
|
||||
AA0000000000000000000006 /* Frameworks */,
|
||||
AA0000000000000000000007 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
AA0000000000000000000001 /* Widgets */,
|
||||
AA0000000000000000000002 /* Shared */,
|
||||
);
|
||||
name = Haru_DanimWidgets;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = Haru_DanimWidgets;
|
||||
productReference = AA0000000000000000000004 /* Haru_DanimWidgets.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@ -83,6 +171,9 @@
|
||||
725A9E832FFCC65000846FFB = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
AA0000000000000000000003 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 725A9E7F2FFCC65000846FFB /* Build configuration list for PBXProject "Haru_Danim" */;
|
||||
@ -100,6 +191,7 @@
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
725A9E832FFCC65000846FFB /* Haru_Danim */,
|
||||
AA0000000000000000000003 /* Haru_DanimWidgets */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -112,6 +204,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000007 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@ -122,8 +221,23 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
AA0000000000000000000005 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
AA000000000000000000000C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = AA0000000000000000000003 /* Haru_DanimWidgets */;
|
||||
targetProxy = AA000000000000000000000B /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
725A9E8D2FFCC65100846FFB /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@ -254,6 +368,7 @@
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -286,6 +401,7 @@
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@ -308,6 +424,56 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
AA0000000000000000000008 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "하루 다님 위젯";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.Haru-Danim.Widgets";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
AA0000000000000000000009 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "하루 다님 위젯";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.yechan.Haru-Danim.Widgets";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@ -329,6 +495,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
AA000000000000000000000A /* Build configuration list for PBXNativeTarget "Haru_DanimWidgets" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
AA0000000000000000000008 /* Debug */,
|
||||
AA0000000000000000000009 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 725A9E7C2FFCC65000846FFB /* Project object */;
|
||||
|
||||
Binary file not shown.
@ -9,6 +9,11 @@
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Haru_DanimWidgets.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 948 KiB |
@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
@ -12,6 +13,7 @@
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
@ -23,6 +25,7 @@
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"filename" : "AppIcon.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
||||
BIN
myApp/Haru_Danim/IOS/Assets.xcassets/AppLogo.imageset/AppLogo.png
vendored
Normal file
BIN
myApp/Haru_Danim/IOS/Assets.xcassets/AppLogo.imageset/AppLogo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 411 KiB |
21
myApp/Haru_Danim/IOS/Assets.xcassets/AppLogo.imageset/Contents.json
vendored
Normal file
21
myApp/Haru_Danim/IOS/Assets.xcassets/AppLogo.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "AppLogo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ struct ContentView: View {
|
||||
.task {
|
||||
#if DEBUG
|
||||
DebugSeed.seedIfRequested(context: context)
|
||||
DebugSeed.autoStartIfRequested(context: context)
|
||||
#endif
|
||||
try? await Task.sleep(for: .seconds(1.2))
|
||||
withAnimation(.easeOut(duration: 0.4)) {
|
||||
@ -36,27 +37,37 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 스플래시 (로고는 플레이스홀더, CLAUDE.md §5.1)
|
||||
// MARK: - 스플래시 (앱 로고 + 앱 이름, CLAUDE.md §5.1)
|
||||
|
||||
struct SplashView: View {
|
||||
@State private var appeared = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// 로고 배경(웜 화이트)과 이어지도록 라이트/다크 공통 배경 사용
|
||||
AppTheme.background.ignoresSafeArea()
|
||||
VStack(spacing: 20) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 28, style: .continuous)
|
||||
.fill(AppTheme.green)
|
||||
.frame(width: 110, height: 110)
|
||||
Image(systemName: "figure.walk")
|
||||
.font(.system(size: 52, weight: .medium))
|
||||
.foregroundStyle(AppTheme.yellow)
|
||||
}
|
||||
VStack(spacing: 18) {
|
||||
Image("AppLogo")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 150, height: 150)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 34, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.12), radius: 14, y: 6)
|
||||
.scaleEffect(appeared ? 1 : 0.85)
|
||||
.opacity(appeared ? 1 : 0)
|
||||
Text("하루 다님")
|
||||
.font(.largeTitle.bold())
|
||||
.foregroundStyle(AppTheme.green)
|
||||
.opacity(appeared ? 1 : 0)
|
||||
Text("하루 습관 및 시간 추적")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(appeared ? 1 : 0)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.spring(duration: 0.5)) {
|
||||
appeared = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,48 +115,55 @@ struct MainTabView: View {
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
switch selection {
|
||||
case .main: MainView()
|
||||
case .action: ActionListView()
|
||||
case .tag: TagListView()
|
||||
case .goal: GoalListView()
|
||||
case .history: HistoryView()
|
||||
case .settings: SettingsView()
|
||||
}
|
||||
ZStack {
|
||||
switch selection {
|
||||
case .main: MainView()
|
||||
case .action: ActionListView()
|
||||
case .tag: TagListView()
|
||||
case .goal: GoalListView()
|
||||
case .history: HistoryView()
|
||||
case .settings: SettingsView()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
tabBar
|
||||
}
|
||||
.background(AppTheme.background)
|
||||
}
|
||||
|
||||
/// 리퀴드 글라스 스타일의 플로팅 탭바
|
||||
private var tabBar: some View {
|
||||
HStack {
|
||||
HStack(spacing: 0) {
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
Button {
|
||||
selection = tab
|
||||
withAnimation(.spring(duration: 0.25)) {
|
||||
selection = tab
|
||||
}
|
||||
} label: {
|
||||
VStack(spacing: 3) {
|
||||
Image(systemName: tab.symbol)
|
||||
.font(.system(size: 20))
|
||||
.font(.system(size: 19, weight: .medium))
|
||||
Text(tab.label)
|
||||
.font(.system(size: 10))
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
.background {
|
||||
if selection == tab {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(AppTheme.green.opacity(0.16))
|
||||
}
|
||||
}
|
||||
.foregroundStyle(selection == tab ? AppTheme.green : Color.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 2)
|
||||
.background(.bar)
|
||||
.overlay(alignment: .top) {
|
||||
Divider()
|
||||
}
|
||||
.padding(6)
|
||||
.glassEffect(.regular, in: RoundedRectangle(cornerRadius: 26, style: .continuous))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 4)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,17 @@ import Foundation
|
||||
import SwiftData
|
||||
|
||||
enum DebugSeed {
|
||||
/// `-autoStart <행동이름>` 런치 인자가 있으면 해당 행동의 시간 측정을 시작 (Live Activity 검증용)
|
||||
static func autoStartIfRequested(context: ModelContext) {
|
||||
guard let name = UserDefaults.standard.string(forKey: "autoStart") else { return }
|
||||
let descriptor = FetchDescriptor<Action>(predicate: #Predicate { $0.name == name })
|
||||
guard let action = try? context.fetch(descriptor).first else { return }
|
||||
if action.runningSession == nil {
|
||||
context.insert(TimeSession(action: action, startAt: .now))
|
||||
}
|
||||
LiveActivityManager.sync(context: context)
|
||||
}
|
||||
|
||||
static func seedIfRequested(context: ModelContext) {
|
||||
guard UserDefaults.standard.bool(forKey: "seedDemo") else { return }
|
||||
let existing = (try? context.fetchCount(FetchDescriptor<Action>())) ?? 0
|
||||
|
||||
67
myApp/Haru_Danim/IOS/Core/LiveActivityManager.swift
Normal file
67
myApp/Haru_Danim/IOS/Core/LiveActivityManager.swift
Normal file
@ -0,0 +1,67 @@
|
||||
//
|
||||
// LiveActivityManager.swift
|
||||
// Haru_Danim
|
||||
//
|
||||
// 추적 상태를 다이나믹 아일랜드/잠금화면 Live Activity에 반영 (CLAUDE.md §4.1, §4.2)
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
import SwiftData
|
||||
|
||||
enum LiveActivityManager {
|
||||
/// 현재 진행 중인 세션들을 읽어 Live Activity를 시작/갱신/종료한다.
|
||||
/// 세션 시작·종료·수정 후마다 호출.
|
||||
static func sync(context: ModelContext) {
|
||||
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
|
||||
|
||||
let descriptor = FetchDescriptor<TimeSession>(
|
||||
predicate: #Predicate { $0.endAt == nil },
|
||||
sortBy: [SortDescriptor(\.startAt, order: .forward)]
|
||||
)
|
||||
let running = (try? context.fetch(descriptor)) ?? []
|
||||
|
||||
let mode = LiveActivityMode(
|
||||
rawValue: UserDefaults.standard.string(forKey: SettingsKeys.liveActivityMode) ?? ""
|
||||
) ?? .latest
|
||||
let representative = mode == .earliest ? running.first : running.last
|
||||
|
||||
guard let representative, let action = representative.action else {
|
||||
endAll()
|
||||
return
|
||||
}
|
||||
|
||||
let state = TrackingActivityAttributes.ContentState(
|
||||
actionName: action.name,
|
||||
symbolName: action.symbolName,
|
||||
colorHex: action.sortedTags.first?.colorHex ?? "#2F6B4F",
|
||||
startedAt: representative.startAt,
|
||||
extraCount: max(running.count - 1, 0)
|
||||
)
|
||||
let content = ActivityContent(state: state, staleDate: nil)
|
||||
|
||||
Task {
|
||||
let activities = Activity<TrackingActivityAttributes>.activities
|
||||
if let activity = activities.first {
|
||||
await activity.update(content)
|
||||
// 혹시 중복 생성된 것이 있으면 정리
|
||||
for extra in activities.dropFirst() {
|
||||
await extra.end(nil, dismissalPolicy: .immediate)
|
||||
}
|
||||
} else {
|
||||
_ = try? Activity<TrackingActivityAttributes>.request(
|
||||
attributes: TrackingActivityAttributes(),
|
||||
content: content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func endAll() {
|
||||
Task {
|
||||
for activity in Activity<TrackingActivityAttributes>.activities {
|
||||
await activity.end(nil, dismissalPolicy: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,6 +20,22 @@ enum SettingsKeys {
|
||||
static let liveActivityMode = "settings.liveActivityMode"
|
||||
/// 프리미엄 결제 여부
|
||||
static let isPremium = "settings.isPremium"
|
||||
/// 메인 탭 그리드 한 줄에 표시할 행동 버튼 개수. 기본값 3
|
||||
static let gridColumns = "settings.gridColumns"
|
||||
/// 이 시간(초)보다 짧게 측정된 세션은 기록하지 않음. 0 = 사용 안 함
|
||||
static let minSessionSeconds = "settings.minSessionSeconds"
|
||||
}
|
||||
|
||||
/// 짧은 기록 무시 옵션 선택지 (초)
|
||||
enum MinSessionOption {
|
||||
static let choices: [(seconds: Int, label: String)] = [
|
||||
(0, "사용 안 함"),
|
||||
(3, "3초"),
|
||||
(5, "5초"),
|
||||
(10, "10초"),
|
||||
(30, "30초"),
|
||||
(60, "1분"),
|
||||
]
|
||||
}
|
||||
|
||||
enum LiveActivityMode: String, CaseIterable, Identifiable {
|
||||
|
||||
@ -15,8 +15,16 @@ struct MainView: View {
|
||||
@Query(filter: #Predicate<TimeSession> { $0.endAt == nil }, sort: \TimeSession.startAt)
|
||||
private var runningSessions: [TimeSession]
|
||||
|
||||
@State private var isEditing = false
|
||||
@State private var jiggle = false
|
||||
@AppStorage(SettingsKeys.gridColumns) private var gridColumns = 3
|
||||
@AppStorage(SettingsKeys.minSessionSeconds) private var minSessionSeconds = 0
|
||||
|
||||
@State private var isEditing: Bool = {
|
||||
#if DEBUG
|
||||
return UserDefaults.standard.bool(forKey: "startEditing")
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}()
|
||||
@State private var draggingAction: Action?
|
||||
@State private var settingsEditingAction: Action?
|
||||
@State private var recordsAction: Action?
|
||||
@ -28,9 +36,12 @@ struct MainView: View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if anyRunning {
|
||||
if anyRunning && !isEditing {
|
||||
runningArea
|
||||
}
|
||||
if isEditing {
|
||||
layoutControl
|
||||
}
|
||||
if actions.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
@ -46,7 +57,6 @@ struct MainView: View {
|
||||
Button(isEditing ? "완료" : "배치 편집") {
|
||||
withAnimation {
|
||||
isEditing.toggle()
|
||||
jiggle = isEditing
|
||||
}
|
||||
}
|
||||
.fontWeight(isEditing ? .bold : .regular)
|
||||
@ -69,6 +79,7 @@ struct MainView: View {
|
||||
Button("삭제", role: .destructive) {
|
||||
if let action = deletingAction {
|
||||
context.delete(action)
|
||||
LiveActivityManager.sync(context: context)
|
||||
}
|
||||
deletingAction = nil
|
||||
}
|
||||
@ -96,27 +107,48 @@ struct MainView: View {
|
||||
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
}
|
||||
|
||||
// MARK: 배치 편집 컨트롤 (한 줄 개수 선택)
|
||||
|
||||
private var layoutControl: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Label("한 줄에 표시할 개수", systemImage: "square.grid.3x3")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(AppTheme.green)
|
||||
Picker("한 줄에 표시할 개수", selection: $gridColumns) {
|
||||
ForEach(2...6, id: \.self) { n in
|
||||
Text("\(n)개").tag(n)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Text("버튼을 길게 눌러 끌면 순서를 바꿀 수 있어요.")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
}
|
||||
|
||||
// MARK: 그리드
|
||||
|
||||
private var grid: some View {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 104), spacing: 12)], spacing: 12) {
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: gridColumns),
|
||||
spacing: 12
|
||||
) {
|
||||
ForEach(actions) { action in
|
||||
cell(for: action)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: actions.map(\.persistentModelID))
|
||||
.animation(.default, value: gridColumns)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func cell(for action: Action) -> some View {
|
||||
let base = ActionButtonCell(action: action, isEditing: isEditing) {
|
||||
let base = ActionButtonCell(action: action, isEditing: isEditing, compact: gridColumns >= 5) {
|
||||
handleTap(action)
|
||||
}
|
||||
.rotationEffect(.degrees(isEditing ? (jiggle ? 1.6 : -1.6) : 0))
|
||||
.animation(
|
||||
isEditing ? .easeInOut(duration: 0.13).repeatForever(autoreverses: true) : .default,
|
||||
value: jiggle
|
||||
)
|
||||
.modifier(JiggleEffect(active: isEditing))
|
||||
|
||||
if isEditing {
|
||||
base
|
||||
@ -179,17 +211,28 @@ struct MainView: View {
|
||||
switch action.trackingType {
|
||||
case .time:
|
||||
if let session = action.runningSession {
|
||||
session.endAt = .now
|
||||
finish(session)
|
||||
} else {
|
||||
context.insert(TimeSession(action: action, startAt: .now))
|
||||
LiveActivityManager.sync(context: context)
|
||||
}
|
||||
case .count:
|
||||
context.insert(CountEntry(action: action, timestamp: .now))
|
||||
}
|
||||
}
|
||||
|
||||
/// 세션 종료. "짧은 기록 무시" 설정보다 짧으면 기록하지 않고 삭제 (실수 방지)
|
||||
private func finish(_ session: TimeSession) {
|
||||
if minSessionSeconds > 0, session.duration() < Double(minSessionSeconds) {
|
||||
context.delete(session)
|
||||
} else {
|
||||
session.endAt = .now
|
||||
}
|
||||
LiveActivityManager.sync(context: context)
|
||||
}
|
||||
|
||||
private func stop(_ session: TimeSession) {
|
||||
session.endAt = .now
|
||||
finish(session)
|
||||
}
|
||||
|
||||
private func moveDragging(to target: Action) {
|
||||
@ -205,6 +248,38 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 지글(흔들림) 애니메이션
|
||||
|
||||
struct JiggleEffect: ViewModifier {
|
||||
let active: Bool
|
||||
@State private var angle: Double = 0
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.rotationEffect(.degrees(angle))
|
||||
.onChange(of: active) { _, on in
|
||||
if on {
|
||||
angle = -1.7
|
||||
withAnimation(.easeInOut(duration: 0.13).repeatForever(autoreverses: true)) {
|
||||
angle = 1.7
|
||||
}
|
||||
} else {
|
||||
withAnimation(.easeOut(duration: 0.15)) {
|
||||
angle = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if active {
|
||||
angle = -1.7
|
||||
withAnimation(.easeInOut(duration: 0.13).repeatForever(autoreverses: true)) {
|
||||
angle = 1.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 드래그 순서 변경
|
||||
|
||||
private struct ActionReorderDelegate: DropDelegate {
|
||||
@ -268,10 +343,13 @@ struct RunningSessionRow: View {
|
||||
struct ActionButtonCell: View {
|
||||
let action: Action
|
||||
let isEditing: Bool
|
||||
var compact: Bool = false
|
||||
let onTap: () -> Void
|
||||
|
||||
@State private var bounce = false
|
||||
|
||||
private var cornerRadius: CGFloat { compact ? 16 : 22 }
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
onTap()
|
||||
@ -279,13 +357,13 @@ struct ActionButtonCell: View {
|
||||
bounce.toggle()
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
VStack(alignment: .leading, spacing: compact ? 3 : 6) {
|
||||
HStack {
|
||||
Image(systemName: action.symbolName)
|
||||
.font(.system(size: 26, weight: .medium))
|
||||
.font(.system(size: compact ? 17 : 26, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
Spacer()
|
||||
if action.isRunning {
|
||||
if action.isRunning && !compact {
|
||||
Image(systemName: "record.circle")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundStyle(AppTheme.yellow)
|
||||
@ -294,26 +372,25 @@ struct ActionButtonCell: View {
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Text(action.name)
|
||||
.font(.footnote.weight(.semibold))
|
||||
.font(compact ? .caption2.weight(.semibold) : .footnote.weight(.semibold))
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(2)
|
||||
.lineLimit(compact ? 1 : 2)
|
||||
.multilineTextAlignment(.leading)
|
||||
footer
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 104, alignment: .topLeading)
|
||||
.padding(compact ? 8 : 12)
|
||||
.frame(minHeight: compact ? 68 : 104, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.fill(action.color.gradient)
|
||||
)
|
||||
.overlay {
|
||||
if action.isRunning {
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.strokeBorder(AppTheme.yellow, lineWidth: 3)
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.strokeBorder(AppTheme.yellow, lineWidth: compact ? 2 : 3)
|
||||
}
|
||||
}
|
||||
.scaleEffect(bounce ? 1.0 : 1.0)
|
||||
}
|
||||
.buttonStyle(PressableButtonStyle())
|
||||
.sensoryFeedback(.impact(weight: .light), trigger: bounce)
|
||||
@ -327,18 +404,20 @@ struct ActionButtonCell: View {
|
||||
if let session = action.runningSession {
|
||||
TimelineView(.periodic(from: .now, by: 1)) { timeline in
|
||||
Text(Format.timer(timeline.date.timeIntervalSince(session.startAt)))
|
||||
.font(.caption.weight(.bold).monospacedDigit())
|
||||
.font((compact ? Font.caption2 : Font.caption).weight(.bold).monospacedDigit())
|
||||
.foregroundStyle(AppTheme.yellow)
|
||||
}
|
||||
} else {
|
||||
Text(todayLabel)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
.lineLimit(1)
|
||||
}
|
||||
case .count:
|
||||
Text(todayLabel)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ struct ActionRecordsSheet: View {
|
||||
.swipeActions {
|
||||
Button("삭제", role: .destructive) {
|
||||
context.delete(session)
|
||||
LiveActivityManager.sync(context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,6 +182,7 @@ struct SessionEditorView: View {
|
||||
Section {
|
||||
Button("기록 삭제", role: .destructive) {
|
||||
context.delete(session)
|
||||
LiveActivityManager.sync(context: context)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@ -195,6 +197,7 @@ struct SessionEditorView: View {
|
||||
Button("저장") {
|
||||
session.startAt = startAt
|
||||
session.endAt = isFinished ? max(endAt, startAt) : nil
|
||||
LiveActivityManager.sync(context: context)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ struct SettingsView: View {
|
||||
@AppStorage(SettingsKeys.dayStartMinutes) private var dayStartMinutes = 0
|
||||
@AppStorage(SettingsKeys.liveActivityMode) private var liveActivityMode = LiveActivityMode.latest.rawValue
|
||||
@AppStorage(SettingsKeys.isPremium) private var isPremium = false
|
||||
@AppStorage(SettingsKeys.minSessionSeconds) private var minSessionSeconds = 0
|
||||
|
||||
/// 하루 시작 시간을 DatePicker와 연결하기 위한 변환
|
||||
private var dayStartDate: Binding<Date> {
|
||||
@ -53,6 +54,17 @@ struct SettingsView: View {
|
||||
} footer: {
|
||||
Text("하루 시작 시간을 걸치는 기록은 통계에서 자동으로 날짜별로 나누어 계산돼요.")
|
||||
}
|
||||
Section {
|
||||
Picker("짧은 기록 무시", selection: $minSessionSeconds) {
|
||||
ForEach(MinSessionOption.choices, id: \.seconds) { option in
|
||||
Text(option.label).tag(option.seconds)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("측정")
|
||||
} footer: {
|
||||
Text("설정한 시간보다 짧게 측정하고 종료한 기록은 저장하지 않아요. 버튼을 실수로 눌렀을 때 유용해요.")
|
||||
}
|
||||
Section {
|
||||
Picker("대표 시간 기준", selection: $liveActivityMode) {
|
||||
ForEach(LiveActivityMode.allCases) { mode in
|
||||
|
||||
40
myApp/Haru_Danim/Shared/TrackingActivityAttributes.swift
Normal file
40
myApp/Haru_Danim/Shared/TrackingActivityAttributes.swift
Normal file
@ -0,0 +1,40 @@
|
||||
//
|
||||
// TrackingActivityAttributes.swift
|
||||
// Haru_Danim
|
||||
//
|
||||
// Live Activity 상태 정의. 앱 타깃과 위젯 확장 타깃 양쪽에 포함된다.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
import SwiftUI
|
||||
|
||||
nonisolated struct TrackingActivityAttributes: ActivityAttributes {
|
||||
nonisolated struct ContentState: Codable, Hashable {
|
||||
/// 대표로 표시할 행동 (설정: 가장 먼저/나중에 시작한 행동)
|
||||
var actionName: String
|
||||
var symbolName: String
|
||||
var colorHex: String
|
||||
var startedAt: Date
|
||||
/// 동시에 추적 중인 추가 행동 개수 (+1, +2 표시용)
|
||||
var extraCount: Int
|
||||
}
|
||||
}
|
||||
|
||||
extension TrackingActivityAttributes.ContentState {
|
||||
var tintColor: Color {
|
||||
let cleaned = colorHex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||||
var value: UInt64 = 0
|
||||
Scanner(string: cleaned).scanHexInt64(&value)
|
||||
return Color(
|
||||
red: Double((value >> 16) & 0xFF) / 255,
|
||||
green: Double((value >> 8) & 0xFF) / 255,
|
||||
blue: Double(value & 0xFF) / 255
|
||||
)
|
||||
}
|
||||
|
||||
/// 타이머 표시용 구간 (상한은 충분히 먼 미래)
|
||||
var timerRange: ClosedRange<Date> {
|
||||
startedAt...startedAt.addingTimeInterval(60 * 60 * 24 * 30)
|
||||
}
|
||||
}
|
||||
118
myApp/Haru_Danim/Widgets/HaruDanimWidgets.swift
Normal file
118
myApp/Haru_Danim/Widgets/HaruDanimWidgets.swift
Normal file
@ -0,0 +1,118 @@
|
||||
//
|
||||
// HaruDanimWidgets.swift
|
||||
// Haru_DanimWidgets
|
||||
//
|
||||
// Live Activity: 잠금화면 배너 + 다이나믹 아일랜드 (CLAUDE.md §4.1, §4.2)
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
@main
|
||||
struct HaruDanimWidgetsBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
TrackingLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
struct TrackingLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: TrackingActivityAttributes.self) { context in
|
||||
LockScreenActivityView(state: context.state)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: context.state.symbolName)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
.background(
|
||||
context.state.tintColor,
|
||||
in: RoundedRectangle(cornerRadius: 9, style: .continuous)
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(context.state.actionName)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
Text("측정 중")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
Text(timerInterval: context.state.timerRange, countsDown: false)
|
||||
.font(.title3.weight(.bold).monospacedDigit())
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(maxWidth: 76)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
if context.state.extraCount > 0 {
|
||||
Text("다른 행동 \(context.state.extraCount)개도 함께 추적 중")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
Image(systemName: context.state.symbolName)
|
||||
.foregroundStyle(context.state.tintColor)
|
||||
} compactTrailing: {
|
||||
HStack(spacing: 2) {
|
||||
Text(timerInterval: context.state.timerRange, countsDown: false)
|
||||
.font(.caption.weight(.semibold).monospacedDigit())
|
||||
.frame(maxWidth: 52)
|
||||
.multilineTextAlignment(.trailing)
|
||||
if context.state.extraCount > 0 {
|
||||
Text("+\(context.state.extraCount)")
|
||||
.font(.caption2.weight(.bold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} minimal: {
|
||||
Image(systemName: context.state.symbolName)
|
||||
.foregroundStyle(context.state.tintColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 잠금화면 배너
|
||||
|
||||
struct LockScreenActivityView: View {
|
||||
let state: TrackingActivityAttributes.ContentState
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: state.symbolName)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(
|
||||
state.tintColor,
|
||||
in: RoundedRectangle(cornerRadius: 11, style: .continuous)
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(state.actionName)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
if state.extraCount > 0 {
|
||||
Text("+\(state.extraCount)개 함께 추적 중")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("측정 중")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Text(timerInterval: state.timerRange, countsDown: false)
|
||||
.font(.title2.weight(.bold).monospacedDigit())
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(maxWidth: 90)
|
||||
}
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
11
myApp/Haru_Danim/Widgets/Info.plist
Normal file
11
myApp/Haru_Danim/Widgets/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>
|
||||
BIN
myApp/Haru_Danim/icon.heic
Normal file
BIN
myApp/Haru_Danim/icon.heic
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user