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 51f1b74a4d

51f1b74a4d50c4eb5e9156af07a8b0ce30468867

parent: 688a72b27e

Unsigned

cmc <hello@cleberg.net> · 2026-03-14 14:56 UTC

implement more learning categories

Layout: unified · split

AlphabetView.swift added +30
@@ -0,0 +1,30 @@
1//
2// AlphabetView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all 26 letters A–Z as large, tappable cards in a scrollable grid.
9/// Uses a smaller minimum size to fit more letters across the screen.
10struct AlphabetView: View {
11 private let columns = [
12 GridItem(.adaptive(minimum: 140, maximum: 300), spacing: 20)
13 ]
14
15 var body: some View {
16 ScrollView {
17 LazyVGrid(columns: columns, spacing: 20) {
18 ForEach(LearnLetter.allCases) { letter in
19 LetterCard(letter: letter)
20 .frame(height: 160)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 AlphabetView()
30}
AnimalCard.swift added +103
@@ -0,0 +1,103 @@
1//
2// AnimalCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one animal.
9/// Tapping the card plays a bounce animation and speaks the animal name aloud.
10struct AnimalCard: View {
11 let animal: LearnAnimal
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(animal.color.gradient)
18 .shadow(color: animal.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 Image(systemName: animal.symbol)
22 .font(.system(size: 64))
23 .foregroundStyle(.white)
24 .symbolRenderingMode(.hierarchical)
25
26 Text(animal.name)
27 .font(.system(size: 32, weight: .bold, design: .rounded))
28 .foregroundStyle(.white)
29 }
30 }
31 .scaleEffect(isTapped ? 1.12 : 1.0)
32 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
33 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
34 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
35 .onTapGesture {
36 isTapped = true
37 SpeechSynthesizer.shared.speak(animal.name)
38 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
39 isTapped = false
40 }
41 }
42 .accessibilityLabel(animal.name)
43 .accessibilityAddTraits(.isButton)
44 }
45}
46
47// MARK: - Animal model
48
49enum LearnAnimal: Int, CaseIterable, Identifiable {
50 case cat, dog, bird, fish, rabbit, turtle, ladybug, ant, lizard
51
52 var id: Int { rawValue }
53
54 var name: String {
55 switch self {
56 case .cat: "Cat"
57 case .dog: "Dog"
58 case .bird: "Bird"
59 case .fish: "Fish"
60 case .rabbit: "Rabbit"
61 case .turtle: "Turtle"
62 case .ladybug: "Ladybug"
63 case .ant: "Ant"
64 case .lizard: "Lizard"
65 }
66 }
67
68 var symbol: String {
69 switch self {
70 case .cat: "cat.fill"
71 case .dog: "dog.fill"
72 case .bird: "bird.fill"
73 case .fish: "fish.fill"
74 case .rabbit: "hare.fill"
75 case .turtle: "tortoise.fill"
76 case .ladybug: "ladybug.fill"
77 case .ant: "ant.fill"
78 case .lizard: "lizard.fill"
79 }
80 }
81
82 var color: Color {
83 switch self {
84 case .cat: Color(red: 0.9, green: 0.55, blue: 0.2) // orange
85 case .dog: Color(red: 0.6, green: 0.45, blue: 0.3) // brown
86 case .bird: Color(red: 0.3, green: 0.65, blue: 0.9) // sky blue
87 case .fish: Color(red: 0.2, green: 0.75, blue: 0.8) // teal
88 case .rabbit: Color(red: 0.75, green: 0.6, blue: 0.8) // lavender
89 case .turtle: Color(red: 0.35, green: 0.7, blue: 0.4) // green
90 case .ladybug: Color(red: 0.9, green: 0.25, blue: 0.25) // red
91 case .ant: Color(red: 0.3, green: 0.3, blue: 0.3) // dark gray
92 case .lizard: Color(red: 0.45, green: 0.75, blue: 0.35) // lime
93 }
94 }
95}
96
97#Preview {
98 HStack(spacing: 24) {
99 AnimalCard(animal: .cat)
100 .frame(width: 200, height: 260)
101 }
102 .padding()
103}
AnimalsView.swift added +30
@@ -0,0 +1,30 @@
1//
2// AnimalsView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays 12 animals as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct AnimalsView: 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(LearnAnimal.allCases) { animal in
19 AnimalCard(animal: animal)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 AnimalsView()
30}
BodyPartCard.swift added +114
@@ -0,0 +1,114 @@
1//
2// BodyPartCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one body part.
9/// Tapping the card plays a bounce animation and speaks the body part name aloud.
10struct BodyPartCard: View {
11 let bodyPart: LearnBodyPart
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(bodyPart.color.gradient)
18 .shadow(color: bodyPart.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 Image(systemName: bodyPart.symbol)
22 .font(.system(size: 64))
23 .foregroundStyle(.white)
24 .symbolRenderingMode(.hierarchical)
25
26 Text(bodyPart.name)
27 .font(.system(size: 32, weight: .bold, design: .rounded))
28 .foregroundStyle(.white)
29 }
30 }
31 .scaleEffect(isTapped ? 1.12 : 1.0)
32 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
33 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
34 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
35 .onTapGesture {
36 isTapped = true
37 SpeechSynthesizer.shared.speak(bodyPart.name)
38 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
39 isTapped = false
40 }
41 }
42 .accessibilityLabel(bodyPart.name)
43 .accessibilityAddTraits(.isButton)
44 }
45}
46
47// MARK: - Body part model
48
49enum LearnBodyPart: Int, CaseIterable, Identifiable {
50 case head, eyes, ears, nose, mouth, hands, fingers, arms, legs, feet, heart, belly
51
52 var id: Int { rawValue }
53
54 var name: String {
55 switch self {
56 case .head: "Head"
57 case .eyes: "Eyes"
58 case .ears: "Ears"
59 case .nose: "Nose"
60 case .mouth: "Mouth"
61 case .hands: "Hands"
62 case .fingers: "Fingers"
63 case .arms: "Arms"
64 case .legs: "Legs"
65 case .feet: "Feet"
66 case .heart: "Heart"
67 case .belly: "Belly"
68 }
69 }
70
71 var symbol: String {
72 switch self {
73 case .head: "brain.head.profile"
74 case .eyes: "eye.fill"
75 case .ears: "ear.fill"
76 case .nose: "nose.fill"
77 case .mouth: "mouth.fill"
78 case .hands: "hand.raised.fill"
79 case .fingers: "hand.point.up.fill"
80 case .arms: "figure.arms.open"
81 case .legs: "figure.walk"
82 case .feet: "shoeprints.fill"
83 case .heart: "heart.fill"
84 case .belly: "circle.fill"
85 }
86 }
87
88 var color: Color {
89 switch self {
90 case .head: Color(red: 0.9, green: 0.5, blue: 0.3) // warm orange
91 case .eyes: Color(red: 0.3, green: 0.65, blue: 0.9) // blue
92 case .ears: Color(red: 0.6, green: 0.45, blue: 0.85) // purple
93 case .nose: Color(red: 0.9, green: 0.4, blue: 0.5) // rose
94 case .mouth: Color(red: 0.9, green: 0.3, blue: 0.35) // red
95 case .hands: Color(red: 0.95, green: 0.7, blue: 0.3) // golden
96 case .fingers: Color(red: 0.35, green: 0.75, blue: 0.5) // green
97 case .arms: Color(red: 0.25, green: 0.7, blue: 0.7) // teal
98 case .legs: Color(red: 0.5, green: 0.4, blue: 0.8) // indigo
99 case .feet: Color(red: 0.7, green: 0.5, blue: 0.3) // brown
100 case .heart: Color(red: 0.85, green: 0.3, blue: 0.45) // deep pink
101 case .belly: Color(red: 1.0, green: 0.6, blue: 0.4) // peach
102 }
103 }
104}
105
106#Preview {
107 HStack(spacing: 24) {
108 BodyPartCard(bodyPart: .head)
109 .frame(width: 200, height: 260)
110 BodyPartCard(bodyPart: .hands)
111 .frame(width: 200, height: 260)
112 }
113 .padding()
114}
BodyPartsView.swift added +29
@@ -0,0 +1,29 @@
1//
2// BodyPartsView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all 12 body parts as large, tappable cards in a scrollable grid.
9struct BodyPartsView: View {
10 private let columns = [
11 GridItem(.adaptive(minimum: 280), spacing: 24)
12 ]
13
14 var body: some View {
15 ScrollView {
16 LazyVGrid(columns: columns, spacing: 24) {
17 ForEach(LearnBodyPart.allCases) { part in
18 BodyPartCard(bodyPart: part)
19 .frame(height: 200)
20 }
21 }
22 .padding(32)
23 }
24 }
25}
26
27#Preview {
28 BodyPartsView()
29}
ColorCard.swift added +86
@@ -0,0 +1,86 @@
1//
2// ColorCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large card filled with a color. Tapping speaks the color name aloud.
9struct ColorCard: View {
10 let learnColor: LearnColor
11 @State private var isTapped = false
12
13 var body: some View {
14 ZStack {
15 RoundedRectangle(cornerRadius: 32, style: .continuous)
16 .fill(learnColor.color.gradient)
17 .shadow(color: learnColor.color.opacity(0.4), radius: 8, y: 4)
18
19 Text(learnColor.name)
20 .font(.system(size: 36, weight: .bold, design: .rounded))
21 .foregroundStyle(.white)
22 .shadow(color: .black.opacity(0.3), radius: 4, y: 2)
23 }
24 .scaleEffect(isTapped ? 1.12 : 1.0)
25 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
26 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
27 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
28 .onTapGesture {
29 isTapped = true
30 SpeechSynthesizer.shared.speak(learnColor.name)
31 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
32 isTapped = false
33 }
34 }
35 .accessibilityLabel(learnColor.name)
36 .accessibilityAddTraits(.isButton)
37 }
38}
39
40// MARK: - Color model
41
42enum LearnColor: Int, CaseIterable, Identifiable {
43 case red, orange, yellow, green, blue, purple, pink, brown, black, white
44
45 var id: Int { rawValue }
46
47 var name: String {
48 switch self {
49 case .red: "Red"
50 case .orange: "Orange"
51 case .yellow: "Yellow"
52 case .green: "Green"
53 case .blue: "Blue"
54 case .purple: "Purple"
55 case .pink: "Pink"
56 case .brown: "Brown"
57 case .black: "Black"
58 case .white: "White"
59 }
60 }
61
62 var color: Color {
63 switch self {
64 case .red: Color(red: 0.9, green: 0.2, blue: 0.2)
65 case .orange: Color(red: 1.0, green: 0.55, blue: 0.15)
66 case .yellow: Color(red: 1.0, green: 0.82, blue: 0.15)
67 case .green: Color(red: 0.2, green: 0.75, blue: 0.35)
68 case .blue: Color(red: 0.2, green: 0.45, blue: 0.9)
69 case .purple: Color(red: 0.55, green: 0.3, blue: 0.85)
70 case .pink: Color(red: 0.95, green: 0.4, blue: 0.6)
71 case .brown: Color(red: 0.55, green: 0.35, blue: 0.2)
72 case .black: Color(red: 0.15, green: 0.15, blue: 0.15)
73 case .white: Color(red: 0.92, green: 0.92, blue: 0.92)
74 }
75 }
76}
77
78#Preview {
79 HStack(spacing: 24) {
80 ColorCard(learnColor: .red)
81 .frame(width: 200, height: 200)
82 ColorCard(learnColor: .blue)
83 .frame(width: 200, height: 200)
84 }
85 .padding()
86}
ColorsView.swift added +30
@@ -0,0 +1,30 @@
1//
2// ColorsView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays 10 colors as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct ColorsView: 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(LearnColor.allCases) { color in
19 ColorCard(learnColor: color)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 ColorsView()
30}
DayByDay.xcodeproj/project.pbxproj +87 −6
@@ -7,17 +7,64 @@
77 objects = {
88
99/* Begin PBXBuildFile section */
10 8B1B4FF92F659AD1005C246F /* WeatherCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */; };
11 8B1B4FFB2F659AD1005C246F /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */; };
12 8B1B4FFD2F659AD2005C246F /* BodyPartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */; };
13 8B1B4FFF2F659AD2005C246F /* BodyPartsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */; };
14 8B1B50012F659AD3005C246F /* FoodCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B50002F659AD3005C246F /* FoodCard.swift */; };
15 8B1B50032F659AD4005C246F /* FoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B50022F659AD4005C246F /* FoodView.swift */; };
16 8B57F7282F63BBB4001F6F5E /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7272F63BBB4001F6F5E /* TodayView.swift */; };
17 8B57F72A2F63BBB5001F6F5E /* NumberCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */; };
18 8B57F72C2F63BBB6001F6F5E /* NumbersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */; };
19 8B57F72E2F63BBB7001F6F5E /* ColorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */; };
20 8B57F7302F63BBB8001F6F5E /* ColorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */; };
21 8B57F7322F63BBB9001F6F5E /* ShapeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */; };
22 8B57F7342F63BBBA001F6F5E /* ShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */; };
23 8B57F7362F63BBBA001F6F5E /* AnimalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */; };
24 8B57F7382F63BBBB001F6F5E /* AnimalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */; };
25 8B57F73A2F63BBBD001F6F5E /* LetterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */; };
26 8B57F73C2F63BBBD001F6F5E /* AlphabetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */; };
1027 8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; };
1128/* End PBXBuildFile section */
1229
1330/* Begin PBXFileReference section */
31 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherCard.swift; sourceTree = "<group>"; };
32 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = "<group>"; };
33 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyPartCard.swift; sourceTree = "<group>"; };
34 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyPartsView.swift; sourceTree = "<group>"; };
35 8B1B50002F659AD3005C246F /* FoodCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoodCard.swift; sourceTree = "<group>"; };
36 8B1B50022F659AD4005C246F /* FoodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoodView.swift; sourceTree = "<group>"; };
37 8B57F7272F63BBB4001F6F5E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
38 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberCard.swift; sourceTree = "<group>"; };
39 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumbersView.swift; sourceTree = "<group>"; };
40 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorCard.swift; sourceTree = "<group>"; };
41 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsView.swift; sourceTree = "<group>"; };
42 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeCard.swift; sourceTree = "<group>"; };
43 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesView.swift; sourceTree = "<group>"; };
44 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalCard.swift; sourceTree = "<group>"; };
45 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalsView.swift; sourceTree = "<group>"; };
46 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterCard.swift; sourceTree = "<group>"; };
47 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetView.swift; sourceTree = "<group>"; };
1448 8B7051B22F5F908800491D58 /* DayByDay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DayByDay.app; sourceTree = BUILT_PRODUCTS_DIR; };
1549 8B7051C02F5F90C800491D58 /* claude.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = claude.md; sourceTree = "<group>"; };
1650/* End PBXFileReference section */
1751
52/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
53 8B5214E62F621E0200E1D35A /* Exceptions for "DayByDay" folder in "DayByDay" target */ = {
54 isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
55 membershipExceptions = (
56 Info.plist,
57 );
58 target = 8B7051B12F5F908800491D58 /* DayByDay */;
59 };
60/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
61
1862/* Begin PBXFileSystemSynchronizedRootGroup section */
1963 8B7051B42F5F908800491D58 /* DayByDay */ = {
2064 isa = PBXFileSystemSynchronizedRootGroup;
65 exceptions = (
66 8B5214E62F621E0200E1D35A /* Exceptions for "DayByDay" folder in "DayByDay" target */,
67 );
2168 path = DayByDay;
2269 sourceTree = "<group>";
2370 };
@@ -40,6 +87,23 @@
4087 8B7051C02F5F90C800491D58 /* claude.md */,
4188 8B7051B42F5F908800491D58 /* DayByDay */,
4289 8B7051B32F5F908800491D58 /* Products */,
90 8B57F7272F63BBB4001F6F5E /* TodayView.swift */,
91 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */,
92 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */,
93 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */,
94 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */,
95 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */,
96 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */,
97 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */,
98 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */,
99 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */,
100 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */,
101 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */,
102 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */,
103 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */,
104 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */,
105 8B1B50002F659AD3005C246F /* FoodCard.swift */,
106 8B1B50022F659AD4005C246F /* FoodView.swift */,
43107 );
44108 sourceTree = "<group>";
45109 };
@@ -126,6 +190,23 @@
126190 isa = PBXSourcesBuildPhase;
127191 buildActionMask = 2147483647;
128192 files = (
193 8B1B50032F659AD4005C246F /* FoodView.swift in Sources */,
194 8B57F72E2F63BBB7001F6F5E /* ColorCard.swift in Sources */,
195 8B57F73C2F63BBBD001F6F5E /* AlphabetView.swift in Sources */,
196 8B57F73A2F63BBBD001F6F5E /* LetterCard.swift in Sources */,
197 8B57F7382F63BBBB001F6F5E /* AnimalsView.swift in Sources */,
198 8B57F72C2F63BBB6001F6F5E /* NumbersView.swift in Sources */,
199 8B57F72A2F63BBB5001F6F5E /* NumberCard.swift in Sources */,
200 8B1B50012F659AD3005C246F /* FoodCard.swift in Sources */,
201 8B1B4FF92F659AD1005C246F /* WeatherCard.swift in Sources */,
202 8B1B4FFB2F659AD1005C246F /* WeatherView.swift in Sources */,
203 8B1B4FFD2F659AD2005C246F /* BodyPartCard.swift in Sources */,
204 8B1B4FFF2F659AD2005C246F /* BodyPartsView.swift in Sources */,
205 8B57F7342F63BBBA001F6F5E /* ShapesView.swift in Sources */,
206 8B57F7362F63BBBA001F6F5E /* AnimalCard.swift in Sources */,
207 8B57F7322F63BBB9001F6F5E /* ShapeCard.swift in Sources */,
208 8B57F7282F63BBB4001F6F5E /* TodayView.swift in Sources */,
209 8B57F7302F63BBB8001F6F5E /* ColorsView.swift in Sources */,
129210 );
130211 runOnlyForDeploymentPostprocessing = 0;
131212 };
@@ -259,21 +340,21 @@
259340 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
260341 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
261342 CODE_SIGN_STYLE = Automatic;
262 CURRENT_PROJECT_VERSION = 1;
343 CURRENT_PROJECT_VERSION = 3;
263344 DEVELOPMENT_TEAM = ZCNAX3VL9D;
264345 ENABLE_PREVIEWS = YES;
265346 GENERATE_INFOPLIST_FILE = YES;
347 INFOPLIST_FILE = DayByDay/Info.plist;
266348 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
267349 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
268350 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
269351 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
270 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
271352 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
272353 LD_RUNPATH_SEARCH_PATHS = (
273354 "$(inherited)",
274355 "@executable_path/Frameworks",
275356 );
276 MARKETING_VERSION = 1.0;
357 MARKETING_VERSION = 1.2;
277358 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
278359 PRODUCT_NAME = "$(TARGET_NAME)";
279360 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -292,21 +373,21 @@
292373 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293374 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
294375 CODE_SIGN_STYLE = Automatic;
295 CURRENT_PROJECT_VERSION = 1;
376 CURRENT_PROJECT_VERSION = 3;
296377 DEVELOPMENT_TEAM = ZCNAX3VL9D;
297378 ENABLE_PREVIEWS = YES;
298379 GENERATE_INFOPLIST_FILE = YES;
380 INFOPLIST_FILE = DayByDay/Info.plist;
299381 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
300382 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
301383 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
302384 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
303 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
304385 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
305386 LD_RUNPATH_SEARCH_PATHS = (
306387 "$(inherited)",
307388 "@executable_path/Frameworks",
308389 );
309 MARKETING_VERSION = 1.0;
390 MARKETING_VERSION = 1.2;
310391 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
311392 PRODUCT_NAME = "$(TARGET_NAME)";
312393 STRING_CATALOG_GENERATE_SYMBOLS = YES;
DayByDay/Assets.xcassets/LaunchBackground.colorset/Contents.json added +38
@@ -0,0 +1,38 @@
1{
2 "colors" : [
3 {
4 "color" : {
5 "color-space" : "srgb",
6 "components" : {
7 "alpha" : "1.000",
8 "blue" : "1.000",
9 "green" : "1.000",
10 "red" : "1.000"
11 }
12 },
13 "idiom" : "universal"
14 },
15 {
16 "appearances" : [
17 {
18 "appearance" : "luminosity",
19 "value" : "dark"
20 }
21 ],
22 "color" : {
23 "color-space" : "srgb",
24 "components" : {
25 "alpha" : "1.000",
26 "blue" : "0.000",
27 "green" : "0.000",
28 "red" : "0.000"
29 }
30 },
31 "idiom" : "universal"
32 }
33 ],
34 "info" : {
35 "author" : "xcode",
36 "version" : 1
37 }
38}
DayByDay/ContentView.swift +160 −42
@@ -7,62 +7,180 @@
77
88import SwiftUI
99
10// MARK: - Category model
11
12enum Category: Int, CaseIterable, Identifiable {
13 case today, days, months, seasons, numbers, colors, shapes, animals, alphabet,
14 weather, bodyParts, food
15
16 var id: Int { rawValue }
17
18 var label: String {
19 switch self {
20 case .today: "Today"
21 case .days: "Days"
22 case .months: "Months"
23 case .seasons: "Seasons"
24 case .numbers: "Numbers"
25 case .colors: "Colors"
26 case .shapes: "Shapes"
27 case .animals: "Animals"
28 case .alphabet: "Alphabet"
29 case .weather: "Weather"
30 case .bodyParts: "Body"
31 case .food: "Food"
32 }
33 }
34
35 var symbol: String {
36 switch self {
37 case .today: "sun.horizon.fill"
38 case .days: "calendar"
39 case .months: "calendar.badge.clock"
40 case .seasons: "leaf.fill"
41 case .numbers: "123.rectangle.fill"
42 case .colors: "paintpalette.fill"
43 case .shapes: "pentagon.fill"
44 case .animals: "pawprint.fill"
45 case .alphabet: "abc"
46 case .weather: "cloud.sun.rain.fill"
47 case .bodyParts: "figure.stand"
48 case .food: "carrot.fill"
49 }
50 }
51
52 var color: Color {
53 switch self {
54 case .today: .orange
55 case .days: .blue
56 case .months: .purple
57 case .seasons: .green
58 case .numbers: .red
59 case .colors: .pink
60 case .shapes: .teal
61 case .animals: Color(red: 0.7, green: 0.45, blue: 0.2)
62 case .alphabet: .indigo
63 case .weather: Color(red: 0.4, green: 0.75, blue: 0.95)
64 case .bodyParts: Color(red: 0.9, green: 0.5, blue: 0.6)
65 case .food: Color(red: 0.95, green: 0.6, blue: 0.2)
66 }
67 }
68
69 /// Whether this tile uses a special gradient background instead of the solid color.
70 var usesGradientBackground: Bool {
71 self == .colors
72 }
73}
74
75// MARK: - Home grid tile
76
77struct CategoryTile: View {
78 let category: Category
79
80 var body: some View {
81 ZStack {
82 if category.usesGradientBackground {
83 RoundedRectangle(cornerRadius: 32, style: .continuous)
84 .fill(
85 LinearGradient(
86 colors: [.red, .orange, .yellow, .green, .blue, .purple],
87 startPoint: .leading,
88 endPoint: .trailing
89 )
90 )
91 .shadow(color: category.color.opacity(0.4), radius: 8, y: 4)
92 } else {
93 RoundedRectangle(cornerRadius: 32, style: .continuous)
94 .fill(category.color.gradient)
95 .shadow(color: category.color.opacity(0.4), radius: 8, y: 4)
96 }
97
98 VStack(spacing: 12) {
99 Image(systemName: category.symbol)
100 .font(.system(size: 48))
101 .foregroundStyle(.white)
102 .symbolRenderingMode(.hierarchical)
103
104 Text(category.label)
105 .font(.system(size: 24, weight: .bold, design: .rounded))
106 .foregroundStyle(.white)
107 }
108 }
109 .accessibilityLabel(category.label)
110 }
111}
112
113// MARK: - Content view
114
10115struct ContentView: View {
11 @State private var selectedTab = 0
12116 @AppStorage("hasSeenVoiceTip") private var hasSeenVoiceTip = false
13117
14 private let tabs: [(label: String, symbol: String)] = [
15 ("Days", "calendar"),
16 ("Months", "calendar.badge.clock"),
17 ("Seasons", "leaf.fill"),
118 private let columns = [
119 GridItem(.flexible(), spacing: 24),
120 GridItem(.flexible(), spacing: 24)
18121 ]
19122
20123 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))
124 NavigationStack {
125 ScrollView {
126 LazyVGrid(columns: columns, spacing: 24) {
127 ForEach(Category.allCases) { category in
128 NavigationLink(value: category) {
129 CategoryTile(category: category)
130 .aspectRatio(1, contentMode: .fit)
44131 }
45 .foregroundStyle(selectedTab == index ? Color.accentColor : Color.secondary)
132 .buttonStyle(.plain)
133 }
134 }
135 .padding(24)
136
137 Button {
138 hasSeenVoiceTip = false
139 } label: {
140 Label("Voice Quality Tips", systemImage: "speaker.wave.2.fill")
141 .font(.system(size: 16, weight: .medium, design: .rounded))
142 .foregroundStyle(.secondary)
46143 .frame(maxWidth: .infinity)
47144 .padding(.vertical, 14)
48 .contentShape(Rectangle())
49 }
50 .buttonStyle(.plain)
51 .accessibilityLabel(tab.label)
145 .background(Color(.systemGray6))
146 .cornerRadius(16)
52147 }
148 .buttonStyle(.plain)
149 .padding(.horizontal, 24)
150 .padding(.bottom, 24)
53151 }
54 .padding(.horizontal, 24)
55 .padding(.bottom, 8)
56 .background(.bar)
57 }
58 .overlay {
59 if !hasSeenVoiceTip {
60 VoiceTipOverlay {
61 withAnimation { hasSeenVoiceTip = true }
152 .navigationTitle("DayByDay")
153 .navigationDestination(for: Category.self) { category in
154 destinationView(for: category)
155 .navigationTitle(category.label)
156 }
157 .overlay {
158 if !hasSeenVoiceTip {
159 VoiceTipOverlay {
160 withAnimation { hasSeenVoiceTip = true }
161 }
62162 }
63163 }
64164 }
65165 }
166
167 @ViewBuilder
168 private func destinationView(for category: Category) -> some View {
169 switch category {
170 case .today: TodayView()
171 case .days: DaysOfWeekView()
172 case .months: MonthsOfYearView()
173 case .seasons: SeasonsView()
174 case .numbers: NumbersView()
175 case .colors: ColorsView()
176 case .shapes: ShapesView()
177 case .animals: AnimalsView()
178 case .alphabet: AlphabetView()
179 case .weather: WeatherView()
180 case .bodyParts: BodyPartsView()
181 case .food: FoodView()
182 }
183 }
66184}
67185
68186/// A one-time informational overlay for parents explaining how to download
@@ -83,7 +201,7 @@ struct VoiceTipOverlay: View {
83201 Text("Better Voices Available")
84202 .font(.system(size: 28, weight: .bold, design: .rounded))
85203
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.")
204 Text("For the best experience, download an enhanced voice on your device.\n\nSettings → Accessibility → Read & Speak → Voices → English → tap a voice marked Enhanced or Premium to download it.")
87205 .font(.system(size: 18, design: .rounded))
88206 .multilineTextAlignment(.center)
89207 .foregroundStyle(.secondary)
DayByDay/DayByDayApp.swift +17
@@ -5,10 +5,27 @@
55// Created by cmc on 2026-03-09.
66//
77
8import AVFoundation
89import SwiftUI
910
1011@main
1112struct DayByDayApp: App {
13 init() {
14 Task.detached(priority: .background) {
15 // Activate the audio session early so the first real tap doesn't block.
16 try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
17 try? AVAudioSession.sharedInstance().setActive(true)
18
19 // Force singleton init (voice selection) and prime the TTS engine
20 // with a silent utterance so subsequent speaks are instant.
21 await MainActor.run {
22 let warmup = AVSpeechUtterance(string: "")
23 warmup.volume = 0
24 SpeechSynthesizer.shared.speakUtterance(warmup)
25 }
26 }
27 }
28
1229 var body: some Scene {
1330 WindowGroup {
1431 ContentView()
DayByDay/Info.plist added +15
@@ -0,0 +1,15 @@
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>UILaunchScreen</key>
6 <dict>
7 <key>UIColorName</key>
8 <string>LaunchBackground</string>
9 <key>UIImageName</key>
10 <string>AppIcon</string>
11 <key>UIImageRespectsSafeAreaInsets</key>
12 <false/>
13 </dict>
14</dict>
15</plist>
DayByDay/SpeechSynthesizer.swift +22 −15
@@ -18,7 +18,16 @@ final class SpeechSynthesizer {
1818 self.voice = Self.bestAvailableVoice()
1919 }
2020
21 /// Speaks a pre-configured utterance directly. Used for warmup at launch.
22 func speakUtterance(_ utterance: AVSpeechUtterance) {
23 synthesizer.speak(utterance)
24 }
25
2126 func speak(_ text: String) {
27 // Ensure speech plays through the speaker even when the silent switch is on.
28 try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
29 try? AVAudioSession.sharedInstance().setActive(true)
30
2231 if synthesizer.isSpeaking {
2332 synthesizer.stopSpeaking(at: .immediate)
2433 }
@@ -32,21 +41,19 @@ final class SpeechSynthesizer {
3241 }
3342
3443 /// 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.
44 /// Filters for en-US, excludes novelty voices, then sorts by quality
45 /// descending so premium (3) > enhanced (2) > default (1).
3746 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")
47 let best = AVSpeechSynthesisVoice.speechVoices()
48 .filter { voice in
49 voice.language.hasPrefix("en-US")
50 && !voice.voiceTraits.contains(.isNoveltyVoice)
51 }
52 .sorted { $0.quality.rawValue > $1.quality.rawValue }
53 .first
54
55 let selected = best ?? AVSpeechSynthesisVoice(language: "en-US")
56 print("[DayByDay] Selected voice: \(selected?.name ?? "nil"), quality: \(selected?.quality.rawValue ?? -1)")
57 return selected
5158 }
5259}
FoodCard.swift added +115
@@ -0,0 +1,115 @@
1//
2// FoodCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one fruit or vegetable.
9/// Tapping the card plays a bounce animation and speaks the food name aloud.
10/// Each food displays its emoji at large size for instant recognition.
11struct FoodCard: View {
12 let food: LearnFood
13 @State private var isTapped = false
14
15 var body: some View {
16 ZStack {
17 RoundedRectangle(cornerRadius: 32, style: .continuous)
18 .fill(food.color.gradient)
19 .shadow(color: food.color.opacity(0.4), radius: 8, y: 4)
20
21 VStack(spacing: 16) {
22 Text(food.emoji)
23 .font(.system(size: 72))
24
25 Text(food.name)
26 .font(.system(size: 28, weight: .bold, design: .rounded))
27 .foregroundStyle(.white)
28 }
29 }
30 .scaleEffect(isTapped ? 1.12 : 1.0)
31 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
32 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
33 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
34 .onTapGesture {
35 isTapped = true
36 SpeechSynthesizer.shared.speak(food.name)
37 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
38 isTapped = false
39 }
40 }
41 .accessibilityLabel(food.name)
42 .accessibilityAddTraits(.isButton)
43 }
44
45}
46
47// MARK: - Food model
48
49enum LearnFood: Int, CaseIterable, Identifiable {
50 case apple, banana, carrot, strawberry, grapes, lemon,
51 broccoli, watermelon, cherry, orange, corn, pear
52
53 var id: Int { rawValue }
54
55 var name: String {
56 switch self {
57 case .apple: "Apple"
58 case .banana: "Banana"
59 case .carrot: "Carrot"
60 case .strawberry: "Strawberry"
61 case .grapes: "Grapes"
62 case .lemon: "Lemon"
63 case .broccoli: "Broccoli"
64 case .watermelon: "Watermelon"
65 case .cherry: "Cherry"
66 case .orange: "Orange"
67 case .corn: "Corn"
68 case .pear: "Pear"
69 }
70 }
71
72 var emoji: String {
73 switch self {
74 case .apple: "🍎"
75 case .banana: "🍌"
76 case .carrot: "🥕"
77 case .strawberry: "🍓"
78 case .grapes: "🍇"
79 case .lemon: "🍋"
80 case .broccoli: "🥦"
81 case .watermelon: "🍉"
82 case .cherry: "🍒"
83 case .orange: "🍊"
84 case .corn: "🌽"
85 case .pear: "🍐"
86 }
87 }
88
89 var color: Color {
90 switch self {
91 case .apple: Color(red: 0.85, green: 0.2, blue: 0.2) // red
92 case .banana: Color(red: 0.95, green: 0.8, blue: 0.2) // yellow
93 case .carrot: Color(red: 1.0, green: 0.55, blue: 0.15) // orange
94 case .strawberry: Color(red: 0.9, green: 0.25, blue: 0.3) // strawberry red
95 case .grapes: Color(red: 0.55, green: 0.3, blue: 0.7) // purple
96 case .lemon: Color(red: 0.9, green: 0.8, blue: 0.2) // lemon yellow
97 case .broccoli: Color(red: 0.3, green: 0.65, blue: 0.3) // green
98 case .watermelon: Color(red: 0.35, green: 0.7, blue: 0.4) // watermelon green
99 case .cherry: Color(red: 0.7, green: 0.15, blue: 0.2) // dark red
100 case .orange: Color(red: 1.0, green: 0.6, blue: 0.15) // orange
101 case .corn: Color(red: 0.9, green: 0.75, blue: 0.2) // golden yellow
102 case .pear: Color(red: 0.55, green: 0.75, blue: 0.3) // pear green
103 }
104 }
105}
106
107#Preview {
108 HStack(spacing: 24) {
109 FoodCard(food: .apple)
110 .frame(width: 200, height: 260)
111 FoodCard(food: .carrot)
112 .frame(width: 200, height: 260)
113 }
114 .padding()
115}
FoodView.swift added +29
@@ -0,0 +1,29 @@
1//
2// FoodView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all 12 fruits and vegetables as large, tappable cards in a scrollable grid.
9struct FoodView: View {
10 private let columns = [
11 GridItem(.adaptive(minimum: 280), spacing: 24)
12 ]
13
14 var body: some View {
15 ScrollView {
16 LazyVGrid(columns: columns, spacing: 24) {
17 ForEach(LearnFood.allCases) { food in
18 FoodCard(food: food)
19 .frame(height: 200)
20 }
21 }
22 .padding(32)
23 }
24 }
25}
26
27#Preview {
28 FoodView()
29}
LetterCard.swift added +84
@@ -0,0 +1,84 @@
1//
2// LetterCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one letter of the alphabet.
9/// Tapping the card plays a bounce animation and speaks the letter aloud.
10struct LetterCard: View {
11 let letter: LearnLetter
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(letter.color.gradient)
18 .shadow(color: letter.color.opacity(0.4), radius: 8, y: 4)
19
20 Text(letter.character)
21 .font(.system(size: 64, weight: .heavy, design: .rounded))
22 .foregroundStyle(.white)
23 }
24 .scaleEffect(isTapped ? 1.12 : 1.0)
25 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
26 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
27 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
28 .onTapGesture {
29 isTapped = true
30 SpeechSynthesizer.shared.speak(letter.spokenName)
31 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
32 isTapped = false
33 }
34 }
35 .accessibilityLabel(letter.spokenName)
36 .accessibilityAddTraits(.isButton)
37 }
38}
39
40// MARK: - Letter model
41
42enum LearnLetter: Int, CaseIterable, Identifiable {
43 case a, b, c, d, e, f, g, h, i, j, k, l, m,
44 n, o, p, q, r, s, t, u, v, w, x, y, z
45
46 var id: Int { rawValue }
47
48 var character: String {
49 String(Character(UnicodeScalar(65 + rawValue)!))
50 }
51
52 /// Speak the letter name clearly for a child.
53 /// Lowercase prevents TTS from saying "Capital A".
54 var spokenName: String {
55 character.lowercased()
56 }
57
58 /// Cycle through 8 bright colors to give variety without unique-per-letter mapping.
59 var color: Color {
60 let palette: [Color] = [
61 Color(red: 0.9, green: 0.3, blue: 0.3), // red
62 Color(red: 1.0, green: 0.55, blue: 0.2), // orange
63 Color(red: 1.0, green: 0.8, blue: 0.2), // yellow
64 Color(red: 0.35, green: 0.75, blue: 0.4), // green
65 Color(red: 0.2, green: 0.65, blue: 0.9), // blue
66 Color(red: 0.55, green: 0.4, blue: 0.85), // purple
67 Color(red: 0.85, green: 0.4, blue: 0.6), // pink
68 Color(red: 0.2, green: 0.75, blue: 0.75), // teal
69 ]
70 return palette[rawValue % palette.count]
71 }
72}
73
74#Preview {
75 HStack(spacing: 24) {
76 LetterCard(letter: .a)
77 .frame(width: 140, height: 160)
78 LetterCard(letter: .b)
79 .frame(width: 140, height: 160)
80 LetterCard(letter: .c)
81 .frame(width: 140, height: 160)
82 }
83 .padding()
84}
NumberCard.swift added +136
@@ -0,0 +1,136 @@
1//
2// NumberCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one number (1–10).
9/// Tapping the card plays a bounce animation and speaks the number name aloud.
10struct NumberCard: View {
11 let number: LearnNumber
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(number.color.gradient)
19 .shadow(color: number.color.opacity(0.4), radius: 8, y: 4)
20
21 VStack(spacing: 10) {
22 Text("\(number.numeral)")
23 .font(.system(size: 60, weight: .heavy, design: .rounded))
24 .foregroundStyle(.white)
25
26 Text(number.name)
27 .font(.system(size: 28, weight: .bold, design: .rounded))
28 .foregroundStyle(.white)
29
30 // Dot pattern showing the quantity
31 DotPattern(count: number.numeral)
32 .padding(.top, 4)
33 }
34 }
35 .overlay {
36 if isHighlighted {
37 RoundedRectangle(cornerRadius: 32, style: .continuous)
38 .strokeBorder(.white, lineWidth: 5)
39 }
40 }
41 .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
42 .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
43 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
44 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
45 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
46 .onTapGesture {
47 isTapped = true
48 SpeechSynthesizer.shared.speak(number.name)
49 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
50 isTapped = false
51 }
52 }
53 .accessibilityLabel(number.name)
54 .accessibilityAddTraits(.isButton)
55 }
56}
57
58// MARK: - Number model
59
60enum LearnNumber: Int, CaseIterable, Identifiable {
61 case one = 1, two, three, four, five, six, seven, eight, nine, ten
62
63 var id: Int { rawValue }
64
65 /// Highlight the number matching today's day-of-month if it falls in 1–10.
66 static var current: LearnNumber? {
67 let day = Calendar.current.component(.day, from: Date())
68 return LearnNumber(rawValue: day)
69 }
70
71 var numeral: Int { rawValue }
72
73 var name: String {
74 switch self {
75 case .one: "One"
76 case .two: "Two"
77 case .three: "Three"
78 case .four: "Four"
79 case .five: "Five"
80 case .six: "Six"
81 case .seven: "Seven"
82 case .eight: "Eight"
83 case .nine: "Nine"
84 case .ten: "Ten"
85 }
86 }
87
88 var color: Color {
89 switch self {
90 case .one: Color(red: 0.9, green: 0.3, blue: 0.3) // red
91 case .two: Color(red: 1.0, green: 0.55, blue: 0.2) // orange
92 case .three: Color(red: 1.0, green: 0.8, blue: 0.2) // yellow
93 case .four: Color(red: 0.35, green: 0.75, blue: 0.4) // green
94 case .five: Color(red: 0.2, green: 0.7, blue: 0.85) // sky blue
95 case .six: Color(red: 0.4, green: 0.45, blue: 0.9) // blue
96 case .seven: Color(red: 0.6, green: 0.4, blue: 0.85) // purple
97 case .eight: Color(red: 0.85, green: 0.35, blue: 0.6) // pink
98 case .nine: Color(red: 0.5, green: 0.75, blue: 0.45) // lime
99 case .ten: Color(red: 0.25, green: 0.8, blue: 0.75) // teal
100 }
101 }
102}
103
104// MARK: - Dot pattern
105
106/// Displays a grid of filled circles representing a quantity.
107/// 1–5 dots use centered rows; 6–10 use a 5-column grid that wraps naturally.
108struct DotPattern: View {
109 let count: Int
110
111 private var columns: [GridItem] {
112 let maxCols = count <= 5 ? min(count, 5) : 5
113 return Array(repeating: GridItem(.fixed(14), spacing: 6), count: maxCols)
114 }
115
116 var body: some View {
117 LazyVGrid(columns: columns, spacing: 6) {
118 ForEach(0..<count, id: \.self) { _ in
119 Circle()
120 .fill(.white.opacity(0.8))
121 .frame(width: 14, height: 14)
122 }
123 }
124 .frame(maxWidth: .infinity)
125 }
126}
127
128#Preview {
129 HStack(spacing: 24) {
130 NumberCard(number: .three, isHighlighted: true)
131 .frame(width: 280, height: 300)
132 NumberCard(number: .seven)
133 .frame(width: 280, height: 300)
134 }
135 .padding()
136}
NumbersView.swift added +33
@@ -0,0 +1,33 @@
1//
2// NumbersView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays numbers 1–10 as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct NumbersView: 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(LearnNumber.allCases) { number in
19 NumberCard(
20 number: number,
21 isHighlighted: number == .current
22 )
23 .frame(height: 200)
24 }
25 }
26 .padding(32)
27 }
28 }
29}
30
31#Preview {
32 NumbersView()
33}
Screenshots/ipad.png

