diff --git a/Andromida.xcodeproj/project.pbxproj b/Andromida.xcodeproj/project.pbxproj index f37707e..5b4f52d 100644 --- a/Andromida.xcodeproj/project.pbxproj +++ b/Andromida.xcodeproj/project.pbxproj @@ -8,6 +8,10 @@ /* Begin PBXBuildFile section */ EAC04AEE2F26BD5B007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC04AED2F26BD5B007F87EA /* Bedrock */; }; + EAC04D322F298D9B007F87EA /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAC04D312F298D9B007F87EA /* WidgetKit.framework */; }; + EAC04D342F298D9B007F87EA /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAC04D332F298D9B007F87EA /* SwiftUI.framework */; }; + EAC04D412F298D9C007F87EA /* AndromidaWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + EAC04E2C2F2998B2007F87EA /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EAC04E2B2F2998B2007F87EA /* Bedrock */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -25,17 +29,67 @@ remoteGlobalIDString = EAC04A972F26BAE8007F87EA; remoteInfo = Andromida; }; + EAC04D3F2F298D9C007F87EA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EAC04A902F26BAE8007F87EA /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAC04D2E2F298D9B007F87EA; + remoteInfo = AndromidaWidgetExtension; + }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + EAC04D462F298D9C007F87EA /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + EAC04D412F298D9C007F87EA /* AndromidaWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ EAC04A982F26BAE8007F87EA /* Andromida.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Andromida.app; sourceTree = BUILT_PRODUCTS_DIR; }; EAC04AA52F26BAE9007F87EA /* AndromidaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AndromidaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AndromidaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AndromidaWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + EAC04D312F298D9B007F87EA /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + EAC04D332F298D9B007F87EA /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + EAC04D4D2F298DD9007F87EA /* AndromidaWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AndromidaWidgetExtension.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + EAC04D422F298D9C007F87EA /* Exceptions for "AndromidaWidget" folder in "AndromidaWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */; + }; + EAC04D512F298EAE007F87EA /* Exceptions for "Andromida" folder in "AndromidaWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + App/Models/ArcHabit.swift, + App/Models/Ritual.swift, + App/Models/RitualArc.swift, + Assets.xcassets, + Shared/Configuration/AppIdentifiers.swift, + Shared/Services/RitualAnalytics.swift, + ); + target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ EAC04A9A2F26BAE8007F87EA /* Andromida */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + EAC04D512F298EAE007F87EA /* Exceptions for "Andromida" folder in "AndromidaWidgetExtension" target */, + ); path = Andromida; sourceTree = ""; }; @@ -49,6 +103,14 @@ path = AndromidaUITests; sourceTree = ""; }; + EAC04D352F298D9B007F87EA /* AndromidaWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + EAC04D422F298D9C007F87EA /* Exceptions for "AndromidaWidget" folder in "AndromidaWidgetExtension" target */, + ); + path = AndromidaWidget; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -74,15 +136,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EAC04D2C2F298D9B007F87EA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EAC04E2C2F2998B2007F87EA /* Bedrock in Frameworks */, + EAC04D342F298D9B007F87EA /* SwiftUI.framework in Frameworks */, + EAC04D322F298D9B007F87EA /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ EAC04A8F2F26BAE8007F87EA = { isa = PBXGroup; children = ( + EAC04D4D2F298DD9007F87EA /* AndromidaWidgetExtension.entitlements */, EAC04A9A2F26BAE8007F87EA /* Andromida */, EAC04AA82F26BAE9007F87EA /* AndromidaTests */, EAC04AB22F26BAE9007F87EA /* AndromidaUITests */, + EAC04D352F298D9B007F87EA /* AndromidaWidget */, + EAC04D302F298D9B007F87EA /* Frameworks */, EAC04A992F26BAE8007F87EA /* Products */, ); sourceTree = ""; @@ -93,10 +168,20 @@ EAC04A982F26BAE8007F87EA /* Andromida.app */, EAC04AA52F26BAE9007F87EA /* AndromidaTests.xctest */, EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */, + EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */, ); name = Products; sourceTree = ""; }; + EAC04D302F298D9B007F87EA /* Frameworks */ = { + isa = PBXGroup; + children = ( + EAC04D312F298D9B007F87EA /* WidgetKit.framework */, + EAC04D332F298D9B007F87EA /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -107,10 +192,12 @@ EAC04A942F26BAE8007F87EA /* Sources */, EAC04A952F26BAE8007F87EA /* Frameworks */, EAC04A962F26BAE8007F87EA /* Resources */, + EAC04D462F298D9C007F87EA /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + EAC04D402F298D9C007F87EA /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( EAC04A9A2F26BAE8007F87EA /* Andromida */, @@ -169,6 +256,29 @@ productReference = EAC04AAF2F26BAE9007F87EA /* AndromidaUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; + EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = EAC04D432F298D9C007F87EA /* Build configuration list for PBXNativeTarget "AndromidaWidgetExtension" */; + buildPhases = ( + EAC04D2B2F298D9B007F87EA /* Sources */, + EAC04D2C2F298D9B007F87EA /* Frameworks */, + EAC04D2D2F298D9B007F87EA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + EAC04D352F298D9B007F87EA /* AndromidaWidget */, + ); + name = AndromidaWidgetExtension; + packageProductDependencies = ( + EAC04E2B2F2998B2007F87EA /* Bedrock */, + ); + productName = AndromidaWidgetExtension; + productReference = EAC04D2F2F298D9B007F87EA /* AndromidaWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -190,6 +300,9 @@ CreatedOnToolsVersion = 26.2; TestTargetID = EAC04A972F26BAE8007F87EA; }; + EAC04D2E2F298D9B007F87EA = { + CreatedOnToolsVersion = 26.2; + }; }; }; buildConfigurationList = EAC04A932F26BAE8007F87EA /* Build configuration list for PBXProject "Andromida" */; @@ -212,6 +325,7 @@ EAC04A972F26BAE8007F87EA /* Andromida */, EAC04AA42F26BAE9007F87EA /* AndromidaTests */, EAC04AAE2F26BAE9007F87EA /* AndromidaUITests */, + EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */, ); }; /* End PBXProject section */ @@ -238,6 +352,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EAC04D2D2F298D9B007F87EA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -262,6 +383,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + EAC04D2B2F298D9B007F87EA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -275,11 +403,18 @@ target = EAC04A972F26BAE8007F87EA /* Andromida */; targetProxy = EAC04AB02F26BAE9007F87EA /* PBXContainerItemProxy */; }; + EAC04D402F298D9C007F87EA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EAC04D2E2F298D9B007F87EA /* AndromidaWidgetExtension */; + targetProxy = EAC04D3F2F298D9C007F87EA /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ EAC04AB72F26BAE9007F87EA /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -344,6 +479,8 @@ }; EAC04AB82F26BAE9007F87EA /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -401,6 +538,8 @@ }; EAC04ABA2F26BAE9007F87EA /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -435,6 +574,8 @@ }; EAC04ABB2F26BAE9007F87EA /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -469,6 +610,8 @@ }; EAC04ABD2F26BAE9007F87EA /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -476,7 +619,7 @@ DEVELOPMENT_TEAM = 6R7KLBPBLZ; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -491,6 +634,8 @@ }; EAC04ABE2F26BAE9007F87EA /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -498,7 +643,7 @@ DEVELOPMENT_TEAM = 6R7KLBPBLZ; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -513,12 +658,14 @@ }; EAC04AC02F26BAE9007F87EA /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 6R7KLBPBLZ; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -533,12 +680,14 @@ }; EAC04AC12F26BAE9007F87EA /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig; buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 6R7KLBPBLZ; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.AndromidaUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -551,6 +700,72 @@ }; name = Release; }; + EAC04D442F298D9C007F87EA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Debug.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = AndromidaWidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AndromidaWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AndromidaWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Andromida.AndromidaWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = "1,2"; + }; + name = Debug; + }; + EAC04D452F298D9C007F87EA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = EAC04A9A2F26BAE8007F87EA /* Andromida */; + baseConfigurationReferenceRelativePath = Shared/Configuration/Release.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = AndromidaWidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6R7KLBPBLZ; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = AndromidaWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = AndromidaWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Andromida.AndromidaWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -590,6 +805,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + EAC04D432F298D9C007F87EA /* Build configuration list for PBXNativeTarget "AndromidaWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EAC04D442F298D9C007F87EA /* Debug */, + EAC04D452F298D9C007F87EA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ @@ -604,6 +828,11 @@ isa = XCSwiftPackageProductDependency; productName = Bedrock; }; + EAC04E2B2F2998B2007F87EA /* Bedrock */ = { + isa = XCSwiftPackageProductDependency; + package = EAC04AEC2F26BD5B007F87EA /* XCLocalSwiftPackageReference "../Bedrock" */; + productName = Bedrock; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = EAC04A902F26BAE8007F87EA /* Project object */; diff --git a/Andromida.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/Andromida.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index 9e6a2b8..9ef0618 100644 --- a/Andromida.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Andromida.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,6 +9,11 @@ orderHint 1 + AndromidaWidgetExtension.xcscheme_^#shared#^_ + + orderHint + 1 + diff --git a/Andromida/Andromida.entitlements b/Andromida/Andromida.entitlements index f807826..888ea93 100644 --- a/Andromida/Andromida.entitlements +++ b/Andromida/Andromida.entitlements @@ -3,12 +3,16 @@ com.apple.developer.icloud-container-identifiers - - iCloud.com.mbrucedogs.Andromida - + + $(CLOUDKIT_CONTAINER_IDENTIFIER) + com.apple.developer.icloud-services CloudKit + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + diff --git a/Andromida/AndromidaApp.swift b/Andromida/AndromidaApp.swift index b97adf5..64ca630 100644 --- a/Andromida/AndromidaApp.swift +++ b/Andromida/AndromidaApp.swift @@ -25,11 +25,15 @@ struct AndromidaApp: App { // Include all models in schema - Ritual, RitualArc, and ArcHabit let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self]) + + // Use App Group for shared container between app and widget let configuration = ModelConfiguration( schema: schema, - isStoredInMemoryOnly: false, - cloudKitDatabase: .private("iCloud.com.mbrucedogs.Andromida") + url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)? + .appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite"), + cloudKitDatabase: .private(AppIdentifiers.cloudKitContainerIdentifier) ) + let container: ModelContainer do { container = try ModelContainer(for: schema, configurations: [configuration]) diff --git a/Andromida/App/Localization/Localizable.xcstrings b/Andromida/App/Localization/Localizable.xcstrings index 916c3bb..e56d0a7 100644 --- a/Andromida/App/Localization/Localizable.xcstrings +++ b/Andromida/App/Localization/Localizable.xcstrings @@ -1131,6 +1131,12 @@ "comment" : "Habit title for a ritual preset focused on self-care, emphasizing gentle stretching as a habit.", "isCommentAutoGenerated" : true }, + "Get a gentle nudge each morning to start your day right" : { + "comment" : "Description for notification permission screen when user selected morning rituals." + }, + "Get a gentle reminder when it's time for your rituals" : { + "comment" : "Default description for notification permission screen." + }, "Get more done" : { "comment" : "Subtitle for the \"Focus\" onboarding goal.", "isCommentAutoGenerated" : true @@ -1143,12 +1149,6 @@ "comment" : "The text for the \"Get Started\" button in the welcome screen.", "isCommentAutoGenerated" : true }, - "Get a gentle nudge each morning to start your day right" : { - "comment" : "Description for notification permission screen when user selected morning rituals." - }, - "Get a gentle reminder when it's time for your rituals" : { - "comment" : "Default description for notification permission screen." - }, "Give your mind a break from screens." : { "comment" : "Notes for a ritual preset focused on giving the mind a break from screens.", "isCommentAutoGenerated" : true @@ -1367,6 +1367,18 @@ "comment" : "Habit title for a mindfulness ritual where the user writes in a journal for 5 minutes.", "isCommentAutoGenerated" : true }, + "Keep going" : { + "comment" : "Cancel button label to continue onboarding.", + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Keep going" + } + } + } + }, "Keep room cool" : { "comment" : "Habit title for keeping the bedroom cool at night.", "isCommentAutoGenerated" : true @@ -1456,29 +1468,6 @@ }, "Mindfulness" : { - }, - "Momentum at a glance" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Momentum at a glance" - } - }, - "es-MX" : { - "stringUnit" : { - "state" : "translated", - "value" : "Impulso de un vistazo" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Élan en un coup d’œil" - } - } - } }, "More" : { "comment" : "The text for a button that expands or collapses a list.", @@ -2169,22 +2158,6 @@ }, "Shows rituals for the current time of day. Check in here daily." : { - }, - "Skip for now" : { - "comment" : "A button label that allows users to skip creating a new ritual for now.", - "isCommentAutoGenerated" : true - }, - "Skip setup?" : { - "comment" : "Alert title asking if the user wants to skip onboarding setup.", - "isCommentAutoGenerated" : true, - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Skip setup?" - } - } - } }, "Skip" : { "comment" : "Button label to skip onboarding.", @@ -2198,26 +2171,18 @@ } } }, - "Keep going" : { - "comment" : "Cancel button label to continue onboarding.", - "isCommentAutoGenerated" : true, - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Keep going" - } - } - } + "Skip for now" : { + "comment" : "A button label that allows users to skip creating a new ritual for now.", + "isCommentAutoGenerated" : true }, - "You can complete setup later in Settings." : { - "comment" : "Alert message explaining that setup can be completed later in Settings.", + "Skip setup?" : { + "comment" : "Alert title asking if the user wants to skip onboarding setup.", "isCommentAutoGenerated" : true, "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "You can complete setup later in Settings." + "value" : "Skip setup?" } } } @@ -2282,9 +2247,6 @@ "comment" : "Description of a trend direction when there is no significant change.", "isCommentAutoGenerated" : true }, - "Stay on track" : { - "comment" : "Headline on the notification permission screen during onboarding." - }, "Start" : { "comment" : "A button that starts a new arc for a ritual.", "isCommentAutoGenerated" : true @@ -2324,6 +2286,9 @@ "comment" : "Subtitle for the \"Morning\" option in the \"Time of Day\" section of the onboarding screen.", "isCommentAutoGenerated" : true }, + "Stay on track" : { + "comment" : "Headline on the notification permission screen during onboarding." + }, "Streak" : { "comment" : "Title of a section in the Insight Cards view, related to streaks of consecutive perfect days.", "isCommentAutoGenerated" : true @@ -2358,29 +2323,6 @@ "comment" : "Description of a ritual preset that serves as a weekly reset to help users start their week on a positive note.", "isCommentAutoGenerated" : true }, - "Switch tabs to explore rituals and insights" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Switch tabs to explore rituals and insights" - } - }, - "es-MX" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cambia de pestaña para explorar rituales y perspectivas" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passez d’un onglet à l’autre pour explorer les rituels et les aperçus" - } - } - } - }, "Take 5 deep breaths" : { "comment" : "Habit title for a ritual preset focused on productivity, encouraging the user to take deep breaths.", "isCommentAutoGenerated" : true @@ -2608,6 +2550,9 @@ "comment" : "Description of a feature card in the \"WhatsNextStepView\" that explains how to manage all your rituals, regardless of the time they were created.", "isCommentAutoGenerated" : true }, + "We'll remind you at the perfect times for your morning and evening rituals" : { + "comment" : "Description for notification permission screen when user selected both morning and evening rituals." + }, "Weekly average" : { "comment" : "Caption for the 7-Day Avg insight card." }, @@ -2623,9 +2568,6 @@ "comment" : "The title of the welcome screen in the setup wizard.", "isCommentAutoGenerated" : true }, - "We'll remind you at the perfect times for your morning and evening rituals" : { - "comment" : "Description for notification permission screen when user selected both morning and evening rituals." - }, "Wellness" : { "comment" : "The category of the morning ritual.", "isCommentAutoGenerated" : true @@ -2676,13 +2618,13 @@ } } }, + "Wind down with a reminder when it's time for your evening ritual" : { + "comment" : "Description for notification permission screen when user selected evening rituals." + }, "Wind down with habits that promote quality sleep." : { "comment" : "Notes section of a ritual preset focused on sleep preparation.", "isCommentAutoGenerated" : true }, - "Wind down with a reminder when it's time for your evening ritual" : { - "comment" : "Description for notification permission screen when user selected evening rituals." - }, "Wind down with intention" : { "comment" : "Subtitle for the \"Evening\" section of the \"Both\" time preference option in the onboarding flow.", "isCommentAutoGenerated" : true @@ -2721,6 +2663,18 @@ "comment" : "A hint displayed below the \"Browse Presets\" button, encouraging users to explore preset rituals.", "isCommentAutoGenerated" : true }, + "You can complete setup later in Settings." : { + "comment" : "Alert message explaining that setup can be completed later in Settings.", + "isCommentAutoGenerated" : true, + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You can complete setup later in Settings." + } + } + } + }, "You completed your first check-in" : { "comment" : "A description of the positive outcome of completing a first check-in.", "isCommentAutoGenerated" : true @@ -2752,29 +2706,6 @@ "comment" : "The title of the preview step in the ritual creation flow.", "isCommentAutoGenerated" : true }, - "Your focus ritual lives here" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Your focus ritual lives here" - } - }, - "es-MX" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tu ritual de enfoque vive aquí" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Votre rituel principal se trouve ici" - } - } - } - }, "Your highest-performing ritual by completion rate in the current arc. Keep it up!" : { "comment" : "Explanation for the Best Ritual insight card." }, diff --git a/Andromida/App/Models/Ritual.swift b/Andromida/App/Models/Ritual.swift index b9aef8e..fc5e98f 100644 --- a/Andromida/App/Models/Ritual.swift +++ b/Andromida/App/Models/Ritual.swift @@ -66,6 +66,18 @@ enum TimeOfDay: String, Codable, CaseIterable, Comparable { static func < (lhs: TimeOfDay, rhs: TimeOfDay) -> Bool { lhs.sortOrder < rhs.sortOrder } + + /// Returns the current time period based on the hour of the day. + static func current(for date: Date = Date()) -> TimeOfDay { + let hour = Calendar.current.component(.hour, from: date) + switch hour { + case 0..<11: return .morning + case 11..<14: return .midday + case 14..<17: return .afternoon + case 17..<21: return .evening + default: return .night + } + } } /// A ritual represents a persistent habit-building journey. It contains multiple diff --git a/Andromida/App/State/RitualStore.swift b/Andromida/App/State/RitualStore.swift index 38f0ac7..2cadb25 100644 --- a/Andromida/App/State/RitualStore.swift +++ b/Andromida/App/State/RitualStore.swift @@ -3,6 +3,7 @@ import Observation import SwiftData import CoreData import Bedrock +import WidgetKit @MainActor @Observable @@ -69,8 +70,12 @@ final class RitualStore: RitualStoreProviding { object: nil, queue: .main ) { [weak self] _ in - Task { @MainActor in - self?.reloadRituals() + // Hop to the main actor and capture a strong reference safely there + Task { @MainActor [weak self] in + guard let strongSelf = self else { return } + strongSelf.reloadRituals() + // Also refresh widgets when data arrives from other devices + WidgetCenter.shared.reloadAllTimelines() } } } @@ -166,16 +171,7 @@ final class RitualStore: RitualStoreProviding { /// Filters based on time of day: morning (before 11am), midday (11am-2pm), afternoon (2pm-5pm), /// evening (5pm-9pm), night (after 9pm). Anytime rituals are always shown. func ritualsForToday() -> [Ritual] { - let hour = calendar.component(.hour, from: Date()) - let currentPeriod: TimeOfDay = { - switch hour { - case 0..<11: return .morning - case 11..<14: return .midday - case 14..<17: return .afternoon - case 17..<21: return .evening - default: return .night - } - }() + let currentPeriod = TimeOfDay.current() return currentRituals.filter { ritual in guard let arc = ritual.currentArc, arc.contains(date: Date()) else { return false } @@ -831,6 +827,8 @@ final class RitualStore: RitualStoreProviding { do { try modelContext.save() reloadRituals() + // Notify widgets that data has changed + WidgetCenter.shared.reloadAllTimelines() } catch { lastErrorMessage = error.localizedDescription } @@ -911,7 +909,7 @@ final class RitualStore: RitualStoreProviding { } private func dayIdentifier(for date: Date) -> String { - dayFormatter.string(from: date) + RitualAnalytics.dayIdentifier(for: date) } // MARK: - History / Calendar Support diff --git a/Andromida/App/State/SettingsStore.swift b/Andromida/App/State/SettingsStore.swift index 848f1ac..be78f46 100644 --- a/Andromida/App/State/SettingsStore.swift +++ b/Andromida/App/State/SettingsStore.swift @@ -96,7 +96,9 @@ final class SettingsStore: CloudSyncable, ThemeProviding { private func observeCloudChanges() { cloudSync.onCloudDataReceived = { [weak self] _ in - self?.handleCloudDataChange() + Task { @MainActor in + self?.handleCloudDataChange() + } } cloudChangeObserver = NotificationCenter.default.addObserver( @@ -109,7 +111,9 @@ final class SettingsStore: CloudSyncable, ThemeProviding { identifier == AppSettingsData.dataIdentifier else { return } - self.handleCloudDataChange() + Task { @MainActor in + self.handleCloudDataChange() + } } } diff --git a/Andromida/App/Views/RootView.swift b/Andromida/App/Views/RootView.swift index 6b8692b..8eb701d 100644 --- a/Andromida/App/Views/RootView.swift +++ b/Andromida/App/Views/RootView.swift @@ -85,6 +85,26 @@ struct RootView: View { store.reminderScheduler.shouldNavigateToToday = false } } + .onOpenURL { url in + handleURL(url) + } + } + + private func handleURL(_ url: URL) { + guard url.scheme == "andromida" else { return } + + switch url.host { + case "today": + selectedTab = .today + case "rituals": + selectedTab = .rituals + case "insights": + selectedTab = .insights + case "history": + selectedTab = .history + default: + break + } } private func refreshCurrentTab() { diff --git a/Andromida/App/Views/Today/Components/TodayNoRitualsForTimeView.swift b/Andromida/App/Views/Today/Components/TodayNoRitualsForTimeView.swift index 7601f93..b4c23c1 100644 --- a/Andromida/App/Views/Today/Components/TodayNoRitualsForTimeView.swift +++ b/Andromida/App/Views/Today/Components/TodayNoRitualsForTimeView.swift @@ -6,14 +6,7 @@ struct TodayNoRitualsForTimeView: View { @Bindable var store: RitualStore private var currentTimePeriod: TimeOfDay { - let hour = Calendar.current.component(.hour, from: Date()) - switch hour { - case 0..<11: return .morning - case 11..<14: return .midday - case 14..<17: return .afternoon - case 17..<21: return .evening - default: return .night - } + TimeOfDay.current() } private var nextRituals: [Ritual] { diff --git a/Andromida/Shared/Configuration/AppIdentifiers.swift b/Andromida/Shared/Configuration/AppIdentifiers.swift new file mode 100644 index 0000000..3b68e0e --- /dev/null +++ b/Andromida/Shared/Configuration/AppIdentifiers.swift @@ -0,0 +1,23 @@ +import Foundation + +enum AppIdentifiers { + // Read from Info.plist (values come from xcconfig) + static let appGroupIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String + ?? "group.com.mbrucedogs.Andromida" + }() + + static let cloudKitContainerIdentifier: String = { + Bundle.main.object(forInfoDictionaryKey: "CloudKitContainerIdentifier") as? String + ?? "iCloud.com.mbrucedogs.Andromida" + }() + + // Derived from bundle identifier + static var bundleIdentifier: String { + Bundle.main.bundleIdentifier ?? "com.mbrucedogs.Andromida" + } + + static var widgetBundleIdentifier: String { + "\(bundleIdentifier).Widget" + } +} diff --git a/Andromida/Shared/Configuration/Base.xcconfig b/Andromida/Shared/Configuration/Base.xcconfig new file mode 100644 index 0000000..97e6070 --- /dev/null +++ b/Andromida/Shared/Configuration/Base.xcconfig @@ -0,0 +1,25 @@ +// Base.xcconfig - Source of truth for all identifiers +// MIGRATION: Update COMPANY_IDENTIFIER and DEVELOPMENT_TEAM below + +// ============================================================================= +// COMPANY IDENTIFIER - CHANGE THIS FOR MIGRATION +// ============================================================================= + +COMPANY_IDENTIFIER = com.mbrucedogs +APP_NAME = Andromida +DEVELOPMENT_TEAM = // Add your Team ID here if needed + +// ============================================================================= +// DERIVED IDENTIFIERS - DO NOT EDIT +// ============================================================================= + +APP_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME) +WATCH_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).watchkitapp +APPCLIP_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Clip +WIDGET_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Widget +INTENT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).Intent +TESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)Tests +UITESTS_BUNDLE_IDENTIFIER = $(COMPANY_IDENTIFIER).$(APP_NAME)UITests + +APP_GROUP_IDENTIFIER = group.$(COMPANY_IDENTIFIER).$(APP_NAME) +CLOUDKIT_CONTAINER_IDENTIFIER = iCloud.$(COMPANY_IDENTIFIER).$(APP_NAME) diff --git a/Andromida/Shared/Configuration/Debug.xcconfig b/Andromida/Shared/Configuration/Debug.xcconfig new file mode 100644 index 0000000..7b0c18a --- /dev/null +++ b/Andromida/Shared/Configuration/Debug.xcconfig @@ -0,0 +1,6 @@ +// Debug.xcconfig +#include "Base.xcconfig" + +// Debug-specific settings +SWIFT_OPTIMIZATION_LEVEL = -Onone +ENABLE_TESTABILITY = YES diff --git a/Andromida/Shared/Configuration/Release.xcconfig b/Andromida/Shared/Configuration/Release.xcconfig new file mode 100644 index 0000000..a0e857f --- /dev/null +++ b/Andromida/Shared/Configuration/Release.xcconfig @@ -0,0 +1,6 @@ +// Release.xcconfig +#include "Base.xcconfig" + +// Release-specific settings +SWIFT_OPTIMIZATION_LEVEL = -O +SWIFT_COMPILATION_MODE = wholemodule diff --git a/Andromida/Shared/Services/RitualAnalytics.swift b/Andromida/Shared/Services/RitualAnalytics.swift new file mode 100644 index 0000000..774b83c --- /dev/null +++ b/Andromida/Shared/Services/RitualAnalytics.swift @@ -0,0 +1,83 @@ +import Foundation + +/// Shared logic for ritual analytics and data processing used by both the app and widgets. +enum RitualAnalytics { + /// Returns a unique string identifier for a given date (YYYY-MM-DD). + static func dayIdentifier(for date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + return formatter.string(from: date) + } + + /// Calculates the current streak of consecutive days with activity. + /// - Parameters: + /// - rituals: The list of rituals to analyze. + /// - calendar: The calendar to use for date calculations. + /// - Returns: The current streak count. + static func calculateCurrentStreak(rituals: [Ritual], calendar: Calendar = .current) -> Int { + var allCompletions = Set() + + // Collect all completed day IDs from all habits across all rituals + for ritual in rituals { + for arc in ritual.arcs ?? [] { + for habit in arc.habits ?? [] { + for dID in habit.completedDayIDs { + allCompletions.insert(dID) + } + } + } + } + + if allCompletions.isEmpty { return 0 } + + // Count backwards from today + var streak = 0 + var checkDate = calendar.startOfDay(for: Date()) + + // If today isn't perfect, check if yesterday was to maintain streak + var currentDayID = dayIdentifier(for: checkDate) + if !allCompletions.contains(currentDayID) { + checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate + currentDayID = dayIdentifier(for: checkDate) + } + + while allCompletions.contains(currentDayID) { + streak += 1 + checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate + currentDayID = dayIdentifier(for: checkDate) + if streak > 3650 { break } // Safety cap (10 years) + } + + return streak + } + + /// Filters rituals that are active on a specific date and match the current time of day. + static func ritualsActive(on date: Date, from rituals: [Ritual]) -> [Ritual] { + let timeOfDay = TimeOfDay.current(for: date) + + return rituals.filter { ritual in + guard ritual.arcs?.first(where: { $0.isActive && $0.contains(date: date) }) != nil else { + return false + } + + return ritual.timeOfDay == .anytime || ritual.timeOfDay == timeOfDay + } + } + + /// Calculates the overall completion rate for all rituals active on a specific date. + static func overallCompletionRate(on date: Date, from rituals: [Ritual]) -> Double { + let dayID = dayIdentifier(for: date) + var allTodayHabits: [ArcHabit] = [] + + for ritual in rituals { + if let arc = ritual.arcs?.first(where: { $0.isActive && $0.contains(date: date) }) { + allTodayHabits.append(contentsOf: arc.habits ?? []) + } + } + + guard !allTodayHabits.isEmpty else { return 0.0 } + + let completedCount = allTodayHabits.filter { $0.completedDayIDs.contains(dayID) }.count + return Double(completedCount) / Double(allTodayHabits.count) + } +} diff --git a/AndromidaWidget/AndromidaWidget.entitlements b/AndromidaWidget/AndromidaWidget.entitlements new file mode 100644 index 0000000..d9849a8 --- /dev/null +++ b/AndromidaWidget/AndromidaWidget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + + diff --git a/AndromidaWidget/AndromidaWidget.swift b/AndromidaWidget/AndromidaWidget.swift new file mode 100644 index 0000000..d782447 --- /dev/null +++ b/AndromidaWidget/AndromidaWidget.swift @@ -0,0 +1,17 @@ +import WidgetKit +import SwiftUI + +@main +struct AndromidaWidget: Widget { + let kind: String = "AndromidaWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: AndromidaWidgetProvider()) { entry in + AndromidaWidgetView(entry: entry) + } + .configurationDisplayName("Andromida") + .description("Track your daily rituals and habits.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + .contentMarginsDisabled() + } +} diff --git a/AndromidaWidget/Info.plist b/AndromidaWidget/Info.plist new file mode 100644 index 0000000..e6e0a7d --- /dev/null +++ b/AndromidaWidget/Info.plist @@ -0,0 +1,15 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + AppGroupIdentifier + $(APP_GROUP_IDENTIFIER) + CloudKitContainerIdentifier + $(CLOUDKIT_CONTAINER_IDENTIFIER) + + diff --git a/AndromidaWidget/Intents/ConfigurationAppIntent.swift b/AndromidaWidget/Intents/ConfigurationAppIntent.swift new file mode 100644 index 0000000..2e720d0 --- /dev/null +++ b/AndromidaWidget/Intents/ConfigurationAppIntent.swift @@ -0,0 +1,9 @@ +import WidgetKit +import AppIntents + +struct ConfigurationAppIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource { "Andromida" } + static var description: IntentDescription { "Track your daily rituals and habits." } + + // We can add more parameters here later, like selecting a specific ritual. +} diff --git a/AndromidaWidget/Models/HabitEntry.swift b/AndromidaWidget/Models/HabitEntry.swift new file mode 100644 index 0000000..1c51445 --- /dev/null +++ b/AndromidaWidget/Models/HabitEntry.swift @@ -0,0 +1,9 @@ +import Foundation + +struct HabitEntry: Identifiable { + let id: UUID + let title: String + let symbolName: String + let ritualTitle: String + let isCompleted: Bool +} diff --git a/AndromidaWidget/Models/WidgetEntry.swift b/AndromidaWidget/Models/WidgetEntry.swift new file mode 100644 index 0000000..46dd9ca --- /dev/null +++ b/AndromidaWidget/Models/WidgetEntry.swift @@ -0,0 +1,14 @@ +import WidgetKit +import Foundation + +struct WidgetEntry: TimelineEntry { + let date: Date + let configuration: ConfigurationAppIntent + let completionRate: Double + let currentStreak: Int + let nextHabits: [HabitEntry] + let weeklyTrend: [Double] + let currentTimeOfDay: String + let currentTimeOfDaySymbol: String + let currentTimeOfDayRange: String +} diff --git a/AndromidaWidget/Providers/AndromidaWidgetProvider.swift b/AndromidaWidget/Providers/AndromidaWidgetProvider.swift new file mode 100644 index 0000000..e2b3cd8 --- /dev/null +++ b/AndromidaWidget/Providers/AndromidaWidgetProvider.swift @@ -0,0 +1,114 @@ +import WidgetKit +import SwiftUI +import SwiftData +import AppIntents + +struct AndromidaWidgetProvider: AppIntentTimelineProvider { + func placeholder(in context: Context) -> WidgetEntry { + WidgetEntry( + date: Date(), + configuration: ConfigurationAppIntent(), + completionRate: 0.75, + currentStreak: 5, + nextHabits: [ + HabitEntry(id: UUID(), title: "Morning Meditation", symbolName: "figure.mind.and.body", ritualTitle: "Mindfulness", isCompleted: false), + HabitEntry(id: UUID(), title: "Drink Water", symbolName: "drop.fill", ritualTitle: "Health", isCompleted: true) + ], + weeklyTrend: [0.5, 0.7, 0.6, 0.9, 0.8, 0.75, 0.0], + currentTimeOfDay: "Morning", + currentTimeOfDaySymbol: "sunrise.fill", + currentTimeOfDayRange: "Before 11am" + ) + } + + func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> WidgetEntry { + await fetchLatestData(for: configuration) + } + + func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { + let entry = await fetchLatestData(for: configuration) + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date() + return Timeline(entries: [entry], policy: .after(nextUpdate)) + } + + @MainActor + private func fetchLatestData(for configuration: ConfigurationAppIntent) -> WidgetEntry { + let schema = Schema([Ritual.self, RitualArc.self, ArcHabit.self]) + let configurationURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppIdentifiers.appGroupIdentifier)? + .appendingPathComponent("Andromida.sqlite") ?? URL.documentsDirectory.appendingPathComponent("Andromida.sqlite") + + let modelConfig = ModelConfiguration(schema: schema, url: configurationURL) + + do { + let container = try ModelContainer(for: schema, configurations: [modelConfig]) + let context = container.mainContext + + let descriptor = FetchDescriptor() + let rituals = try context.fetch(descriptor) + + let today = Date() + let dayID = RitualAnalytics.dayIdentifier(for: today) + let timeOfDay = TimeOfDay.current(for: today) + + // Match the app's logic for "Today" view + let todayRituals = RitualAnalytics.ritualsActive(on: today, from: rituals) + .sorted { lhs, rhs in + if lhs.timeOfDay != rhs.timeOfDay { + return lhs.timeOfDay < rhs.timeOfDay + } + return lhs.sortIndex < rhs.sortIndex + } + + var visibleHabits: [HabitEntry] = [] + for ritual in todayRituals { + if let arc = ritual.arcs?.first(where: { $0.isActive && $0.contains(date: today) }) { + // Sort habits within each ritual by their sortIndex + let sortedHabits = (arc.habits ?? []).sorted { $0.sortIndex < $1.sortIndex } + for habit in sortedHabits { + visibleHabits.append(HabitEntry( + id: habit.id, + title: habit.title, + symbolName: habit.symbolName, + ritualTitle: ritual.title, + isCompleted: habit.completedDayIDs.contains(dayID) + )) + } + } + } + + // Calculate overall progress across ALL rituals for today + let overallRate = RitualAnalytics.overallCompletionRate(on: today, from: rituals) + + // Next habits (limit to 4) - still filtered by current time of day for the list + let nextHabits = visibleHabits.prefix(4) + + // Streak calculation + let streak = RitualAnalytics.calculateCurrentStreak(rituals: rituals) + + return WidgetEntry( + date: today, + configuration: configuration, + completionRate: overallRate, + currentStreak: streak, + nextHabits: Array(nextHabits), + weeklyTrend: [], + currentTimeOfDay: timeOfDay.displayName, + currentTimeOfDaySymbol: timeOfDay.symbolName, + currentTimeOfDayRange: timeOfDay.timeRange + ) + } catch { + // Return a default entry instead of placeholder(in: .preview) + return WidgetEntry( + date: Date(), + configuration: configuration, + completionRate: 0.0, + currentStreak: 0, + nextHabits: [], + weeklyTrend: [], + currentTimeOfDay: "Today", + currentTimeOfDaySymbol: "clock.fill", + currentTimeOfDayRange: "" + ) + } + } +} diff --git a/AndromidaWidget/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/AndromidaWidget/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/AndromidaWidget/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AndromidaWidget/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/AndromidaWidget/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/AndromidaWidget/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AndromidaWidget/Resources/Assets.xcassets/Contents.json b/AndromidaWidget/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/AndromidaWidget/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AndromidaWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json b/AndromidaWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/AndromidaWidget/Resources/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AndromidaWidget/Views/AndromidaWidgetView.swift b/AndromidaWidget/Views/AndromidaWidgetView.swift new file mode 100644 index 0000000..8489a79 --- /dev/null +++ b/AndromidaWidget/Views/AndromidaWidgetView.swift @@ -0,0 +1,30 @@ +import SwiftUI +import WidgetKit + +struct AndromidaWidgetView: View { + var entry: WidgetEntry + @Environment(\.widgetFamily) var family + + var body: some View { + switch family { + case .systemSmall: + SmallWidgetView(entry: entry) + .widgetURL(URL(string: "andromida://today")) + case .systemMedium: + MediumWidgetView(entry: entry) + .widgetURL(URL(string: "andromida://today")) + case .systemLarge: + LargeWidgetView(entry: entry) + .widgetURL(URL(string: "andromida://today")) + default: + SmallWidgetView(entry: entry) + .widgetURL(URL(string: "andromida://today")) + } + } +} + +// MARK: - Branding Colors Helper +extension Color { + static let brandingPrimary = Color(red: 0.12, green: 0.09, blue: 0.08) + static let brandingAccent = Color(red: 0.95, green: 0.60, blue: 0.45) // Matches the orange-ish accent in your app +} diff --git a/AndromidaWidget/Views/Components/LargeWidgetView.swift b/AndromidaWidget/Views/Components/LargeWidgetView.swift new file mode 100644 index 0000000..db7f24c --- /dev/null +++ b/AndromidaWidget/Views/Components/LargeWidgetView.swift @@ -0,0 +1,78 @@ +import SwiftUI +import WidgetKit +import Bedrock + +struct LargeWidgetView: View { + let entry: WidgetEntry + + var body: some View { + VStack(alignment: .leading, spacing: Design.Spacing.large) { + HStack { + VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { + Text(String(localized: "Today's Progress")) + .styled(.heading, emphasis: .custom(.white)) + Text("\(entry.currentStreak) day streak") + .styled(.subheading, emphasis: .custom(Color.brandingAccent)) + } + Spacer() + ZStack { + Circle() + .stroke(Color.white.opacity(0.1), lineWidth: 6) + Circle() + .trim(from: 0, to: entry.completionRate) + .stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 6, lineCap: .round)) + .rotationEffect(.degrees(-90)) + Text("\(Int(entry.completionRate * 100))%") + .styled(.captionEmphasis, emphasis: .custom(.white)) + } + .frame(width: 50, height: 50) + } + + Divider() + .background(Color.white.opacity(0.2)) + + if entry.nextHabits.isEmpty { + Spacer() + WidgetEmptyStateView( + title: String(localized: "No rituals scheduled for \(entry.currentTimeOfDay.lowercased())."), + subtitle: entry.currentTimeOfDay, + symbolName: entry.currentTimeOfDaySymbol, + timeRange: entry.currentTimeOfDayRange + ) + Spacer() + } else { + Text(String(localized: "Habits")) + .styled(.captionEmphasis, emphasis: .custom(.white.opacity(0.7))) + + VStack(spacing: Design.Spacing.medium) { + ForEach(entry.nextHabits) { habit in + HStack(spacing: Design.Spacing.medium) { + Image(systemName: habit.symbolName) + .foregroundColor(Color.brandingAccent) + .font(.system(size: 18)) + .frame(width: 24) + + VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { + Text(habit.title) + .styled(.subheading, emphasis: .custom(.white)) + Text(habit.ritualTitle) + .styled(.caption, emphasis: .custom(.white.opacity(0.5))) + } + Spacer() + + Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundColor(habit.isCompleted ? .green : .white.opacity(0.2)) + .font(.system(size: 20)) + } + } + } + } + + Spacer() + } + .padding(Design.Spacing.large) + .containerBackground(for: .widget) { + Color.brandingPrimary + } + } +} diff --git a/AndromidaWidget/Views/Components/MediumWidgetView.swift b/AndromidaWidget/Views/Components/MediumWidgetView.swift new file mode 100644 index 0000000..53e5832 --- /dev/null +++ b/AndromidaWidget/Views/Components/MediumWidgetView.swift @@ -0,0 +1,79 @@ +import SwiftUI +import WidgetKit +import Bedrock + +struct MediumWidgetView: View { + let entry: WidgetEntry + + var body: some View { + HStack(spacing: 0) { + // Left side: Progress & Streak + VStack(spacing: Design.Spacing.medium) { + ZStack { + Circle() + .stroke(Color.white.opacity(0.1), lineWidth: 8) + Circle() + .trim(from: 0, to: entry.completionRate) + .stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 8, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 0) { + Text("\(Int(entry.completionRate * 100))%") + .styled(.heading, emphasis: .custom(.white)) + Text(String(localized: "Today")) + .styled(.caption, emphasis: .custom(.white.opacity(0.7))) + } + } + .frame(width: 72, height: 72) + + HStack(spacing: Design.Spacing.xSmall) { + Image(systemName: "flame.fill") + .foregroundColor(Color.brandingAccent) + .font(.system(size: 14)) + Text("\(entry.currentStreak)d") + .styled(.captionEmphasis, emphasis: .custom(.white)) + } + } + .frame(width: 110) + + // Right side: Habits + VStack(alignment: .leading, spacing: Design.Spacing.medium) { + if entry.nextHabits.isEmpty { + WidgetEmptyStateView( + title: String(localized: "No rituals now"), + subtitle: entry.currentTimeOfDay, + symbolName: entry.currentTimeOfDaySymbol, + timeRange: entry.currentTimeOfDayRange + ) + } else { + Text(String(localized: "Next Habits")) + .styled(.captionEmphasis, emphasis: .custom(.white.opacity(0.7))) + + VStack(alignment: .leading, spacing: Design.Spacing.small) { + ForEach(entry.nextHabits.prefix(3)) { habit in + HStack(spacing: Design.Spacing.small) { + Image(systemName: habit.isCompleted ? "checkmark.circle.fill" : habit.symbolName) + .foregroundColor(habit.isCompleted ? .green : Color.brandingAccent) + .font(.system(size: 14)) + .frame(width: 20) + + Text(habit.title) + .styled(.subheading, emphasis: .custom(.white)) + .lineLimit(1) + } + } + } + } + + Spacer() + } + .padding(.vertical, Design.Spacing.large) + .padding(.trailing, Design.Spacing.medium) + + Spacer() + } + .containerBackground(for: .widget) { + Color.brandingPrimary + } + } +} diff --git a/AndromidaWidget/Views/Components/SmallWidgetView.swift b/AndromidaWidget/Views/Components/SmallWidgetView.swift new file mode 100644 index 0000000..1644d74 --- /dev/null +++ b/AndromidaWidget/Views/Components/SmallWidgetView.swift @@ -0,0 +1,38 @@ +import SwiftUI +import WidgetKit +import Bedrock + +struct SmallWidgetView: View { + let entry: WidgetEntry + + var body: some View { + VStack(spacing: Design.Spacing.small) { + ZStack { + Circle() + .stroke(Color.white.opacity(0.1), lineWidth: 8) + Circle() + .trim(from: 0, to: entry.completionRate) + .stroke(Color.brandingAccent, style: StrokeStyle(lineWidth: 8, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 0) { + Text("\(Int(entry.completionRate * 100))%") + .styled(.heading, emphasis: .custom(.white)) + Text(String(localized: "Today")) + .styled(.caption, emphasis: .custom(.white.opacity(0.7))) + } + } + .frame(width: 80, height: 80) + + HStack(spacing: Design.Spacing.xSmall) { + Image(systemName: "flame.fill") + .foregroundColor(Color.brandingAccent) + Text("\(entry.currentStreak) day streak") + .styled(.captionEmphasis, emphasis: .custom(.white)) + } + } + .containerBackground(for: .widget) { + Color.brandingPrimary + } + } +} diff --git a/AndromidaWidget/Views/Components/WidgetEmptyStateView.swift b/AndromidaWidget/Views/Components/WidgetEmptyStateView.swift new file mode 100644 index 0000000..305cad0 --- /dev/null +++ b/AndromidaWidget/Views/Components/WidgetEmptyStateView.swift @@ -0,0 +1,31 @@ +import SwiftUI +import Bedrock + +struct WidgetEmptyStateView: View { + let title: String + let subtitle: String + let symbolName: String + let timeRange: String + + var body: some View { + VStack(spacing: Design.Spacing.small) { + SymbolIcon(symbolName, size: .hero, color: Color.brandingAccent.opacity(0.6)) + + VStack(spacing: Design.Spacing.xSmall) { + Text(title) + .styled(.subheading, emphasis: .custom(.white)) + .multilineTextAlignment(.center) + + if !timeRange.isEmpty { + Text(timeRange) + .styled(.caption, emphasis: .custom(.white.opacity(0.5))) + } + } + + Text(String(localized: "Enjoy this moment.")) + .styled(.caption, emphasis: .custom(.white.opacity(0.5))) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} diff --git a/AndromidaWidgetExtension.entitlements b/AndromidaWidgetExtension.entitlements new file mode 100644 index 0000000..d9849a8 --- /dev/null +++ b/AndromidaWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + $(APP_GROUP_IDENTIFIER) + + +