Remove widget extension

Delete KisaniCalWidget target, Shared/WidgetDataStore, WidgetBridge,
App Group entitlements, and all syncWidget() calls.
This commit is contained in:
kutesir
2026-05-29 10:36:39 +03:00
parent 1d4d7f07c5
commit 28f8b4a0f1
9 changed files with 3 additions and 702 deletions

View File

@@ -10,10 +10,7 @@
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.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 */; };
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 */; };
@@ -24,57 +21,25 @@
A3B2D6622A2EE35C8D5A3C9B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72308FEE0226F45414C04DDD /* SettingsView.swift */; };
A9FF93259AE8FF0ABF69D71A /* DesignTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4A35C0E1270E2E15C03F23 /* DesignTokens.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 */; };
CBE7295BF5ADE08FE93AFAAF /* FloatingTabState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */; };
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */; };
D591A72235A53D4038FBC2B4 /* KisaniCalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */; };
EF03EC9F974B14D100DD5528 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */; };
/* 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 */
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
@@ -85,15 +50,6 @@
/* End PBXFileReference section */
/* Begin PBXGroup section */
10E8E769F7D262FF425B460B /* KisaniCalWidget */ = {
isa = PBXGroup;
children = (
74EE7C2E74094E3E27FD78E4 /* KisaniCalWidget.entitlements */,
825C519927190A5CDB3CD4A8 /* KisaniCalWidget.swift */,
);
path = KisaniCalWidget;
sourceTree = "<group>";
};
21B93C269F283F11B415B18C /* Components */ = {
isa = PBXGroup;
children = (
@@ -103,14 +59,6 @@
path = Components;
sourceTree = "<group>";
};
2217B7EEC45957B820311EC7 /* Shared */ = {
isa = PBXGroup;
children = (
A3364C849866EAAA5FEB46A1 /* WidgetDataStore.swift */,
);
path = Shared;
sourceTree = "<group>";
};
23CBCF100C5EF55E737379CA /* Models */ = {
isa = PBXGroup;
children = (
@@ -124,8 +72,6 @@
isa = PBXGroup;
children = (
F70DA4746C68CD405435DAB6 /* KisaniCal */,
10E8E769F7D262FF425B460B /* KisaniCalWidget */,
2217B7EEC45957B820311EC7 /* Shared */,
51BD1B5DEDE9FAD9CA2FF6DA /* Products */,
);
sourceTree = "<group>";
@@ -134,7 +80,6 @@
isa = PBXGroup;
children = (
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */,
363924D4E7E82486E23C1C57 /* KisaniCalWidget.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -175,9 +120,7 @@
children = (
72FDF9C8DD37134576356B89 /* Assets.xcassets */,
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */,
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */,
001F67ADC72FBAEC03EB7E01 /* KisaniCalApp.swift */,
8934E4F21B90C9650F71D0A2 /* WidgetBridge.swift */,
21B93C269F283F11B415B18C /* Components */,
FB9BF734B9E493EEB09ACE21 /* Managers */,
23CBCF100C5EF55E737379CA /* Models */,
@@ -205,12 +148,10 @@
buildPhases = (
82EA49FE155821D424C46912 /* Sources */,
E1F1A3EE4469B851B972E4AE /* Resources */,
B3BE3000B362E4DDC4EE9E76 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */,
);
name = KisaniCal;
packageProductDependencies = (
@@ -219,23 +160,6 @@
productReference = 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */;
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 */
/* Begin PBXProject section */
@@ -248,9 +172,6 @@
297C0C2BFBA10AB52D5D49CE = {
ProvisioningStyle = Automatic;
};
532C52E96C1EE1E30A7E9157 = {
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = EF1C4F3BDA4AA7E008FC2CDE /* Build configuration list for PBXProject "KisaniCal" */;
@@ -268,7 +189,6 @@
projectRoot = "";
targets = (
297C0C2BFBA10AB52D5D49CE /* KisaniCal */,
532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */,
);
};
/* End PBXProject section */
@@ -305,38 +225,18 @@
6921CB73A3257502FF778381 /* SplashView.swift in Sources */,
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */,
C79C33BE2802E81AA00175CC /* TodayView.swift in Sources */,
4F829CF0B3BD04CD2EAB8C73 /* WidgetBridge.swift in Sources */,
3256F64040E5BBCD30644047 /* WidgetDataStore.swift in Sources */,
9E3F966F6B6AF2E4F8109E51 /* WorkoutView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B3D66A55B74C7B9AE6099149 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C1B3FC29820FCAF069D2C6FE /* KisaniCalWidget.swift in Sources */,
CF130E143FC6FFE2FE853FB6 /* WidgetDataStore.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3FA0FA885B1A4FC539D57049 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 532C52E96C1EE1E30A7E9157 /* KisaniCalWidget */;
targetProxy = D4A1B57264F223C8871E1BC9 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
0CEA6637F45C32A237FBA20D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
ENABLE_PREVIEWS = YES;
@@ -417,40 +317,6 @@
};
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 = 17.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 = 17.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 */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -519,7 +385,6 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
ENABLE_PREVIEWS = YES;
@@ -547,15 +412,6 @@
/* End XCBuildConfiguration 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" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -2,9 +2,7 @@
<!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>
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
<true/>
</dict>
</plist>

View File

@@ -76,18 +76,14 @@ struct ContentView: View {
}
.onAppear {
if !hasOnboarded { showOnboarding = true }
taskVM.syncWidget()
workoutVM.syncWidget()
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
.onChange(of: scenePhase) { phase in
if phase == .active {
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
taskVM.syncWidget()
workoutVM.syncWidget()
}
}
.onChange(of: taskVM.tasks) { _ in
taskVM.syncWidget()
NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM)
}
.onChange(of: workoutVM.doneSets) { _ in

View File

@@ -1,41 +0,0 @@
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

@@ -1,10 +0,0 @@
<?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

@@ -1,414 +0,0 @@
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

@@ -1,61 +0,0 @@
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,7 +13,6 @@ targets:
deploymentTarget: "16.0"
sources:
- path: KisaniCal
- path: Shared
settings:
base:
SWIFT_VERSION: 5.9
@@ -29,27 +28,5 @@ targets:
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
CODE_SIGN_STYLE: Automatic
ENABLE_PREVIEWS: YES
CODE_SIGN_ENTITLEMENTS: KisaniCal/KisaniCal.entitlements
INFOPLIST_KEY_NSCalendarsUsageDescription: "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: "17.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"