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 @@
7 objects = { 7 objects = {
8 8
9/* Begin PBXBuildFile section */ 9/* 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 */; };
10 8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; }; 27 8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; };
11/* End PBXBuildFile section */ 28/* End PBXBuildFile section */
12 29
13/* Begin PBXFileReference section */ 30/* 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>"; };
14 8B7051B22F5F908800491D58 /* DayByDay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DayByDay.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 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>"; }; 49 8B7051C02F5F90C800491D58 /* claude.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = claude.md; sourceTree = "<group>"; };
16/* End PBXFileReference section */ 50/* End PBXFileReference section */
17 51
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
18/* Begin PBXFileSystemSynchronizedRootGroup section */ 62/* Begin PBXFileSystemSynchronizedRootGroup section */
19 8B7051B42F5F908800491D58 /* DayByDay */ = { 63 8B7051B42F5F908800491D58 /* DayByDay */ = {
20 isa = PBXFileSystemSynchronizedRootGroup; 64 isa = PBXFileSystemSynchronizedRootGroup;
65 exceptions = (
66 8B5214E62F621E0200E1D35A /* Exceptions for "DayByDay" folder in "DayByDay" target */,
67 );
21 path = DayByDay; 68 path = DayByDay;
22 sourceTree = "<group>"; 69 sourceTree = "<group>";
23 }; 70 };
@@ -40,6 +87,23 @@
40 8B7051C02F5F90C800491D58 /* claude.md */, 87 8B7051C02F5F90C800491D58 /* claude.md */,
41 8B7051B42F5F908800491D58 /* DayByDay */, 88 8B7051B42F5F908800491D58 /* DayByDay */,
42 8B7051B32F5F908800491D58 /* Products */, 89 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 */,
43 ); 107 );
44 sourceTree = "<group>"; 108 sourceTree = "<group>";
45 }; 109 };
@@ -126,6 +190,23 @@
126 isa = PBXSourcesBuildPhase; 190 isa = PBXSourcesBuildPhase;
127 buildActionMask = 2147483647; 191 buildActionMask = 2147483647;
128 files = ( 192 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 */,
129 ); 210 );
130 runOnlyForDeploymentPostprocessing = 0; 211 runOnlyForDeploymentPostprocessing = 0;
131 }; 212 };
@@ -259,21 +340,21 @@
259 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
260 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 341 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
261 CODE_SIGN_STYLE = Automatic; 342 CODE_SIGN_STYLE = Automatic;
262 CURRENT_PROJECT_VERSION = 1; 343 CURRENT_PROJECT_VERSION = 3;
263 DEVELOPMENT_TEAM = ZCNAX3VL9D; 344 DEVELOPMENT_TEAM = ZCNAX3VL9D;
264 ENABLE_PREVIEWS = YES; 345 ENABLE_PREVIEWS = YES;
265 GENERATE_INFOPLIST_FILE = YES; 346 GENERATE_INFOPLIST_FILE = YES;
347 INFOPLIST_FILE = DayByDay/Info.plist;
266 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day"; 348 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
267 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; 349 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
268 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 350 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
269 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 351 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
270 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
271 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 352 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
272 LD_RUNPATH_SEARCH_PATHS = ( 353 LD_RUNPATH_SEARCH_PATHS = (
273 "$(inherited)", 354 "$(inherited)",
274 "@executable_path/Frameworks", 355 "@executable_path/Frameworks",
275 ); 356 );
276 MARKETING_VERSION = 1.0; 357 MARKETING_VERSION = 1.2;
277 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; 358 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
278 PRODUCT_NAME = "$(TARGET_NAME)"; 359 PRODUCT_NAME = "$(TARGET_NAME)";
279 STRING_CATALOG_GENERATE_SYMBOLS = YES; 360 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -292,21 +373,21 @@
292 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
293 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 374 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
294 CODE_SIGN_STYLE = Automatic; 375 CODE_SIGN_STYLE = Automatic;
295 CURRENT_PROJECT_VERSION = 1; 376 CURRENT_PROJECT_VERSION = 3;
296 DEVELOPMENT_TEAM = ZCNAX3VL9D; 377 DEVELOPMENT_TEAM = ZCNAX3VL9D;
297 ENABLE_PREVIEWS = YES; 378 ENABLE_PREVIEWS = YES;
298 GENERATE_INFOPLIST_FILE = YES; 379 GENERATE_INFOPLIST_FILE = YES;
380 INFOPLIST_FILE = DayByDay/Info.plist;
299 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day"; 381 INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
300 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; 382 INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
301 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 383 INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
302 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 384 INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
303 INFOPLIST_KEY_UILaunchScreen_Generation = YES;
304 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 385 INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
305 LD_RUNPATH_SEARCH_PATHS = ( 386 LD_RUNPATH_SEARCH_PATHS = (
306 "$(inherited)", 387 "$(inherited)",
307 "@executable_path/Frameworks", 388 "@executable_path/Frameworks",
308 ); 389 );
309 MARKETING_VERSION = 1.0; 390 MARKETING_VERSION = 1.2;
310 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; 391 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
311 PRODUCT_NAME = "$(TARGET_NAME)"; 392 PRODUCT_NAME = "$(TARGET_NAME)";
312 STRING_CATALOG_GENERATE_SYMBOLS = YES; 393 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 @@
7 7
8import SwiftUI 8import SwiftUI
9 9
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
10struct ContentView: View { 115struct ContentView: View {
11 @State private var selectedTab = 0
12 @AppStorage("hasSeenVoiceTip") private var hasSeenVoiceTip = false 116 @AppStorage("hasSeenVoiceTip") private var hasSeenVoiceTip = false
13 117
14 private let tabs: [(label: String, symbol: String)] = [ 118 private let columns = [
15 ("Days", "calendar"), 119 GridItem(.flexible(), spacing: 24),
16 ("Months", "calendar.badge.clock"), 120 GridItem(.flexible(), spacing: 24)
17 ("Seasons", "leaf.fill"),
18 ] 121 ]
19 122
20 var body: some View { 123 var body: some View {
21 VStack(spacing: 0) { 124 NavigationStack {
22 Group { 125 ScrollView {
23 switch selectedTab { 126 LazyVGrid(columns: columns, spacing: 24) {
24 case 0: DaysOfWeekView() 127 ForEach(Category.allCases) { category in
25 case 1: MonthsOfYearView() 128 NavigationLink(value: category) {
26 default: SeasonsView() 129 CategoryTile(category: category)
27 } 130 .aspectRatio(1, contentMode: .fit)
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 } 131 }
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)
46 .frame(maxWidth: .infinity) 143 .frame(maxWidth: .infinity)
47 .padding(.vertical, 14) 144 .padding(.vertical, 14)
48 .contentShape(Rectangle()) 145 .background(Color(.systemGray6))
49 } 146 .cornerRadius(16)
50 .buttonStyle(.plain)
51 .accessibilityLabel(tab.label)
52 } 147 }
148 .buttonStyle(.plain)
149 .padding(.horizontal, 24)
150 .padding(.bottom, 24)
53 } 151 }
54 .padding(.horizontal, 24) 152 .navigationTitle("DayByDay")
55 .padding(.bottom, 8) 153 .navigationDestination(for: Category.self) { category in
56 .background(.bar) 154 destinationView(for: category)
57 } 155 .navigationTitle(category.label)
58 .overlay { 156 }
59 if !hasSeenVoiceTip { 157 .overlay {
60 VoiceTipOverlay { 158 if !hasSeenVoiceTip {
61 withAnimation { hasSeenVoiceTip = true } 159 VoiceTipOverlay {
160 withAnimation { hasSeenVoiceTip = true }
161 }
62 } 162 }
63 } 163 }
64 } 164 }
65 } 165 }
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 }
66} 184}
67 185
68/// A one-time informational overlay for parents explaining how to download 186/// A one-time informational overlay for parents explaining how to download
@@ -83,7 +201,7 @@ struct VoiceTipOverlay: View {
83 Text("Better Voices Available") 201 Text("Better Voices Available")
84 .font(.system(size: 28, weight: .bold, design: .rounded)) 202 .font(.system(size: 28, weight: .bold, design: .rounded))
85 203
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.")
87 .font(.system(size: 18, design: .rounded)) 205 .font(.system(size: 18, design: .rounded))
88 .multilineTextAlignment(.center) 206 .multilineTextAlignment(.center)
89 .foregroundStyle(.secondary) 207 .foregroundStyle(.secondary)
DayByDay/DayByDayApp.swift +17
@@ -5,10 +5,27 @@
5// Created by cmc on 2026-03-09. 5// Created by cmc on 2026-03-09.
6// 6//
7 7
8import AVFoundation
8import SwiftUI 9import SwiftUI
9 10
10@main 11@main
11struct DayByDayApp: App { 12struct 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
12 var body: some Scene { 29 var body: some Scene {
13 WindowGroup { 30 WindowGroup {
14 ContentView() 31 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 {
18 self.voice = Self.bestAvailableVoice() 18 self.voice = Self.bestAvailableVoice()
19 } 19 }
20 20
21 /// Speaks a pre-configured utterance directly. Used for warmup at launch.
22 func speakUtterance(_ utterance: AVSpeechUtterance) {
23 synthesizer.speak(utterance)
24 }
25
21 func speak(_ text: String) { 26 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
22 if synthesizer.isSpeaking { 31 if synthesizer.isSpeaking {
23 synthesizer.stopSpeaking(at: .immediate) 32 synthesizer.stopSpeaking(at: .immediate)
24 } 33 }
@@ -32,21 +41,19 @@ final class SpeechSynthesizer {
32 } 41 }
33 42
34 /// Picks the best en-US voice on the device. 43 /// Picks the best en-US voice on the device.
35 /// Preference order: premium > enhanced > default quality. 44 /// Filters for en-US, excludes novelty voices, then sorts by quality
36 /// Skips novelty voices so the child always hears a natural-sounding voice. 45 /// descending so premium (3) > enhanced (2) > default (1).
37 private static func bestAvailableVoice() -> AVSpeechSynthesisVoice? { 46 private static func bestAvailableVoice() -> AVSpeechSynthesisVoice? {
38 let candidates = AVSpeechSynthesisVoice.speechVoices().filter { voice in 47 let best = AVSpeechSynthesisVoice.speechVoices()
39 voice.language.hasPrefix("en-US") 48 .filter { voice in
40 && !voice.voiceTraits.contains(.isNoveltyVoice) 49 voice.language.hasPrefix("en-US")
41 } 50 && !voice.voiceTraits.contains(.isNoveltyVoice)
42 51 }
43 if let premium = candidates.first(where: { $0.quality == .premium }) { 52 .sorted { $0.quality.rawValue > $1.quality.rawValue }
44 return premium 53 .first
45 } 54
46 if let enhanced = candidates.first(where: { $0.quality == .enhanced }) { 55 let selected = best ?? AVSpeechSynthesisVoice(language: "en-US")
47 return enhanced 56 print("[DayByDay] Selected voice: \(selected?.name ?? "nil"), quality: \(selected?.quality.rawValue ?? -1)")
48 } 57 return selected
49 // Fall back to the best default-quality voice.
50 return candidates.first ?? AVSpeechSynthesisVoice(language: "en-US")
51 } 58 }
52} 59}
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 @@
1# DayByDay – Project Context 1# DayByDay – Project Context
2 2
3## What this is 3## What this is
4An iPad app for pre-K children (ages 4–5) that teaches days of the week, months 4An iOS 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. 5of the year, seasons, numbers, colors, shapes, animals, the alphabet, weather,
6body parts, and fruits & vegetables through simple, tap-based interactions.
6 7
7## Target user 8## Target user
8A single child, approximately 4–5 years old, preparing to start school. Cannot 9A single child, approximately 4–5 years old, preparing to start school. Cannot
@@ -13,15 +14,28 @@ independently.
13- Large tap targets — nothing small or fiddly 14- Large tap targets — nothing small or fiddly
14- Bright, clean colors — simple and friendly, not cluttered 15- Bright, clean colors — simple and friendly, not cluttered
15- No text-dependent interactions — the child cannot read 16- No text-dependent interactions — the child cannot read
16- Audio is the primary feedback mechanism (day/month/season name read aloud on 17- Audio is the primary feedback mechanism (names read aloud on tap)
17 tap)
18- Simple animations on interaction — enough to delight, not enough to distract 18- Simple animations on interaction — enough to delight, not enough to distract
19- No timers, no failure states, no scores — purely exploratory 19- No timers, no failure states, no scores — purely exploratory
20 20
21## Content 21## Content (12 categories, accessible from a NavigationStack home grid)
22- Today: summary card with current day, date, month, season
22- Days of the week: Sunday through Saturday 23- Days of the week: Sunday through Saturday
23- Months of the year: January through December 24- Months of the year: January through December
24- Seasons: Spring, Summer, Fall, Winter 25- 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)
25 39
26## Technical constraints 40## Technical constraints
27- SwiftUI, iOS only 41- SwiftUI, iOS only
@@ -30,6 +44,7 @@ independently.
30- Targets latest iOS 44- Targets latest iOS
31 45
32## What good looks like 46## What good looks like
33Each concept (day, month, season) should feel like its own tactile card or 47Each concept should feel like its own tactile card or object the child can tap
34object the child can tap and explore. Transitions should be smooth. Audio should 48and explore. Transitions should be smooth. Audio should play immediately on tap
35play immediately on tap with no perceptible delay. 49with no perceptible delay. The home screen is a colorful 2-column grid of square category
50tiles that navigate into each section.