feat: iCloud KV Store sync for tasks, workout data, and settings
- Add NSUbiquitousKeyValueStore entitlement to KisaniCal.entitlements - New CloudSyncManager: mirrors UserDefaults to iCloud KV Store on every save; restores to UserDefaults on fresh install when local data is missing; observes external changes to pull updates in real-time; guards against 1 MB limit - TaskViewModel: restore from iCloud on init, push to iCloud on every save - WorkoutViewModel: restore programs/schedule/activePid/completedDates from iCloud on init, push all four keys on every save - ContentView: restoreSettings() + backupSettings() on appear and foreground — syncs ~18 small settings keys (appearance, tabs, tutorial flags, calendar prefs, workout notifications) Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
@@ -18,20 +18,21 @@
|
|||||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A4491BFA50721082024756 /* SharedComponents.swift */; };
|
||||||
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
|
9070521B1D36A5551976C275 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF6CCD95A587E26E30F5712 /* CalendarView.swift */; };
|
||||||
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */; };
|
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */; };
|
||||||
|
A1B2C3D4E5F6A7B8C9D0E1F2 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */; };
|
||||||
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
|
A1DBC4D2F09C02B8CEE6449E /* AddExerciseSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */; };
|
||||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
|
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
|
||||||
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
|
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */; };
|
||||||
|
B2C3D4E5F6A7B8C9D0E1F2A3 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */; };
|
||||||
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */; };
|
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */; };
|
||||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
|
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
|
||||||
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
|
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
|
||||||
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; };
|
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; };
|
||||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
|
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
|
||||||
|
CC11DD22EE33FF44AA55BB66 /* CloudSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */; };
|
||||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
|
||||||
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */; };
|
F1A2B3C4D5E6F7A8B9C0D1E2 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */; };
|
||||||
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */; };
|
F5A6B7C8D9E0F1A2B3C4D5E6 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */; };
|
||||||
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */; };
|
F9B0C1D2E3F4A5B6C7D8E9F0 /* ProfileSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */; };
|
||||||
A1B2C3D4E5F6A7B8C9D0E1F2 /* TutorialManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */; };
|
|
||||||
B2C3D4E5F6A7B8C9D0E1F2A3 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -47,8 +48,11 @@
|
|||||||
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExerciseSheet.swift; sourceTree = "<group>"; };
|
||||||
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
||||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||||
|
CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudSyncManager.swift; sourceTree = "<group>"; };
|
||||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialManager.swift; sourceTree = "<group>"; };
|
||||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||||
|
B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
|
||||||
B38135942FCAF0BD0037DC41 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
B38135942FCAF0BD0037DC41 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
||||||
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
|
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
|
||||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
@@ -60,8 +64,6 @@
|
|||||||
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
|
||||||
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
F7A8B9C0D1E2F3A4B5C6D7E8 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
|
||||||
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
F8A9B0C1D2E3F4A5B6C7D8E9 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
||||||
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialManager.swift; sourceTree = "<group>"; };
|
|
||||||
B1C2D3E4F5A6B7C8D9E0F1A2 /* TutorialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialView.swift; sourceTree = "<group>"; };
|
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -156,6 +158,7 @@
|
|||||||
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */,
|
F3A4B5C6D7E8F9A0B1C2D3E4 /* AuthManager.swift */,
|
||||||
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */,
|
B38135952FCAF9CE0037DC41 /* HealthKitManager.swift */,
|
||||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
|
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
|
||||||
|
CC00DD11EE22FF33AA44BB55 /* CloudSyncManager.swift */,
|
||||||
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */,
|
326B77A7B317A7DB44E13EA5 /* ShortcutHandler.swift */,
|
||||||
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */,
|
A0B1C2D3E4F5A6B7C8D9E0F1 /* TutorialManager.swift */,
|
||||||
);
|
);
|
||||||
@@ -247,7 +250,8 @@
|
|||||||
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
|
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
|
||||||
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
|
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
|
||||||
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */,
|
B38135962FCAF9CE0037DC41 /* HealthKitManager.swift in Sources */,
|
||||||
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
|
CC11DD22EE33FF44AA55BB66 /* CloudSyncManager.swift in Sources */,
|
||||||
|
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */,
|
||||||
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
|
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */,
|
||||||
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
|
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */,
|
||||||
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
|
8BE9D3D650B0F06169EC7048 /* SharedComponents.swift in Sources */,
|
||||||
@@ -270,10 +274,11 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
||||||
@@ -282,13 +287,14 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
|
NEW_SETTING = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
@@ -429,10 +435,11 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 8;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
|
||||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
INFOPLIST_KEY_NSCalendarsUsageDescription = "Kisani Cal shows your calendar events alongside tasks.";
|
||||||
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
INFOPLIST_KEY_NSHealthShareUsageDescription = "KisaniCal reads your steps, active calories, resting heart rate, and workout history to display your fitness stats.";
|
||||||
@@ -441,13 +448,14 @@
|
|||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
|
NEW_SETTING = "";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.9;
|
SWIFT_VERSION = 5.9;
|
||||||
|
|||||||
@@ -1,17 +1,92 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{ "filename" : "AppIcon-20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" },
|
{
|
||||||
{ "filename" : "AppIcon-20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" },
|
"filename" : "AppIcon-20@2x.png",
|
||||||
{ "filename" : "AppIcon-29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" },
|
"idiom" : "iphone",
|
||||||
{ "filename" : "AppIcon-29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" },
|
"scale" : "2x",
|
||||||
{ "filename" : "AppIcon-40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" },
|
"size" : "20x20"
|
||||||
{ "filename" : "AppIcon-40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" },
|
},
|
||||||
{ "filename" : "AppIcon-60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" },
|
{
|
||||||
{ "filename" : "AppIcon-60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" },
|
"filename" : "AppIcon-20@3x.png",
|
||||||
{ "filename" : "AppIcon-76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" },
|
"idiom" : "iphone",
|
||||||
{ "filename" : "AppIcon-76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" },
|
"scale" : "3x",
|
||||||
{ "filename" : "AppIcon-83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" },
|
"size" : "20x20"
|
||||||
{ "filename" : "AppIcon-1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-29@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-29@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-40@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-40@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-60@2x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-60@3x.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-76@1x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-76@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-83.5@2x.png",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "AppIcon-1024.png",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"author" : "xcode",
|
"author" : "xcode",
|
||||||
|
|||||||
@@ -84,11 +84,14 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if !hasOnboarded { showOnboarding = true }
|
if !hasOnboarded { showOnboarding = true }
|
||||||
|
CloudSyncManager.shared.restoreSettings()
|
||||||
|
CloudSyncManager.shared.backupSettings()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
}
|
}
|
||||||
.onChange(of: scenePhase) { phase in
|
.onChange(of: scenePhase) { phase in
|
||||||
if phase == .active {
|
if phase == .active {
|
||||||
taskVM.reload()
|
taskVM.reload()
|
||||||
|
CloudSyncManager.shared.backupSettings()
|
||||||
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>Default</string>
|
<string>Default</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
|
||||||
|
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
104
KisaniCal/Managers/CloudSyncManager.swift
Normal file
104
KisaniCal/Managers/CloudSyncManager.swift
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Mirrors UserDefaults to iCloud Key-Value Store.
|
||||||
|
// On fresh install, restores from iCloud so settings/data survive reinstalls and new builds.
|
||||||
|
// Silently no-ops when iCloud is unavailable.
|
||||||
|
final class CloudSyncManager {
|
||||||
|
static let shared = CloudSyncManager()
|
||||||
|
|
||||||
|
private let kv = NSUbiquitousKeyValueStore.default
|
||||||
|
private let maxBytes = 900_000 // stay under 1 MB total limit
|
||||||
|
|
||||||
|
// Settings keys that are always small enough to sync unconditionally
|
||||||
|
private let settingsKeys: [String] = [
|
||||||
|
"appearanceMode", "showMatrixTab", "showWorkoutTab",
|
||||||
|
"bodyWeightKg", "weightUnit",
|
||||||
|
"kisani.tutorial.done.v1", "kisani.tutorial.today.v1", "kisani.tutorial.workout.v1",
|
||||||
|
"kisani.cal.hiddenIDs", "kisani.cal.localEnabled", "kisani.cal.dnd",
|
||||||
|
"workoutReminderEnabled", "workoutCheckInEnabled",
|
||||||
|
"workoutHour", "workoutMinute", "checkInHour", "checkInMinute",
|
||||||
|
]
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(kvStoreChanged(_:)),
|
||||||
|
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
||||||
|
object: kv
|
||||||
|
)
|
||||||
|
kv.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Write
|
||||||
|
|
||||||
|
/// Push a Data blob to iCloud (tasks, workout programs, etc.)
|
||||||
|
func push(data: Data, forKey key: String) {
|
||||||
|
guard data.count < maxBytes else { return }
|
||||||
|
kv.set(data, forKey: key)
|
||||||
|
kv.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push any plist-compatible value to iCloud (strings, numbers, arrays, dicts)
|
||||||
|
func push(value: Any, forKey key: String) {
|
||||||
|
kv.set(value, forKey: key)
|
||||||
|
kv.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Read / Restore
|
||||||
|
|
||||||
|
/// Copy iCloud value into UserDefaults ONLY when UserDefaults has no value yet.
|
||||||
|
/// This safely restores data after a fresh install or new build without overwriting live data.
|
||||||
|
func restoreIfMissing(key: String) {
|
||||||
|
guard UserDefaults.standard.object(forKey: key) == nil,
|
||||||
|
let icloudValue = kv.object(forKey: key)
|
||||||
|
else { return }
|
||||||
|
UserDefaults.standard.set(icloudValue, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Bulk helpers
|
||||||
|
|
||||||
|
/// Backup all small settings keys from UserDefaults → iCloud
|
||||||
|
func backupSettings() {
|
||||||
|
for key in settingsKeys {
|
||||||
|
if let val = UserDefaults.standard.object(forKey: key) {
|
||||||
|
kv.set(val, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kv.synchronize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore all settings keys from iCloud → UserDefaults where missing
|
||||||
|
func restoreSettings() {
|
||||||
|
for key in settingsKeys {
|
||||||
|
restoreIfMissing(key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - External change handler
|
||||||
|
|
||||||
|
/// When another device (or a reinstall) writes to iCloud KV store,
|
||||||
|
/// pull the updated values into UserDefaults and notify the app.
|
||||||
|
@objc private func kvStoreChanged(_ notification: Notification) {
|
||||||
|
guard let info = notification.userInfo,
|
||||||
|
let changedKeys = info[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String]
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
for key in changedKeys {
|
||||||
|
if let val = kv.object(forKey: key) {
|
||||||
|
UserDefaults.standard.set(val, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .kisaniCloudDataRefreshed,
|
||||||
|
object: nil,
|
||||||
|
userInfo: ["keys": changedKeys]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notification.Name {
|
||||||
|
static let kisaniCloudDataRefreshed = Notification.Name("kisani.cloud.refreshed")
|
||||||
|
}
|
||||||
@@ -216,6 +216,12 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
self.kActivePid = "kisani.workout.activePid.\(uid)"
|
||||||
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
|
self.kWorkoutDates = "kisani.workout.completedDates.\(uid)"
|
||||||
|
|
||||||
|
// Restore all workout keys from iCloud if missing locally (fresh install / new build)
|
||||||
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.programs.\(uid)")
|
||||||
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.schedule.\(uid)")
|
||||||
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.activePid.\(uid)")
|
||||||
|
CloudSyncManager.shared.restoreIfMissing(key: "kisani.workout.completedDates.\(uid)")
|
||||||
|
|
||||||
// ── Attempt to restore persisted state ──
|
// ── Attempt to restore persisted state ──
|
||||||
if let data = UserDefaults.standard.data(forKey: kPrograms),
|
if let data = UserDefaults.standard.data(forKey: kPrograms),
|
||||||
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
let saved = try? JSONDecoder().decode([WorkoutProgram].self, from: data),
|
||||||
@@ -283,11 +289,15 @@ class WorkoutViewModel: ObservableObject {
|
|||||||
private func save() {
|
private func save() {
|
||||||
if let data = try? JSONEncoder().encode(programs) {
|
if let data = try? JSONEncoder().encode(programs) {
|
||||||
UserDefaults.standard.set(data, forKey: kPrograms)
|
UserDefaults.standard.set(data, forKey: kPrograms)
|
||||||
|
CloudSyncManager.shared.push(data: data, forKey: kPrograms)
|
||||||
}
|
}
|
||||||
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
let schedDict = schedule.reduce(into: [String: String]()) { $0[String($1.key)] = $1.value.uuidString }
|
||||||
UserDefaults.standard.set(schedDict, forKey: kSchedule)
|
UserDefaults.standard.set(schedDict, forKey: kSchedule)
|
||||||
UserDefaults.standard.set(activeProgramId.uuidString, forKey: kActivePid)
|
UserDefaults.standard.set(activeProgramId.uuidString, forKey: kActivePid)
|
||||||
UserDefaults.standard.set(workoutDates, forKey: kWorkoutDates)
|
UserDefaults.standard.set(workoutDates, forKey: kWorkoutDates)
|
||||||
|
CloudSyncManager.shared.push(value: schedDict, forKey: kSchedule)
|
||||||
|
CloudSyncManager.shared.push(value: activeProgramId.uuidString, forKey: kActivePid)
|
||||||
|
CloudSyncManager.shared.push(value: workoutDates, forKey: kWorkoutDates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logWorkoutCompleted(on date: Date = Date()) {
|
func logWorkoutCompleted(on date: Date = Date()) {
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ class TaskViewModel: ObservableObject {
|
|||||||
init() {
|
init() {
|
||||||
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
let uid = AuthManager.shared.currentUser?.id ?? "shared"
|
||||||
self.persistKey = "kisani.tasks.v2.\(uid)"
|
self.persistKey = "kisani.tasks.v2.\(uid)"
|
||||||
|
CloudSyncManager.shared.restoreIfMissing(key: persistKey)
|
||||||
if let data = UserDefaults.standard.data(forKey: persistKey),
|
if let data = UserDefaults.standard.data(forKey: persistKey),
|
||||||
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
let saved = try? JSONDecoder().decode([TaskItem].self, from: data), !saved.isEmpty {
|
||||||
tasks = saved
|
tasks = saved
|
||||||
@@ -147,6 +148,7 @@ class TaskViewModel: ObservableObject {
|
|||||||
private func save() {
|
private func save() {
|
||||||
if let data = try? JSONEncoder().encode(tasks) {
|
if let data = try? JSONEncoder().encode(tasks) {
|
||||||
UserDefaults.standard.set(data, forKey: persistKey)
|
UserDefaults.standard.set(data, forKey: persistKey)
|
||||||
|
CloudSyncManager.shared.push(data: data, forKey: persistKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user