krz/daybyday

iOS app for children to learn days of the week, months, and seasons with audio assist. education ios kids swift https://krz.sh/apps/daybyday/

Commit a3a414ae0f

a3a414ae0f6b64a5d7977c6cb04f89409170707d

parent: 3fbf073757

Unsigned

cmc <hello@cleberg.net> · 2026-03-10 01:30 UTC

initial 1.0 release

Layout: unified · split

DayByDay.xcodeproj/project.pbxproj added +346
@@ -0,0 +1,346 @@
1// !$*UTF8*$!
2{
3 archiveVersion = 1;
4 classes = {
5 };
6 objectVersion = 77;
7 objects = {
8
9/* Begin PBXBuildFile section */
10 8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; };
11/* End PBXBuildFile section */
12
13/* Begin PBXFileReference section */
14 8B7051B22F5F908800491D58 /* DayByDay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DayByDay.app; sourceTree = BUILT_PRODUCTS_DIR; };
15 8B7051C02F5F90C800491D58 /* claude.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = claude.md; sourceTree = "<group>"; };
16/* End PBXFileReference section */
17
18/* Begin PBXFileSystemSynchronizedRootGroup section */
19 8B7051B42F5F908800491D58 /* DayByDay */ = {
20 isa = PBXFileSystemSynchronizedRootGroup;
21 path = DayByDay;
22 sourceTree = "<group>";
23 };
24/* End PBXFileSystemSynchronizedRootGroup section */
25
26/* Begin PBXFrameworksBuildPhase section */
27 8B7051AF2F5F908800491D58 /* Frameworks */ = {
28 isa = PBXFrameworksBuildPhase;
29 buildActionMask = 2147483647;
30 files = (
31 );
32 runOnlyForDeploymentPostprocessing = 0;
33 };
34/* End PBXFrameworksBuildPhase section */
35
36/* Begin PBXGroup section */
37 8B7051A92F5F908800491D58 = {
38 isa = PBXGroup;
39 children = (
40 8B7051C02F5F90C800491D58 /* claude.md */,
41 8B7051B42F5F908800491D58 /* DayByDay */,
42 8B7051B32F5F908800491D58 /* Products */,
43 );
44 sourceTree = "<group>";
45 };
46 8B7051B32F5F908800491D58 /* Products */ = {
47 isa = PBXGroup;
48 children = (
49 8B7051B22F5F908800491D58 /* DayByDay.app */,
50 );
51 name = Products;
52 sourceTree = "<group>";
53 };
54/* End PBXGroup section */
55
56/* Begin PBXNativeTarget section */
57 8B7051B12F5F908800491D58 /* DayByDay */ = {
58 isa = PBXNativeTarget;
59 buildConfigurationList = 8B7051BD2F5F908900491D58 /* Build configuration list for PBXNativeTarget "DayByDay" */;
60 buildPhases = (
61 8B7051AE2F5F908800491D58 /* Sources */,
62 8B7051AF2F5F908800491D58 /* Frameworks */,
63 8B7051B02F5F908800491D58 /* Resources */,
64 );
65 buildRules = (
66 );
67 dependencies = (
68 );
69 fileSystemSynchronizedGroups = (
70 8B7051B42F5F908800491D58 /* DayByDay */,
71 );
72 name = DayByDay;
73 packageProductDependencies = (
74 );
75 productName = DayByDay;
76 productReference = 8B7051B22F5F908800491D58 /* DayByDay.app */;
77 productType = "com.apple.product-type.application";
78 };
79/* End PBXNativeTarget section */
80
81/* Begin PBXProject section */
82 8B7051AA2F5F908800491D58 /* Project object */ = {
83 isa = PBXProject;
84 attributes = {
85 BuildIndependentTargetsInParallel = 1;
86 LastSwiftUpdateCheck = 2630;
87 LastUpgradeCheck = 2630;
88 TargetAttributes = {
89 8B7051B12F5F908800491D58 = {
90 CreatedOnToolsVersion = 26.3;
91 };
92 };
93 };
94 buildConfigurationList = 8B7051AD2F5F908800491D58 /* Build configuration list for PBXProject "DayByDay" */;
95 developmentRegion = en;
96 hasScannedForEncodings = 0;
97 knownRegions = (
98 en,
99 Base,
100 );
101 mainGroup = 8B7051A92F5F908800491D58;
102 minimizedProjectReferenceProxies = 1;
103 preferredProjectObjectVersion = 77;
104 productRefGroup = 8B7051B32F5F908800491D58 /* Products */;
105 projectDirPath = "";
106 projectRoot = "";
107 targets = (
108 8B7051B12F5F908800491D58 /* DayByDay */,
109 );
110 };
111/* End PBXProject section */
112
113/* Begin PBXResourcesBuildPhase section */
114 8B7051B02F5F908800491D58 /* Resources */ = {
115 isa = PBXResourcesBuildPhase;
116 buildActionMask = 2147483647;
117 files = (
118 8B7051C12F5F90CD00491D58 /* claude.md in Resources */,
119 );
120 runOnlyForDeploymentPostprocessing = 0;
121 };
122/* End PBXResourcesBuildPhase section */
123
124/* Begin PBXSourcesBuildPhase section */
125 8B7051AE2F5F908800491D58 /* Sources */ = {
126 isa = PBXSourcesBuildPhase;
127 buildActionMask = 2147483647;
128 files = (
129 );
130 runOnlyForDeploymentPostprocessing = 0;
131 };
132/* End PBXSourcesBuildPhase section */
133
134/* Begin XCBuildConfiguration section */
135 8B7051BB2F5F908900491D58 /* Debug */ = {
136 isa = XCBuildConfiguration;
137 buildSettings = {
138 ALWAYS_SEARCH_USER_PATHS = NO;
139 ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
140 CLANG_ANALYZER_NONNULL = YES;
141 CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
142 CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
143 CLANG_ENABLE_MODULES = YES;
144 CLANG_ENABLE_OBJC_ARC = YES;
145 CLANG_ENABLE_OBJC_WEAK = YES;
146 CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
147 CLANG_WARN_BOOL_CONVERSION = YES;
148 CLANG_WARN_COMMA = YES;
149 CLANG_WARN_CONSTANT_CONVERSION = YES;
150 CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
151 CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
152 CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
153 CLANG_WARN_EMPTY_BODY = YES;
154 CLANG_WARN_ENUM_CONVERSION = YES;
155 CLANG_WARN_INFINITE_RECURSION = YES;
156 CLANG_WARN_INT_CONVERSION = YES;
157 CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
158 CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
159 CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
160 CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
161 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
162 CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
163 CLANG_WARN_STRICT_PROTOTYPES = YES;
164 CLANG_WARN_SUSPICIOUS_MOVE = YES;
165 CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
166 CLANG_WARN_UNREACHABLE_CODE = YES;
167 CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
168 COPY_PHASE_STRIP = NO;
169 DEBUG_INFORMATION_FORMAT = dwarf;
170 DEVELOPMENT_TEAM = ZCNAX3VL9D;
171 ENABLE_STRICT_OBJC_MSGSEND = YES;
172 ENABLE_TESTABILITY = YES;
173 ENABLE_USER_SCRIPT_SANDBOXING = YES;
174 GCC_C_LANGUAGE_STANDARD = gnu17;
175 GCC_DYNAMIC_NO_PIC = NO;
176 GCC_NO_COMMON_BLOCKS = YES;
177 GCC_OPTIMIZATION_LEVEL = 0;
178 GCC_PREPROCESSOR_DEFINITIONS = (
179 "DEBUG=1",
180 "$(inherited)",
181 );
182 GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
183 GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
184 GCC_WARN_UNDECLARED_SELECTOR = YES;
185 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
186 GCC_WARN_UNUSED_FUNCTION = YES;
187 GCC_WARN_UNUSED_VARIABLE = YES;
188 IPHONEOS_DEPLOYMENT_TARGET = 26.2;
189 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
190 MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
191 MTL_FAST_MATH = YES;
192 ONLY_ACTIVE_ARCH = YES;
193 SDKROOT = iphoneos;
194 SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
195 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
196 };
197 name = Debug;
198 };
199 8B7051BC2F5F908900491D58 /* Release */ = {
200 isa = XCBuildConfiguration;
201 buildSettings = {
202 ALWAYS_SEARCH_USER_PATHS = NO;
203 ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
204 CLANG_ANALYZER_NONNULL = YES;
205 CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
206 CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
207 CLANG_ENABLE_MODULES = YES;
208 CLANG_ENABLE_OBJC_ARC = YES;
209 CLANG_ENABLE_OBJC_WEAK = YES;
210 CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
211 CLANG_WARN_BOOL_CONVERSION = YES;
212 CLANG_WARN_COMMA = YES;
213 CLANG_WARN_CONSTANT_CONVERSION = YES;
214 CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
215 CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
216 CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
217 CLANG_WARN_EMPTY_BODY = YES;
218 CLANG_WARN_ENUM_CONVERSION = YES;
219 CLANG_WARN_INFINITE_RECURSION = YES;
220 CLANG_WARN_INT_CONVERSION = YES;
221 CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
222 CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
223 CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
224 CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
225 CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
226 CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
227 CLANG_WARN_STRICT_PROTOTYPES = YES;
228 CLANG_WARN_SUSPICIOUS_MOVE = YES;
229 CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
230 CLANG_WARN_UNREACHABLE_CODE = YES;
231 CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
232 COPY_PHASE_STRIP = NO;
233 DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
234 DEVELOPMENT_TEAM = ZCNAX3VL9D;
235 ENABLE_NS_ASSERTIONS = NO;
236 ENABLE_STRICT_OBJC_MSGSEND = YES;
237 ENABLE_USER_SCRIPT_SANDBOXING = YES;
238 GCC_C_LANGUAGE_STANDARD = gnu17;
239 GCC_NO_COMMON_BLOCKS = YES;
240 GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
241 GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
242 GCC_WARN_UNDECLARED_SELECTOR = YES;
243 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
244 GCC_WARN_UNUSED_FUNCTION = YES;
245 GCC_WARN_UNUSED_VARIABLE = YES;
246 IPHONEOS_DEPLOYMENT_TARGET = 26.2;
247 LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
248 MTL_ENABLE_DEBUG_INFO = NO;
249 MTL_FAST_MATH = YES;
250 SDKROOT = iphoneos;
251 SWIFT_COMPILATION_MODE = wholemodule;
252 VALIDATE_PRODUCT = YES;
253 };
254 name = Release;
255 };
256 8B7051BE2F5F908900491D58 /* Debug */ = {
257 isa = XCBuildConfiguration;
258 buildSettings = {
259 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
260 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
261 CODE_SIGN_STYLE = Automatic;
262 CURRENT_PROJECT_VERSION = 1;
263 DEVELOPMENT_TEAM = ZCNAX3VL9D;
264 ENABLE_PREVIEWS = YES;
265 GENERATE_INFOPLIST_FILE = YES;
266 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
267 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
268 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
269 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
270 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
271 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
272 LD_RUNPATH_SEARCH_PATHS = (
273 "$(inherited)",
274 "@executable_path/Frameworks",
275 );
276 MARKETING_VERSION = 1.0;
277 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
278 PRODUCT_NAME = "$(TARGET_NAME)";
279 STRING_CATALOG_GENERATE_SYMBOLS = YES;
280 SWIFT_APPROACHABLE_CONCURRENCY = YES;
281 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
282 SWIFT_EMIT_LOC_STRINGS = YES;
283 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
284 SWIFT_VERSION = 5.0;
285 TARGETED_DEVICE_FAMILY = "1,2";
286 };
287 name = Debug;
288 };
289 8B7051BF2F5F908900491D58 /* Release */ = {
290 isa = XCBuildConfiguration;
291 buildSettings = {
292 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
294 CODE_SIGN_STYLE = Automatic;
295 CURRENT_PROJECT_VERSION = 1;
296 DEVELOPMENT_TEAM = ZCNAX3VL9D;
297 ENABLE_PREVIEWS = YES;
298 GENERATE_INFOPLIST_FILE = YES;
299 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
300 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
301 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
302 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
303 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
304 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
305 LD_RUNPATH_SEARCH_PATHS = (
306 "$(inherited)",
307 "@executable_path/Frameworks",
308 );
309 MARKETING_VERSION = 1.0;
310 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
311 PRODUCT_NAME = "$(TARGET_NAME)";
312 STRING_CATALOG_GENERATE_SYMBOLS = YES;
313 SWIFT_APPROACHABLE_CONCURRENCY = YES;
314 SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
315 SWIFT_EMIT_LOC_STRINGS = YES;
316 SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
317 SWIFT_VERSION = 5.0;
318 TARGETED_DEVICE_FAMILY = "1,2";
319 };
320 name = Release;
321 };
322/* End XCBuildConfiguration section */
323
324/* Begin XCConfigurationList section */
325 8B7051AD2F5F908800491D58 /* Build configuration list for PBXProject "DayByDay" */ = {
326 isa = XCConfigurationList;
327 buildConfigurations = (
328 8B7051BB2F5F908900491D58 /* Debug */,
329 8B7051BC2F5F908900491D58 /* Release */,
330 );
331 defaultConfigurationIsVisible = 0;
332 defaultConfigurationName = Release;
333 };
334 8B7051BD2F5F908900491D58 /* Build configuration list for PBXNativeTarget "DayByDay" */ = {
335 isa = XCConfigurationList;
336 buildConfigurations = (
337 8B7051BE2F5F908900491D58 /* Debug */,
338 8B7051BF2F5F908900491D58 /* Release */,
339 );
340 defaultConfigurationIsVisible = 0;
341 defaultConfigurationName = Release;
342 };
343/* End XCConfigurationList section */
344 };
345 rootObject = 8B7051AA2F5F908800491D58 /* Project object */;
346}
DayByDay.xcodeproj/project.xcworkspace/contents.xcworkspacedata added +7
@@ -0,0 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<Workspace
3 version = "1.0">
4 <FileRef
5 location = "self:">
6 </FileRef>
7</Workspace>
DayByDay.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist added +14
@@ -0,0 +1,14 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3<plist version="1.0">
4<dict>
5 <key>SchemeUserState</key>
6 <dict>
7 <key>DayByDay.xcscheme_^#shared#^_</key>
8 <dict>
9 <key>orderHint</key>
10 <integer>0</integer>
11 </dict>
12 </dict>
13</dict>
14</plist>
DayByDay/Assets.xcassets/AccentColor.colorset/Contents.json added +15
@@ -0,0 +1,15 @@
1{
2 "colors" : [
3 {
4 "color" : {
5 "platform" : "universal",
6 "reference" : "systemOrangeColor"
7 },
8 "idiom" : "universal"
9 }
10 ],
11 "info" : {
12 "author" : "xcode",
13 "version" : 1
14 }
15}
DayByDay/Assets.xcassets/AppIcon.appiconset/Contents.json added +37
@@ -0,0 +1,37 @@
1{
2 "images" : [
3 {
4 "filename" : "logo_1024.png",
5 "idiom" : "universal",
6 "platform" : "ios",
7 "size" : "1024x1024"
8 },
9 {
10 "appearances" : [
11 {
12 "appearance" : "luminosity",
13 "value" : "dark"
14 }
15 ],
16 "filename" : "logo_dark_1024.png",
17 "idiom" : "universal",
18 "platform" : "ios",
19 "size" : "1024x1024"
20 },
21 {
22 "appearances" : [
23 {
24 "appearance" : "luminosity",
25 "value" : "tinted"
26 }
27 ],
28 "idiom" : "universal",
29 "platform" : "ios",
30 "size" : "1024x1024"
31 }
32 ],
33 "info" : {
34 "author" : "xcode",
35 "version" : 1
36 }
37}
DayByDay/Assets.xcassets/AppIcon.appiconset/logo_1024.png added

