feat: add widget extension — small, medium, large

- KisaniCalWidget: 3 sizes sharing the app's design language
  Small: date badge + kisaniCAL. wordmark + task count
  Medium: badge + task list (3 items) + workout row
  Large: badge + full task list (6 items) + workout row

- WidgetDataStore (Shared/): lightweight Codable types for App Group
  UserDefaults cross-process data exchange

- WidgetBridge: TaskViewModel/WorkoutViewModel extensions that sync
  data into App Group and call WidgetCenter.reloadAllTimelines()

- ContentView: calls syncWidget() on appear and scene becoming active

- project.yml + entitlements updated; project regenerated via xcodegen
This commit is contained in:
kutesir
2026-05-28 19:48:01 +03:00
parent 05a579e2f7
commit 5e232f2d81
9 changed files with 739 additions and 24 deletions

View File

@@ -10,7 +10,12 @@
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; }; 096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; }; 13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; }; 1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
3256F64040E5BBCD30644047 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; };
3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; }; 3C793FD5DA00D3E9C0D51FEC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72FDF9C8DD37134576356B89 /* Assets.xcassets */; };
46D78859DD34FDE6172F213D /* KisaniCalWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
4F829CF0B3BD04CD2EAB8C73 /* WidgetBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */; };
67CE1747E5DB3FDA79D0FDFD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106EEF572C6F8990408329F0 /* OnboardingView.swift */; };
6921CB73A3257502FF778381 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */; };
7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; }; 7CEBC340BFA9238D121946AC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */; };
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 */; };
@@ -19,35 +24,74 @@
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 */; };
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; }; BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44530A77DF12A17E52AAF34 /* MatrixView.swift */; };
C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */; };
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; }; C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C786EBC7DF879D64EB28165E /* TodayView.swift */; };
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; };
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; }; D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */; }; EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */; };
E2F3A4B5C6D7E8F9A0B1C2D3 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
D4A1B57264F223C8871E1BC9 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = AF6DE7A812408E3742522E90 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 532C52E96C1EE1E30A7E9157;
remoteInfo = KisaniCalWidget;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
46D78859DD34FDE6172F213D /* KisaniCalWidget.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; }; 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalApp.swift; sourceTree = "<group>"; };
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; }; 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutView.swift; sourceTree = "<group>"; };
23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; }; 23A4491BFA50721082024756 /* SharedComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedComponents.swift; sourceTree = "<group>"; };
363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; }; 5722CC4B59E3939724142710 /* ExerciseModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExerciseModels.swift; sourceTree = "<group>"; };
57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 57714B79AFFF60DA90EB86E3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 72308FEE0226F45414C04DDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 72FDF9C8DD37134576356B89 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
74EE7C2E74094E3E27FD78E4 /* KisaniCalWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCalWidget.entitlements; sourceTree = "<group>"; };
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>"; };
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KisaniCalWidget.swift; sourceTree = "<group>"; };
8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBridge.swift; sourceTree = "<group>"; };
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.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>"; };
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>"; };
BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; }; BD4A35C0E1270E2E15C03F23 /* DesignTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesignTokens.swift; sourceTree = "<group>"; };
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; }; C786EBC7DF879D64EB28165E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; }; D44530A77DF12A17E52AAF34 /* MatrixView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixView.swift; sourceTree = "<group>"; };
DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; }; DCC2AFB4FA765383740767CB /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
10E8E769F7D262FF425B460B /* KisaniCalWidget */ = {
isa = PBXGroup;
children = (
74EE7C2E74094E3E27FD78E4 /* KisaniCalWidget.entitlements */,
825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */,
);
path = KisaniCalWidget;
sourceTree = "<group>";
};
21B93C269F283F11B415B18C /* Components */ = { 21B93C269F283F11B415B18C /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -56,6 +100,14 @@
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
2217B7EEC45957B820311EC7 /* Shared */ = {
isa = PBXGroup;
children = (
A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */,
);
path = Shared;
sourceTree = "<group>";
};
23CBCF100C5EF55E737379CA /* Models */ = { 23CBCF100C5EF55E737379CA /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -69,6 +121,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F70DA4746C68CD405435DAB6 /* KisaniCal */, F70DA4746C68CD405435DAB6 /* KisaniCal */,
10E8E769F7D262FF425B460B /* KisaniCalWidget */,
2217B7EEC45957B820311EC7 /* Shared */,
51BD1B5DEDE9FAD9CA2FF6DA /* Products */, 51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@@ -77,6 +131,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */, 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -87,11 +142,11 @@
768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */, 768D4E314252E2A90A06CCF2 /* AddExerciseSheet.swift */,
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */, ADF6CCD95A587E26E30F5712 /* CalendarView.swift */,
D44530A77DF12A17E52AAF34 /* MatrixView.swift */, D44530A77DF12A17E52AAF34 /* MatrixView.swift */,
106EEF572C6F8990408329F0 /* OnboardingView.swift */,
72308FEE0226F45414C04DDD /* SettingsView.swift */, 72308FEE0226F45414C04DDD /* SettingsView.swift */,
BE2D50B4B4AC227BD21F4B60 /* SplashView.swift */,
C786EBC7DF879D64EB28165E /* TodayView.swift */, C786EBC7DF879D64EB28165E /* TodayView.swift */,
20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */, 20DAD771EFCC8B3B1DBD4DD6 /* WorkoutView.swift */,
C2D3E4F5A6B7C8D9E0F1A2B3 /* OnboardingView.swift */,
D3E4F5A6B7C8D9E0F1A2B3C4 /* SplashView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -112,22 +167,16 @@
path = Theme; path = Theme;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A1B2C3D4E5F6070A8B9C0D1E /* Managers */ = {
isa = PBXGroup;
children = (
F7A1B2C3D4E5F6A7B8C9D0E1 /* NotificationManager.swift */,
);
path = Managers;
sourceTree = "<group>";
};
F70DA4746C68CD405435DAB6 /* KisaniCal */ = { F70DA4746C68CD405435DAB6 /* KisaniCal */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
72FDF9C8DD37134576356B89 /* Assets.xcassets */, 72FDF9C8DD37134576356B89 /* Assets.xcassets */,
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */, BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */,
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */, 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */,
21B93C269F283F11B415B18C /* Components */, 21B93C269F283F11B415B18C /* Components */,
A1B2C3D4E5F6070A8B9C0D1E /* Managers */, FB9BF734B9E493EEB09ACE21 /* Managers */,
23CBCF100C5EF55E737379CA /* Models */, 23CBCF100C5EF55E737379CA /* Models */,
9981C53A4E263A0DBC3DF194 /* Preview Content */, 9981C53A4E263A0DBC3DF194 /* Preview Content */,
E0A03E79A679740978E61BF1 /* Theme */, E0A03E79A679740978E61BF1 /* Theme */,
@@ -136,6 +185,14 @@
path = KisaniCal; path = KisaniCal;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
FB9BF734B9E493EEB09ACE21 /* Managers */ = {
isa = PBXGroup;
children = (
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */,
);
path = Managers;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -145,10 +202,12 @@
buildPhases = ( buildPhases = (
82EA49FE155821D424C46912 /* Sources */, 82EA49FE155821D424C46912 /* Sources */,
E1F1A3EE4469B851B972E4AE /* Resources */, E1F1A3EE4469B851B972E4AE /* Resources */,
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */,
); );
name = KisaniCal; name = KisaniCal;
packageProductDependencies = ( packageProductDependencies = (
@@ -157,6 +216,23 @@
productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */; productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6B8469B838ACDD00336D1CAD /* Build configuration list for PBXNativeTarget "KisaniCalWidget" */;
buildPhases = (
B3D66A55B74C7B9AE6099149 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = KisaniCalWidget;
packageProductDependencies = (
);
productName = KisaniCalWidget;
productReference = 363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@@ -169,6 +245,9 @@
297C0C2BFBA10AB52D5D49CE = { 297C0C2BFBA10AB52D5D49CE = {
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
532C52E96C1EE1E30A7E9157 = {
ProvisioningStyle = Automatic;
};
}; };
}; };
buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */; buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */;
@@ -186,6 +265,7 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
297C0C2BFBA10AB52D5D49CE /* KisaniCal */, 297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -214,28 +294,47 @@
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */, 1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */,
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */, D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */,
BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */, BEAFF968632A34C70B11C5AC /* MatrixView.swift in Sources */,
EF03EC9F974B14D100DD5528 /* NotificationManager.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 */,
6921CB73A3257502FF778381 /* SplashView.swift in Sources */,
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */, 096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
E7F1A2B3C4D5E6F7A8B9C0D1 /* NotificationManager.swift in Sources */,
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */, C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
4F829CF0B3BD04CD2EAB8C73 /* WidgetBridge.swift in Sources */,
3256F64040E5BBCD30644047 /* WidgetDataStore.swift in Sources */,
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */, 9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
B1C2D3E4F5A6B7C8D9E0F1A2 /* OnboardingView.swift in Sources */, );
E2F3A4B5C6D7E8F9A0B1C2D3 /* SplashView.swift in Sources */, runOnlyForDeploymentPostprocessing = 0;
};
B3D66A55B74C7B9AE6099149 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */,
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */;
targetProxy = D4A1B57264F223C8871E1BC9 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
0CEA6637F45C32A237FBA20D /* Release */ = { 0CEA6637F45C32A237FBA20D /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = K8BLMMR883;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal"; INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
@@ -314,6 +413,40 @@
}; };
name = Release; name = Release;
}; };
47C8DA15444717168992CD67 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = KisaniCalWidget/KisaniCalWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
9F668AA275094D23AACA2F61 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = KisaniCalWidget/KisaniCalWidget.entitlements;
CODE_SIGN_STYLE = Automatic;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSExtensionPointIdentifier = "com.apple.widgetkit-extension";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.widget;
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.9;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
BD527AA54887489CC665FB9D /* Debug */ = { BD527AA54887489CC665FB9D /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -382,9 +515,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = K8BLMMR883;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal"; INFOPLIST_KEY_CFBundleDisplayName = "Kisani Cal";
@@ -410,6 +543,15 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
6B8469B838ACDD00336D1CAD /* Build configuration list for PBXNativeTarget "KisaniCalWidget" */ = {
isa = XCConfigurationList;
buildConfigurations = (
47C8DA15444717168992CD67 /* Debug */,
9F668AA275094D23AACA2F61 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = { B60FDD2C88D1D33CF1EC02B3 /* Build configuration list for PBXNativeTarget "KisaniCal" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -73,12 +73,19 @@ struct ContentView: View {
OnboardingView(isPresented: $showOnboarding) OnboardingView(isPresented: $showOnboarding)
.onDisappear { hasOnboarded = true } .onDisappear { hasOnboarded = true }
} }
.onAppear { if !hasOnboarded { showOnboarding = true } } .onAppear {
if !hasOnboarded { showOnboarding = true }
taskVM.syncWidget()
workoutVM.syncWidget()
}
.onChange(of: scenePhase) { phase in .onChange(of: scenePhase) { phase in
if phase == .active { if phase == .active {
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
taskVM.syncWidget()
workoutVM.syncWidget()
} }
} }
.onChange(of: taskVM.tasks.count) { _ in taskVM.syncWidget() }
.onChange(of: workoutVM.doneSets) { _ in .onChange(of: workoutVM.doneSets) { _ in
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
} }

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.kutesir.KisaniCal</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,41 @@
import WidgetKit
// Syncs app ViewModels into App Group UserDefaults so the widget can read them,
// then asks WidgetKit to reload all timelines.
extension TaskViewModel {
func syncWidget() {
let cal = Calendar.current
let start = cal.startOfDay(for: Date())
let end = cal.date(byAdding: .day, value: 1, to: start)!
let entries = tasks
.filter { !$0.isComplete }
.prefix(10)
.map { t in
WidgetDataStore.WidgetTask(
id: t.id,
title: t.title,
dueDate: t.dueDate,
isToday: t.dueDate.map { $0 >= start && $0 < end } ?? false,
isOverdue: t.dueDate.map { $0 < start } ?? false
)
}
WidgetDataStore.saveTasks(Array(entries))
WidgetCenter.shared.reloadAllTimelines()
}
}
extension WorkoutViewModel {
func syncWidget() {
let weekday = Calendar.current.component(.weekday, from: Date())
let todayProgram = program(for: Date())
let workout = WidgetDataStore.WidgetWorkout(
name: activeProgram?.name ?? "Workout",
sectionsCount: activeProgram?.sections.count ?? 0,
hasWorkoutToday: todayProgram != nil
)
WidgetDataStore.saveWorkout(workout)
WidgetCenter.shared.reloadAllTimelines()
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.kutesir.KisaniCal</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,414 @@
import WidgetKit
import SwiftUI
// MARK: - Colors (no UIKit dependency)
private enum WC {
static let accent = Color(red: 232/255, green: 98/255, blue: 42/255)
static let green = Color(red: 62/255, green: 207/255, blue: 142/255)
static let blue = Color(red: 77/255, green: 157/255, blue: 224/255)
static func bg(_ s: ColorScheme) -> Color {
s == .dark ? Color(red: 9/255, green: 9/255, blue: 9/255)
: Color(red: 245/255, green: 243/255, blue: 240/255)
}
static func surface(_ s: ColorScheme) -> Color {
s == .dark ? Color(red: 19/255, green: 19/255, blue: 19/255)
: Color(red: 255/255, green: 253/255, blue: 250/255)
}
static func text(_ s: ColorScheme) -> Color {
s == .dark ? Color(red: 240/255, green: 237/255, blue: 232/255)
: Color(red: 26/255, green: 23/255, blue: 20/255)
}
static func text2(_ s: ColorScheme) -> Color { text(s).opacity(0.48) }
static func text3(_ s: ColorScheme) -> Color { text(s).opacity(0.30) }
static func border(_ s: ColorScheme) -> Color {
s == .dark ? Color.white.opacity(0.07) : Color.black.opacity(0.09)
}
}
// MARK: - Timeline Entry
struct KisaniEntry: TimelineEntry {
let date: Date
let todayTasks: [WidgetDataStore.WidgetTask]
let overdueTasks: [WidgetDataStore.WidgetTask]
let workout: WidgetDataStore.WidgetWorkout?
}
// MARK: - Provider
struct KisaniProvider: TimelineProvider {
func placeholder(in context: Context) -> KisaniEntry {
KisaniEntry(
date: Date(),
todayTasks: previewTasks,
overdueTasks: [],
workout: WidgetDataStore.WidgetWorkout(
name: "Shoulders & Arms", sectionsCount: 4, hasWorkoutToday: true)
)
}
func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) {
completion(entry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<KisaniEntry>) -> Void) {
let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
completion(Timeline(entries: [entry()], policy: .after(next)))
}
private func entry() -> KisaniEntry {
let all = WidgetDataStore.loadTasks()
let today = all.filter { $0.isToday }
let overdue = all.filter { $0.isOverdue }
let workout = WidgetDataStore.loadWorkout()
return KisaniEntry(date: Date(), todayTasks: today, overdueTasks: overdue, workout: workout)
}
private var previewTasks: [WidgetDataStore.WidgetTask] {
[
.init(id: UUID(), title: "Azure exam prep", dueDate: Date(), isToday: true, isOverdue: false),
.init(id: UUID(), title: "Push AWS project", dueDate: Date(), isToday: true, isOverdue: false),
.init(id: UUID(), title: "Review PR #42", dueDate: Date(), isToday: false, isOverdue: true),
]
}
}
// MARK: - Widget Bundle
@main
struct KisaniCalWidgetBundle: WidgetBundle {
var body: some Widget {
KisaniCalWidget()
}
}
struct KisaniCalWidget: Widget {
let kind = "KisaniCalWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
KisaniWidgetView(entry: entry)
.containerBackground(for: .widget) { Color.clear }
}
.configurationDisplayName("Kisani Cal")
.description("Today's tasks and workout at a glance.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
// MARK: - Root Widget View
struct KisaniWidgetView: View {
@Environment(\.widgetFamily) private var family
let entry: KisaniEntry
var body: some View {
switch family {
case .systemSmall: SmallWidget(entry: entry)
case .systemMedium: MediumWidget(entry: entry)
default: LargeWidget(entry: entry)
}
}
}
// MARK: - Shared Sub-Views
private struct DateBadge: View {
@Environment(\.colorScheme) private var cs
let size: CGFloat
private static let monthFmt: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "MMM"; return f
}()
var body: some View {
let day = Calendar.current.component(.day, from: Date())
VStack(spacing: 0) {
Text(Self.monthFmt.string(from: Date()).uppercased())
.font(.system(size: size * 0.18, weight: .bold, design: .monospaced))
.foregroundColor(.white)
.kerning(1)
.frame(maxWidth: .infinity)
.frame(height: size * 0.32)
.background(WC.accent)
Text("\(day)")
.font(.system(size: size * 0.48, weight: .bold, design: .monospaced))
.foregroundColor(WC.text(cs))
.frame(maxWidth: .infinity)
.frame(height: size * 0.68)
.background(WC.surface(cs))
}
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: size * 0.22))
.overlay(
RoundedRectangle(cornerRadius: size * 0.22)
.stroke(WC.border(cs), lineWidth: 1)
)
}
}
private struct TaskRow: View {
@Environment(\.colorScheme) private var cs
let task: WidgetDataStore.WidgetTask
private static let fmt: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "d MMM"; return f
}()
var body: some View {
HStack(spacing: 5) {
Circle()
.stroke(task.isOverdue ? WC.accent : WC.text3(cs), lineWidth: 1.2)
.frame(width: 7, height: 7)
Text(task.title)
.font(.system(size: 11, weight: .regular, design: .default))
.foregroundColor(WC.text(cs))
.lineLimit(1)
Spacer(minLength: 0)
if let d = task.dueDate {
Text(task.isOverdue ? "Overdue" : Self.fmt.string(from: d))
.font(.system(size: 9, weight: .regular, design: .monospaced))
.foregroundColor(task.isOverdue ? WC.accent : WC.text3(cs))
}
}
}
}
private struct WorkoutRow: View {
@Environment(\.colorScheme) private var cs
let workout: WidgetDataStore.WidgetWorkout
var body: some View {
HStack(spacing: 6) {
WorkoutSegBar(count: workout.sectionsCount)
.frame(width: 22, height: 22)
Text(workout.name)
.font(.system(size: 11, weight: .semibold, design: .default))
.foregroundColor(WC.text(cs))
.lineLimit(1)
Spacer(minLength: 0)
Text("Workout")
.font(.system(size: 9, weight: .regular, design: .monospaced))
.foregroundColor(WC.blue)
}
}
}
private struct WorkoutSegBar: View {
@Environment(\.colorScheme) private var cs
let count: Int
var body: some View {
let n = max(1, min(6, count))
VStack(spacing: 1.5) {
ForEach(0..<n, id: \.self) { _ in
RoundedRectangle(cornerRadius: 1)
.fill(WC.blue)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.padding(4)
.background(WC.blue.opacity(0.12))
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
// MARK: - Small Widget
private struct SmallWidget: View {
@Environment(\.colorScheme) private var cs
let entry: KisaniEntry
var body: some View {
ZStack {
WC.bg(cs).ignoresSafeArea()
VStack(spacing: 10) {
DateBadge(size: 72)
VStack(spacing: 2) {
HStack(spacing: 0) {
Text("kisani")
.font(.system(size: 11, weight: .regular, design: .monospaced))
.foregroundColor(WC.text2(cs))
Text("CAL")
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundColor(WC.text(cs))
Text(".")
.font(.system(size: 11, weight: .bold, design: .monospaced))
.foregroundColor(WC.accent)
}
let todayCount = entry.todayTasks.count
let overdueCount = entry.overdueTasks.count
Group {
if overdueCount > 0 {
Text("\(overdueCount) overdue · \(todayCount) today")
.foregroundColor(WC.accent)
} else if todayCount > 0 {
Text("\(todayCount) task\(todayCount == 1 ? "" : "s") today")
.foregroundColor(WC.text2(cs))
} else {
Text("All clear")
.foregroundColor(WC.text3(cs))
}
}
.font(.system(size: 9, weight: .regular, design: .monospaced))
}
}
.padding(14)
}
}
}
// MARK: - Medium Widget
private struct MediumWidget: View {
@Environment(\.colorScheme) private var cs
let entry: KisaniEntry
private var displayTasks: [WidgetDataStore.WidgetTask] {
let all = entry.overdueTasks + entry.todayTasks
return Array(all.prefix(3))
}
var body: some View {
ZStack {
WC.bg(cs).ignoresSafeArea()
HStack(alignment: .top, spacing: 14) {
// Left: badge + brand
VStack(spacing: 8) {
DateBadge(size: 64)
HStack(spacing: 0) {
Text("kisani")
.font(.system(size: 9, weight: .regular, design: .monospaced))
.foregroundColor(WC.text2(cs))
Text("CAL.")
.font(.system(size: 9, weight: .bold, design: .monospaced))
.foregroundColor(WC.accent)
}
}
// Right: tasks + workout
VStack(alignment: .leading, spacing: 5) {
if displayTasks.isEmpty && entry.workout == nil {
Spacer()
Text("Nothing scheduled")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(WC.text3(cs))
Spacer()
} else {
ForEach(displayTasks) { task in
TaskRow(task: task)
}
if let workout = entry.workout, workout.hasWorkoutToday,
displayTasks.count < 3 {
if !displayTasks.isEmpty {
Divider().opacity(0.4)
}
WorkoutRow(workout: workout)
}
Spacer(minLength: 0)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(14)
}
}
}
// MARK: - Large Widget
private struct LargeWidget: View {
@Environment(\.colorScheme) private var cs
let entry: KisaniEntry
private static let dayFmt: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "EEEE, MMM d"; return f
}()
private var allTasks: [WidgetDataStore.WidgetTask] {
Array((entry.overdueTasks + entry.todayTasks).prefix(6))
}
var body: some View {
ZStack {
WC.bg(cs).ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
// Header row
HStack(spacing: 12) {
DateBadge(size: 56)
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 0) {
Text("kisani")
.font(.system(size: 14, weight: .regular, design: .monospaced))
.foregroundColor(WC.text2(cs))
Text("CAL")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(WC.text(cs))
Text(".")
.font(.system(size: 14, weight: .bold, design: .monospaced))
.foregroundColor(WC.accent)
}
Text(Self.dayFmt.string(from: Date()))
.font(.system(size: 10, design: .monospaced))
.foregroundColor(WC.text2(cs))
}
Spacer()
let total = entry.todayTasks.count + entry.overdueTasks.count
VStack(alignment: .trailing, spacing: 2) {
Text("\(total)")
.font(.system(size: 22, weight: .bold, design: .monospaced))
.foregroundColor(entry.overdueTasks.isEmpty ? WC.text(cs) : WC.accent)
Text("tasks")
.font(.system(size: 9, design: .monospaced))
.foregroundColor(WC.text3(cs))
}
}
.padding(.horizontal, 14)
.padding(.top, 14)
.padding(.bottom, 12)
Divider().opacity(0.4).padding(.horizontal, 14)
// Task list
VStack(alignment: .leading, spacing: 7) {
if allTasks.isEmpty {
HStack {
Spacer()
Text("Nothing due today")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(WC.text3(cs))
Spacer()
}
.padding(.vertical, 8)
} else {
ForEach(allTasks) { task in
TaskRow(task: task)
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
Spacer(minLength: 0)
// Workout row
if let workout = entry.workout, workout.hasWorkoutToday {
Divider().opacity(0.4).padding(.horizontal, 14)
WorkoutRow(workout: workout)
.padding(.horizontal, 14)
.padding(.vertical, 10)
}
}
}
}
}

View File

@@ -0,0 +1,61 @@
import Foundation
// Shared between app and widget extension via App Group UserDefaults.
// Types are pure Codable no UIKit / AppColors dependency.
struct WidgetDataStore {
static let suiteName = "group.com.kutesir.KisaniCal"
static let tasksKey = "kisani.widget.tasks"
static let workoutKey = "kisani.widget.workout"
// MARK: - Shared types
struct WidgetTask: Codable, Identifiable {
let id: UUID
let title: String
let dueDate: Date?
let isToday: Bool
let isOverdue: Bool
}
struct WidgetWorkout: Codable {
let name: String
let sectionsCount: Int
let hasWorkoutToday: Bool
}
// MARK: - Write (app target)
static func saveTasks(_ tasks: [WidgetTask]) {
guard let data = try? JSONEncoder().encode(tasks),
let ud = UserDefaults(suiteName: suiteName) else { return }
ud.set(data, forKey: tasksKey)
}
static func saveWorkout(_ workout: WidgetWorkout?) {
guard let ud = UserDefaults(suiteName: suiteName) else { return }
if let workout, let data = try? JSONEncoder().encode(workout) {
ud.set(data, forKey: workoutKey)
} else {
ud.removeObject(forKey: workoutKey)
}
}
// MARK: - Read (widget target)
static func loadTasks() -> [WidgetTask] {
guard let ud = UserDefaults(suiteName: suiteName),
let data = ud.data(forKey: tasksKey),
let list = try? JSONDecoder().decode([WidgetTask].self, from: data)
else { return [] }
return list
}
static func loadWorkout() -> WidgetWorkout? {
guard let ud = UserDefaults(suiteName: suiteName),
let data = ud.data(forKey: workoutKey),
let workout = try? JSONDecoder().decode(WidgetWorkout.self, from: data)
else { return nil }
return workout
}
}

View File

@@ -13,6 +13,7 @@ targets:
deploymentTarget: "16.0" deploymentTarget: "16.0"
sources: sources:
- path: KisaniCal - path: KisaniCal
- path: Shared
settings: settings:
base: base:
SWIFT_VERSION: 5.9 SWIFT_VERSION: 5.9
@@ -28,5 +29,27 @@ targets:
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
CODE_SIGN_STYLE: Automatic CODE_SIGN_STYLE: Automatic
ENABLE_PREVIEWS: YES ENABLE_PREVIEWS: YES
CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements
INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks." INFOPLIST_KEY_NSCalendarsUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks." INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription: "Kisani Cal shows your calendar events alongside tasks."
dependencies:
- target: KisaniCalWidget
embed: true
KisaniCalWidget:
type: app-extension
platform: iOS
deploymentTarget: "16.0"
sources:
- path: KisaniCalWidget
- path: Shared
settings:
base:
SWIFT_VERSION: 5.9
PRODUCT_BUNDLE_IDENTIFIER: com.kutesir.KisaniCal.widget
GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_NSExtensionPointIdentifier: com.apple.widgetkit-extension
CODE_SIGN_STYLE: Automatic
CODE_SIGN_ENTITLEMENTS: KisaniCalWidget/KisaniCalWidget.entitlements
SKIP_INSTALL: YES
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"