Binary file not shown.

Screenshots/iphone.png

Binary file not shown.

ShapeCard.swift added +130
@@ -0,0 +1,130 @@
1//
2// ShapeCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one shape.
9/// Tapping the card plays a bounce animation and speaks the shape name aloud.
10struct ShapeCard: View {
11 let shape: LearnShape
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(shape.color.gradient)
18 .shadow(color: shape.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 shape.shapeView
22 .frame(width: 80, height: 80)
23 .foregroundStyle(.white)
24
25 Text(shape.name)
26 .font(.system(size: 32, weight: .bold, design: .rounded))
27 .foregroundStyle(.white)
28 }
29 }
30 .scaleEffect(isTapped ? 1.12 : 1.0)
31 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
32 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
33 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
34 .onTapGesture {
35 isTapped = true
36 SpeechSynthesizer.shared.speak(shape.name)
37 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
38 isTapped = false
39 }
40 }
41 .accessibilityLabel(shape.name)
42 .accessibilityAddTraits(.isButton)
43 }
44}
45
46// MARK: - Shape model
47
48enum LearnShape: Int, CaseIterable, Identifiable {
49 case circle, square, triangle, star, heart, oval, diamond, rectangle
50
51 var id: Int { rawValue }
52
53 var name: String {
54 switch self {
55 case .circle: "Circle"
56 case .square: "Square"
57 case .triangle: "Triangle"
58 case .star: "Star"
59 case .heart: "Heart"
60 case .oval: "Oval"
61 case .diamond: "Diamond"
62 case .rectangle: "Rectangle"
63 }
64 }
65
66 @ViewBuilder
67 var shapeView: some View {
68 switch self {
69 case .circle:
70 Circle().fill(.white)
71 case .square:
72 SwiftUI.Rectangle().fill(.white)
73 case .triangle:
74 TriangleShape().fill(.white)
75 case .star:
76 Image(systemName: "star.fill")
77 .font(.system(size: 72))
78 .foregroundStyle(.white)
79 case .heart:
80 Image(systemName: "heart.fill")
81 .font(.system(size: 72))
82 .foregroundStyle(.white)
83 case .oval:
84 Ellipse().fill(.white)
85 .frame(width: 90, height: 60)
86 case .diamond:
87 SwiftUI.Rectangle().fill(.white)
88 .frame(width: 60, height: 60)
89 .rotationEffect(.degrees(45))
90 case .rectangle:
91 SwiftUI.Rectangle().fill(.white)
92 .frame(width: 90, height: 55)
93 }
94 }
95
96 var color: Color {
97 switch self {
98 case .circle: Color(red: 0.9, green: 0.3, blue: 0.3) // red
99 case .square: Color(red: 0.3, green: 0.5, blue: 0.9) // blue
100 case .triangle: Color(red: 0.35, green: 0.75, blue: 0.45) // green
101 case .star: Color(red: 1.0, green: 0.75, blue: 0.15) // gold
102 case .heart: Color(red: 0.9, green: 0.35, blue: 0.5) // pink
103 case .oval: Color(red: 0.55, green: 0.4, blue: 0.85) // purple
104 case .diamond: Color(red: 0.2, green: 0.75, blue: 0.8) // teal
105 case .rectangle: Color(red: 1.0, green: 0.55, blue: 0.2) // orange
106 }
107 }
108}
109
110/// A simple triangle drawn with a Path.
111struct TriangleShape: Shape {
112 func path(in rect: CGRect) -> Path {
113 Path { path in
114 path.move(to: CGPoint(x: rect.midX, y: rect.minY))
115 path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
116 path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
117 path.closeSubpath()
118 }
119 }
120}
121
122#Preview {
123 HStack(spacing: 24) {
124 ShapeCard(shape: .triangle)
125 .frame(width: 200, height: 260)
126 ShapeCard(shape: .star)
127 .frame(width: 200, height: 260)
128 }
129 .padding()
130}
ShapesView.swift added +30
@@ -0,0 +1,30 @@
1//
2// ShapesView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays 8 shapes as large, tappable cards in a scrollable grid.
9/// Adapts between 1 and 2 columns depending on available width.
10struct ShapesView: 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(LearnShape.allCases) { shape in
19 ShapeCard(shape: shape)
20 .frame(height: 200)
21 }
22 }
23 .padding(32)
24 }
25 }
26}
27
28#Preview {
29 ShapesView()
30}
TodayView.swift added +84
@@ -0,0 +1,84 @@
1//
2// TodayView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single-screen summary of today: day of the week, date, month, and season.
9/// The child taps the large card to hear "Today is [day], [month] [date]. It is [season]."
10struct TodayView: View {
11 @State private var isTapped = false
12
13 private var todayText: String {
14 let day = DayOfWeek.current.name
15 let month = MonthOfYear.current.name
16 let date = Calendar.current.component(.day, from: Date())
17 let season = Season.current.name
18 return "Today is \(day), \(month) \(date). It is \(season)."
19 }
20
21 var body: some View {
22 ScrollView {
23 VStack(spacing: 32) {
24 // Main today card
25 ZStack {
26 RoundedRectangle(cornerRadius: 32, style: .continuous)
27 .fill(Color.orange.gradient)
28 .shadow(color: Color.orange.opacity(0.4), radius: 8, y: 4)
29
30 VStack(spacing: 20) {
31 Image(systemName: "sun.horizon.fill")
32 .font(.system(size: 80))
33 .foregroundStyle(.white)
34 .symbolRenderingMode(.hierarchical)
35
36 // Day of the week
37 Text(DayOfWeek.current.name)
38 .font(.system(size: 48, weight: .bold, design: .rounded))
39 .foregroundStyle(.white)
40
41 // Date number
42 let dayNumber = Calendar.current.component(.day, from: Date())
43 Text("\(dayNumber)")
44 .font(.system(size: 72, weight: .heavy, design: .rounded))
45 .foregroundStyle(.white.opacity(0.9))
46
47 // Month and season
48 HStack(spacing: 24) {
49 Label(MonthOfYear.current.name, systemImage: MonthOfYear.current.symbol)
50 Label(Season.current.name, systemImage: Season.current.symbol)
51 }
52 .font(.system(size: 24, weight: .semibold, design: .rounded))
53 .foregroundStyle(.white.opacity(0.9))
54 }
55 .padding(.vertical, 32)
56 }
57 .frame(height: 420)
58 .scaleEffect(isTapped ? 1.08 : 1.0)
59 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
60 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
61 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
62 .onTapGesture {
63 isTapped = true
64 SpeechSynthesizer.shared.speak(todayText)
65 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
66 isTapped = false
67 }
68 }
69 .accessibilityLabel(todayText)
70 .accessibilityAddTraits(.isButton)
71
72 // Tap hint
73 Label("Tap to hear today", systemImage: "speaker.wave.2.fill")
74 .font(.system(size: 22, weight: .medium, design: .rounded))
75 .foregroundStyle(.secondary)
76 }
77 .padding(32)
78 }
79 }
80}
81
82#Preview {
83 TodayView()
84}
WeatherCard.swift added +102
@@ -0,0 +1,102 @@
1//
2// WeatherCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one weather type.
9/// Tapping the card plays a bounce animation and speaks the weather name aloud.
10struct WeatherCard: View {
11 let weather: LearnWeather
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(weather.color.gradient)
18 .shadow(color: weather.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 Image(systemName: weather.symbol)
22 .font(.system(size: 64))
23 .foregroundStyle(.white)
24 .symbolRenderingMode(.hierarchical)
25
26 Text(weather.name)
27 .font(.system(size: 32, weight: .bold, design: .rounded))
28 .foregroundStyle(.white)
29 }
30 }
31 .scaleEffect(isTapped ? 1.12 : 1.0)
32 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
33 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
34 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
35 .onTapGesture {
36 isTapped = true
37 SpeechSynthesizer.shared.speak(weather.name)
38 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
39 isTapped = false
40 }
41 }
42 .accessibilityLabel(weather.name)
43 .accessibilityAddTraits(.isButton)
44 }
45}
46
47// MARK: - Weather model
48
49enum LearnWeather: Int, CaseIterable, Identifiable {
50 case sunny, cloudy, rainy, snowy, windy, stormy, foggy, rainbow
51
52 var id: Int { rawValue }
53
54 var name: String {
55 switch self {
56 case .sunny: "Sunny"
57 case .cloudy: "Cloudy"
58 case .rainy: "Rainy"
59 case .snowy: "Snowy"
60 case .windy: "Windy"
61 case .stormy: "Stormy"
62 case .foggy: "Foggy"
63 case .rainbow: "Rainbow"
64 }
65 }
66
67 var symbol: String {
68 switch self {
69 case .sunny: "sun.max.fill"
70 case .cloudy: "cloud.fill"
71 case .rainy: "cloud.rain.fill"
72 case .snowy: "cloud.snow.fill"
73 case .windy: "wind"
74 case .stormy: "cloud.bolt.rain.fill"
75 case .foggy: "cloud.fog.fill"
76 case .rainbow: "rainbow"
77 }
78 }
79
80 var color: Color {
81 switch self {
82 case .sunny: Color(red: 1.0, green: 0.7, blue: 0.2) // orange
83 case .cloudy: Color(red: 0.6, green: 0.6, blue: 0.65) // gray
84 case .rainy: Color(red: 0.3, green: 0.6, blue: 0.9) // blue
85 case .snowy: Color(red: 0.55, green: 0.8, blue: 0.95) // light blue
86 case .windy: Color(red: 0.25, green: 0.7, blue: 0.7) // teal
87 case .stormy: Color(red: 0.2, green: 0.3, blue: 0.6) // dark blue
88 case .foggy: Color(red: 0.6, green: 0.5, blue: 0.7) // muted purple
89 case .rainbow: Color(red: 0.9, green: 0.45, blue: 0.6) // pink
90 }
91 }
92}
93
94#Preview {
95 HStack(spacing: 24) {
96 WeatherCard(weather: .sunny)
97 .frame(width: 200, height: 260)
98 WeatherCard(weather: .stormy)
99 .frame(width: 200, height: 260)
100 }
101 .padding()
102}
WeatherView.swift added +29
@@ -0,0 +1,29 @@
1//
2// WeatherView.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// Displays all 8 weather types as large, tappable cards in a scrollable grid.
9struct WeatherView: View {
10 private let columns = [
11 GridItem(.adaptive(minimum: 280), spacing: 24)
12 ]
13
14 var body: some View {
15 ScrollView {
16 LazyVGrid(columns: columns, spacing: 24) {
17 ForEach(LearnWeather.allCases) { weather in
18 WeatherCard(weather: weather)
19 .frame(height: 200)
20 }
21 }
22 .padding(32)
23 }
24 }
25}
26
27#Preview {
28 WeatherView()
29}
claude.md +23 −8
@@ -1,8 +1,9 @@
11# DayByDay – Project Context
22
33## What this is
4An iPad app for pre-K children (ages 4–5) that teaches days of the week, months
5of the year, and seasons through simple, tap-based interactions.
4An iOS app for pre-K children (ages 4–5) that teaches days of the week, months
5of the year, seasons, numbers, colors, shapes, animals, the alphabet, weather,
6body parts, and fruits & vegetables through simple, tap-based interactions.
67
78## Target user
89A single child, approximately 4–5 years old, preparing to start school. Cannot
@@ -13,15 +14,28 @@ independently.
1314- Large tap targets — nothing small or fiddly
1415- Bright, clean colors — simple and friendly, not cluttered
1516- No text-dependent interactions — the child cannot read
16- Audio is the primary feedback mechanism (day/month/season name read aloud on
17 tap)
17- Audio is the primary feedback mechanism (names read aloud on tap)
1818- Simple animations on interaction — enough to delight, not enough to distract
1919- No timers, no failure states, no scores — purely exploratory
2020
21## Content
21## Content (12 categories, accessible from a NavigationStack home grid)
22- Today: summary card with current day, date, month, season
2223- Days of the week: Sunday through Saturday
2324- Months of the year: January through December
2425- Seasons: Spring, Summer, Fall, Winter
26- Numbers: 1 through 10 (numeral + word + dot pattern)
27- Colors: 10 basic colors (Red, Orange, Yellow, Green, Blue, Purple, Pink,
28 Brown, Black, White)
29- Shapes: 8 shapes (Circle, Square, Triangle, Star, Heart, Oval, Diamond,
30 Rectangle)
31- Animals: 12 animals with SF Symbol icons
32- Alphabet: A through Z
33- Weather: 8 weather types (Sunny, Cloudy, Rainy, Snowy, Windy, Stormy,
34 Foggy, Rainbow)
35- Body Parts: 12 body parts (Head, Eyes, Ears, Nose, Mouth, Hands, Fingers,
36 Arms, Legs, Feet, Heart, Belly)
37- Fruits & Vegetables: 12 foods (Apple, Banana, Carrot, Strawberry, Grapes,
38 Lemon, Broccoli, Watermelon, Cherry, Orange, Corn, Pear)
2539
2640## Technical constraints
2741- SwiftUI, iOS only
@@ -30,6 +44,7 @@ independently.
3044- Targets latest iOS
3145
3246## What good looks like
33Each concept (day, month, season) should feel like its own tactile card or
34object the child can tap and explore. Transitions should be smooth. Audio should
35play immediately on tap with no perceptible delay.
47Each concept should feel like its own tactile card or object the child can tap
48and explore. Transitions should be smooth. Audio should play immediately on tap
49with no perceptible delay. The home screen is a colorful 2-column grid of square category
50tiles that navigate into each section.