Binary file not shown.

DayByDay/Assets.xcassets/AppIcon.appiconset/logo_dark_1024.png added

Binary file not shown.

DayByDay/Assets.xcassets/Contents.json added +6
@@ -0,0 +1,6 @@
1{
2 "info" : {
3 "author" : "xcode",
4 "version" : 1
5 }
6}
DayByDay/ContentView.swift added +110
@@ -0,0 +1,110 @@
1//
2// ContentView.swift
3// DayByDay
4//
5// Created by cmc on 2026-03-09.
6//
7
8import SwiftUI
9
10struct ContentView: View {
11 @State private var selectedTab = 0
12 @AppStorage("hasSeenVoiceTip") private var hasSeenVoiceTip = false
13
14 private let tabs: [(label: String, symbol: String)] = [
15 ("Days", "calendar"),
16 ("Months", "calendar.badge.clock"),
17 ("Seasons", "leaf.fill"),
18 ]
19
20 var body: some View {
21 VStack(spacing: 0) {
22 Group {
23 switch selectedTab {
24 case 0: DaysOfWeekView()
25 case 1: MonthsOfYearView()
26 default: SeasonsView()
27 }
28 }
29 .frame(maxWidth: .infinity, maxHeight: .infinity)
30
31 Divider()
32
33 // Bottom tab bar with large tap targets
34 HStack(spacing: 0) {
35 ForEach(Array(tabs.enumerated()), id: \.offset) { index, tab in
36 Button {
37 selectedTab = index
38 } label: {
39 VStack(spacing: 6) {
40 Image(systemName: tab.symbol)
41 .font(.system(size: 32))
42 Text(tab.label)
43 .font(.system(size: 18, weight: .semibold, design: .rounded))
44 }
45 .foregroundStyle(selectedTab == index ? Color.accentColor : Color.secondary)
46 .frame(maxWidth: .infinity)
47 .padding(.vertical, 14)
48 .contentShape(Rectangle())
49 }
50 .buttonStyle(.plain)
51 .accessibilityLabel(tab.label)
52 }
53 }
54 .padding(.horizontal, 24)
55 .padding(.bottom, 8)
56 .background(.bar)
57 }
58 .overlay {
59 if !hasSeenVoiceTip {
60 VoiceTipOverlay {
61 withAnimation { hasSeenVoiceTip = true }
62 }
63 }
64 }
65 }
66}
67
68/// A one-time informational overlay for parents explaining how to download
69/// a higher-quality voice for the best experience. Dismissed with a single tap.
70struct VoiceTipOverlay: View {
71 let onDismiss: () -> Void
72
73 var body: some View {
74 ZStack {
75 Color.black.opacity(0.4)
76 .ignoresSafeArea()
77
78 VStack(spacing: 20) {
79 Image(systemName: "speaker.wave.3.fill")
80 .font(.system(size: 48))
81 .foregroundStyle(.blue)
82
83 Text("Better Voices Available")
84 .font(.system(size: 28, weight: .bold, design: .rounded))
85
86 Text("For the best experience, download an enhanced voice on your iPad.\n\nSettings → Accessibility → Spoken Content → Voices → English → tap a voice marked Enhanced or Premium to download it.")
87 .font(.system(size: 18, design: .rounded))
88 .multilineTextAlignment(.center)
89 .foregroundStyle(.secondary)
90
91 Text("Tap anywhere to dismiss")
92 .font(.system(size: 16, weight: .medium, design: .rounded))
93 .foregroundStyle(.tertiary)
94 .padding(.top, 8)
95 }
96 .padding(40)
97 .frame(maxWidth: 520)
98 .background(
99 RoundedRectangle(cornerRadius: 32, style: .continuous)
100 .fill(.regularMaterial)
101 )
102 }
103 .contentShape(Rectangle())
104 .onTapGesture(perform: onDismiss)
105 }
106}
107
108#Preview {
109 ContentView()
110}
DayByDay/DayByDayApp.swift added +17
@@ -0,0 +1,17 @@
1//
2// DayByDayApp.swift
3// DayByDay
4//
5// Created by cmc on 2026-03-09.
6//
7
8import SwiftUI
9
10@main
11struct DayByDayApp: App {
12 var body: some Scene {
13 WindowGroup {
14 ContentView()
15 }
16 }
17}
DayByDay/DayCard.swift added +117
@@ -0,0 +1,117 @@
1//
2// DayCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one day of the week.
9/// Tapping the card plays a bounce animation and speaks the day name aloud.
10struct DayCard: View {
11 let day: DayOfWeek
12 var isHighlighted = false
13 @State private var isTapped = false
14
15 var body: some View {
16 ZStack {
17 RoundedRectangle(cornerRadius: 32, style: .continuous)
18 .fill(day.color.gradient)
19 .shadow(color: day.color.opacity(0.4), radius: 8, y: 4)
20
21 VStack(spacing: 16) {
22 Image(systemName: day.symbol)
23 .font(.system(size: 64))
24 .foregroundStyle(.white)
25 .symbolRenderingMode(.hierarchical)
26
27 Text(day.name)
28 .font(.system(size: 32, weight: .bold, design: .rounded))
29 .foregroundStyle(.white)
30 }
31 }
32 .overlay {
33 if isHighlighted {
34 RoundedRectangle(cornerRadius: 32, style: .continuous)
35 .strokeBorder(.white, lineWidth: 5)
36 }
37 }
38 .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
39 .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
40 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
41 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
42 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
43 .onTapGesture {
44 isTapped = true
45 SpeechSynthesizer.shared.speak(day.name)
46 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
47 isTapped = false
48 }
49 }
50 .accessibilityLabel(day.name)
51 .accessibilityAddTraits(.isButton)
52 }
53}
54
55// MARK: - Day model
56
57enum DayOfWeek: Int, CaseIterable, Identifiable {
58 case sunday, monday, tuesday, wednesday, thursday, friday, saturday
59
60 var id: Int { rawValue }
61
62 /// The day of the week for today's date.
63 static var current: DayOfWeek {
64 // Calendar weekday: 1 = Sunday, 7 = Saturday
65 let weekday = Calendar.current.component(.weekday, from: Date())
66 return DayOfWeek(rawValue: weekday - 1)!
67 }
68
69 var name: String {
70 switch self {
71 case .sunday: "Sunday"
72 case .monday: "Monday"
73 case .tuesday: "Tuesday"
74 case .wednesday: "Wednesday"
75 case .thursday: "Thursday"
76 case .friday: "Friday"
77 case .saturday: "Saturday"
78 }
79 }
80
81 /// A recognisable SF Symbol icon for each day so children who cannot read
82 /// still get a unique visual cue.
83 var symbol: String {
84 switch self {
85 case .sunday: "sun.max.fill"
86 case .monday: "moon.fill"
87 case .tuesday: "star.fill"
88 case .wednesday: "cloud.fill"
89 case .thursday: "bolt.fill"
90 case .friday: "heart.fill"
91 case .saturday: "sparkles"
92 }
93 }
94
95 /// Bright, child-friendly colour for each card.
96 var color: Color {
97 switch self {
98 case .sunday: Color(red: 1.0, green: 0.6, blue: 0.2) // orange
99 case .monday: Color(red: 0.35, green: 0.5, blue: 0.9) // blue
100 case .tuesday: Color(red: 0.9, green: 0.3, blue: 0.4) // red-pink
101 case .wednesday: Color(red: 0.3, green: 0.75, blue: 0.45) // green
102 case .thursday: Color(red: 0.55, green: 0.4, blue: 0.9) // purple
103 case .friday: Color(red: 1.0, green: 0.8, blue: 0.2) // yellow
104 case .saturday: Color(red: 0.2, green: 0.8, blue: 0.8) // teal
105 }
106 }
107}
108
109#Preview {
110 HStack(spacing: 24) {
111 DayCard(day: .wednesday, isHighlighted: true)
112 .frame(width: 200, height: 260)
113 DayCard(day: .thursday)
114 .frame(width: 200, height: 260)
115 }
116 .padding()
117}
DayByDay/DaysOfWeekView.swift added +30
@@ -0,0 +1,30 @@
1//
2// DaysOfWeekView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all seven days of the week as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct DaysOfWeekView: View {
11 private let columns = [
12 GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
13 ]
14
15 var body: some View {
16 ScrollView {
17 LazyVGrid(columns: columns, spacing: 24) {
18 ForEach(DayOfWeek.allCases) { day in
19 DayCard(day: day, isHighlighted: day == .current)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 DaysOfWeekView()
30}
DayByDay/MonthCard.swift added +130
@@ -0,0 +1,130 @@
1//
2// MonthCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one month of the year.
9/// Tapping the card plays a bounce animation and speaks the month name aloud.
10struct MonthCard: View {
11 let month: MonthOfYear
12 var isHighlighted = false
13 @State private var isTapped = false
14
15 var body: some View {
16 ZStack {
17 RoundedRectangle(cornerRadius: 32, style: .continuous)
18 .fill(month.color.gradient)
19 .shadow(color: month.color.opacity(0.4), radius: 8, y: 4)
20
21 VStack(spacing: 16) {
22 Image(systemName: month.symbol)
23 .font(.system(size: 64))
24 .foregroundStyle(.white)
25 .symbolRenderingMode(.hierarchical)
26
27 Text(month.name)
28 .font(.system(size: 32, weight: .bold, design: .rounded))
29 .foregroundStyle(.white)
30 }
31 }
32 .overlay {
33 if isHighlighted {
34 RoundedRectangle(cornerRadius: 32, style: .continuous)
35 .strokeBorder(.white, lineWidth: 5)
36 }
37 }
38 .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
39 .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
40 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
41 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
42 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
43 .onTapGesture {
44 isTapped = true
45 SpeechSynthesizer.shared.speak(month.name)
46 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
47 isTapped = false
48 }
49 }
50 .accessibilityLabel(month.name)
51 .accessibilityAddTraits(.isButton)
52 }
53}
54
55// MARK: - Month model
56
57enum MonthOfYear: Int, CaseIterable, Identifiable {
58 case january, february, march, april, may, june,
59 july, august, september, october, november, december
60
61 var id: Int { rawValue }
62
63 /// The month for today's date.
64 static var current: MonthOfYear {
65 // Calendar month: 1 = January, 12 = December
66 let month = Calendar.current.component(.month, from: Date())
67 return MonthOfYear(rawValue: month - 1)!
68 }
69
70 var name: String {
71 switch self {
72 case .january: "January"
73 case .february: "February"
74 case .march: "March"
75 case .april: "April"
76 case .may: "May"
77 case .june: "June"
78 case .july: "July"
79 case .august: "August"
80 case .september: "September"
81 case .october: "October"
82 case .november: "November"
83 case .december: "December"
84 }
85 }
86
87 var symbol: String {
88 switch self {
89 case .january: "snowflake"
90 case .february: "heart.fill"
91 case .march: "wind"
92 case .april: "cloud.rain.fill"
93 case .may: "leaf.fill"
94 case .june: "sun.max.fill"
95 case .july: "fireworks"
96 case .august: "beach.umbrella.fill"
97 case .september: "book.fill"
98 case .october: "moon.stars.fill"
99 case .november: "fork.knife"
100 case .december: "gift.fill"
101 }
102 }
103
104 var color: Color {
105 switch self {
106 case .january: Color(red: 0.55, green: 0.75, blue: 0.95) // ice blue
107 case .february: Color(red: 0.9, green: 0.35, blue: 0.5) // pink
108 case .march: Color(red: 0.45, green: 0.8, blue: 0.55) // spring green
109 case .april: Color(red: 0.5, green: 0.65, blue: 0.9) // rain blue
110 case .may: Color(red: 0.95, green: 0.7, blue: 0.2) // golden
111 case .june: Color(red: 1.0, green: 0.55, blue: 0.25) // warm orange
112 case .july: Color(red: 0.85, green: 0.25, blue: 0.3) // red
113 case .august: Color(red: 0.2, green: 0.75, blue: 0.8) // ocean teal
114 case .september: Color(red: 0.6, green: 0.45, blue: 0.85) // purple
115 case .october: Color(red: 0.9, green: 0.5, blue: 0.15) // pumpkin
116 case .november: Color(red: 0.7, green: 0.5, blue: 0.3) // brown
117 case .december: Color(red: 0.3, green: 0.5, blue: 0.85) // winter blue
118 }
119 }
120}
121
122#Preview {
123 HStack(spacing: 24) {
124 MonthCard(month: .march, isHighlighted: true)
125 .frame(width: 200, height: 260)
126 MonthCard(month: .april)
127 .frame(width: 200, height: 260)
128 }
129 .padding()
130}
DayByDay/MonthsOfYearView.swift added +30
@@ -0,0 +1,30 @@
1//
2// MonthsOfYearView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all twelve months as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct MonthsOfYearView: View {
11 private let columns = [
12 GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
13 ]
14
15 var body: some View {
16 ScrollView {
17 LazyVGrid(columns: columns, spacing: 24) {
18 ForEach(MonthOfYear.allCases) { month in
19 MonthCard(month: month, isHighlighted: month == .current)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 MonthsOfYearView()
30}
DayByDay/SeasonCard.swift added +111
@@ -0,0 +1,111 @@
1//
2// SeasonCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one season.
9/// Tapping the card plays a bounce animation and speaks the season name aloud.
10struct SeasonCard: View {
11 let season: Season
12 var isHighlighted = false
13 @State private var isTapped = false
14
15 var body: some View {
16 ZStack {
17 RoundedRectangle(cornerRadius: 32, style: .continuous)
18 .fill(season.color.gradient)
19 .shadow(color: season.color.opacity(0.4), radius: 8, y: 4)
20
21 VStack(spacing: 16) {
22 Image(systemName: season.symbol)
23 .font(.system(size: 64))
24 .foregroundStyle(.white)
25 .symbolRenderingMode(.hierarchical)
26
27 Text(season.name)
28 .font(.system(size: 32, weight: .bold, design: .rounded))
29 .foregroundStyle(.white)
30 }
31 }
32 .overlay {
33 if isHighlighted {
34 RoundedRectangle(cornerRadius: 32, style: .continuous)
35 .strokeBorder(.white, lineWidth: 5)
36 }
37 }
38 .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
39 .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
40 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
41 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
42 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
43 .onTapGesture {
44 isTapped = true
45 SpeechSynthesizer.shared.speak(season.name)
46 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
47 isTapped = false
48 }
49 }
50 .accessibilityLabel(season.name)
51 .accessibilityAddTraits(.isButton)
52 }
53}
54
55// MARK: - Season model
56
57enum Season: Int, CaseIterable, Identifiable {
58 case spring, summer, fall, winter
59
60 var id: Int { rawValue }
61
62 /// The season for today's date.
63 /// Spring: March–May, Summer: June–August,
64 /// Fall: September–November, Winter: December–February.
65 static var current: Season {
66 let month = Calendar.current.component(.month, from: Date())
67 switch month {
68 case 3...5: return .spring
69 case 6...8: return .summer
70 case 9...11: return .fall
71 default: return .winter
72 }
73 }
74
75 var name: String {
76 switch self {
77 case .spring: "Spring"
78 case .summer: "Summer"
79 case .fall: "Fall"
80 case .winter: "Winter"
81 }
82 }
83
84 var symbol: String {
85 switch self {
86 case .spring: "leaf.fill"
87 case .summer: "sun.max.fill"
88 case .fall: "wind"
89 case .winter: "snowflake"
90 }
91 }
92
93 var color: Color {
94 switch self {
95 case .spring: Color(red: 0.45, green: 0.8, blue: 0.5) // fresh green
96 case .summer: Color(red: 1.0, green: 0.6, blue: 0.15) // sunny orange
97 case .fall: Color(red: 0.85, green: 0.45, blue: 0.2) // autumn orange
98 case .winter: Color(red: 0.4, green: 0.65, blue: 0.95) // cool blue
99 }
100 }
101}
102
103#Preview {
104 HStack(spacing: 24) {
105 SeasonCard(season: .spring, isHighlighted: true)
106 .frame(width: 200, height: 260)
107 SeasonCard(season: .summer)
108 .frame(width: 200, height: 260)
109 }
110 .padding()
111}
DayByDay/SeasonsView.swift added +30
@@ -0,0 +1,30 @@
1//
2// SeasonsView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all four seasons as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct SeasonsView: View {
11 private let columns = [
12 GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
13 ]
14
15 var body: some View {
16 ScrollView {
17 LazyVGrid(columns: columns, spacing: 24) {
18 ForEach(Season.allCases) { season in
19 SeasonCard(season: season, isHighlighted: season == .current)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 SeasonsView()
30}
DayByDay/SpeechSynthesizer.swift added +52
@@ -0,0 +1,52 @@
1//
2// SpeechSynthesizer.swift
3// DayByDay
4//
5
6import AVFoundation
7
8/// Wrapper around AVSpeechSynthesizer that selects the highest-quality
9/// en-US neural voice available on the device. Falls back gracefully if
10/// premium/enhanced voices haven't been downloaded yet.
11final class SpeechSynthesizer {
12 static let shared = SpeechSynthesizer()
13
14 private let synthesizer = AVSpeechSynthesizer()
15 private let voice: AVSpeechSynthesisVoice?
16
17 private init() {
18 self.voice = Self.bestAvailableVoice()
19 }
20
21 func speak(_ text: String) {
22 if synthesizer.isSpeaking {
23 synthesizer.stopSpeaking(at: .immediate)
24 }
25
26 let utterance = AVSpeechUtterance(string: text)
27 utterance.voice = voice
28 utterance.rate = 0.4
29 utterance.pitchMultiplier = 1.0
30 utterance.preUtteranceDelay = 0
31 synthesizer.speak(utterance)
32 }
33
34 /// Picks the best en-US voice on the device.
35 /// Preference order: premium > enhanced > default quality.
36 /// Skips novelty voices so the child always hears a natural-sounding voice.
37 private static func bestAvailableVoice() -> AVSpeechSynthesisVoice? {
38 let candidates = AVSpeechSynthesisVoice.speechVoices().filter { voice in
39 voice.language.hasPrefix("en-US")
40 && !voice.voiceTraits.contains(.isNoveltyVoice)
41 }
42
43 if let premium = candidates.first(where: { $0.quality == .premium }) {
44 return premium
45 }
46 if let enhanced = candidates.first(where: { $0.quality == .enhanced }) {
47 return enhanced
48 }
49 // Fall back to the best default-quality voice.
50 return candidates.first ?? AVSpeechSynthesisVoice(language: "en-US")
51 }
52}
claude.md added +29
@@ -0,0 +1,29 @@
1# DayByDay – Project Context
2
3## What this is
4An iPad app for pre-K children (ages 4–5) that teaches days of the week, months of the year, and seasons through simple, tap-based interactions.
5
6## Target user
7A single child, approximately 4–5 years old, preparing to start school. Cannot read yet. Parent may sit alongside but the child should be able to use the app independently.
8
9## Design principles
10- Large tap targets — nothing small or fiddly
11- Bright, clean colors — simple and friendly, not cluttered
12- No text-dependent interactions — the child cannot read
13- Audio is the primary feedback mechanism (day/month/season name read aloud on tap)
14- Simple animations on interaction — enough to delight, not enough to distract
15- No timers, no failure states, no scores — purely exploratory
16
17## Content
18- Days of the week: Sunday through Saturday
19- Months of the year: January through December
20- Seasons: Spring, Summer, Fall, Winter
21
22## Technical constraints
23- SwiftUI, iPad only
24- Fully offline — no network requests, no accounts, no analytics
25- No in-app purchases
26- Targets latest iPadOS
27
28## What good looks like
29Each concept (day, month, season) should feel like its own tactile card or object the child can tap and explore. Transitions should be smooth. Audio should play immediately on tap with no perceptible delay.