summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AlphabetView.swift30
-rw-r--r--AnimalCard.swift103
-rw-r--r--AnimalsView.swift30
-rw-r--r--BodyPartCard.swift114
-rw-r--r--BodyPartsView.swift29
-rw-r--r--ColorCard.swift86
-rw-r--r--ColorsView.swift30
-rw-r--r--DayByDay.xcodeproj/project.pbxproj93
-rw-r--r--DayByDay/Assets.xcassets/LaunchBackground.colorset/Contents.json38
-rw-r--r--DayByDay/ContentView.swift202
-rw-r--r--DayByDay/DayByDayApp.swift17
-rw-r--r--DayByDay/Info.plist15
-rw-r--r--DayByDay/SpeechSynthesizer.swift37
-rw-r--r--FoodCard.swift115
-rw-r--r--FoodView.swift29
-rw-r--r--LetterCard.swift84
-rw-r--r--NumberCard.swift136
-rw-r--r--NumbersView.swift33
-rw-r--r--Screenshots/ipad.pngbin1350388 -> 1273841 bytes
-rw-r--r--Screenshots/iphone.pngbin1176013 -> 1601412 bytes
-rw-r--r--ShapeCard.swift130
-rw-r--r--ShapesView.swift30
-rw-r--r--TodayView.swift84
-rw-r--r--WeatherCard.swift102
-rw-r--r--WeatherView.swift29
-rw-r--r--claude.md31
26 files changed, 1556 insertions, 71 deletions
diff --git a/AlphabetView.swift b/AlphabetView.swift
new file mode 100644
index 0000000..64af026
--- /dev/null
+++ b/AlphabetView.swift
@@ -0,0 +1,30 @@
+//
+// AlphabetView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays all 26 letters A–Z as large, tappable cards in a scrollable grid.
+/// Uses a smaller minimum size to fit more letters across the screen.
+struct AlphabetView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 140, maximum: 300), spacing: 20)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 20) {
+ ForEach(LearnLetter.allCases) { letter in
+ LetterCard(letter: letter)
+ .frame(height: 160)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ AlphabetView()
+}
diff --git a/AnimalCard.swift b/AnimalCard.swift
new file mode 100644
index 0000000..2207d45
--- /dev/null
+++ b/AnimalCard.swift
@@ -0,0 +1,103 @@
+//
+// AnimalCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one animal.
+/// Tapping the card plays a bounce animation and speaks the animal name aloud.
+struct AnimalCard: View {
+ let animal: LearnAnimal
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(animal.color.gradient)
+ .shadow(color: animal.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 16) {
+ Image(systemName: animal.symbol)
+ .font(.system(size: 64))
+ .foregroundStyle(.white)
+ .symbolRenderingMode(.hierarchical)
+
+ Text(animal.name)
+ .font(.system(size: 32, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(animal.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(animal.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Animal model
+
+enum LearnAnimal: Int, CaseIterable, Identifiable {
+ case cat, dog, bird, fish, rabbit, turtle, ladybug, ant, lizard
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .cat: "Cat"
+ case .dog: "Dog"
+ case .bird: "Bird"
+ case .fish: "Fish"
+ case .rabbit: "Rabbit"
+ case .turtle: "Turtle"
+ case .ladybug: "Ladybug"
+ case .ant: "Ant"
+ case .lizard: "Lizard"
+ }
+ }
+
+ var symbol: String {
+ switch self {
+ case .cat: "cat.fill"
+ case .dog: "dog.fill"
+ case .bird: "bird.fill"
+ case .fish: "fish.fill"
+ case .rabbit: "hare.fill"
+ case .turtle: "tortoise.fill"
+ case .ladybug: "ladybug.fill"
+ case .ant: "ant.fill"
+ case .lizard: "lizard.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .cat: Color(red: 0.9, green: 0.55, blue: 0.2) // orange
+ case .dog: Color(red: 0.6, green: 0.45, blue: 0.3) // brown
+ case .bird: Color(red: 0.3, green: 0.65, blue: 0.9) // sky blue
+ case .fish: Color(red: 0.2, green: 0.75, blue: 0.8) // teal
+ case .rabbit: Color(red: 0.75, green: 0.6, blue: 0.8) // lavender
+ case .turtle: Color(red: 0.35, green: 0.7, blue: 0.4) // green
+ case .ladybug: Color(red: 0.9, green: 0.25, blue: 0.25) // red
+ case .ant: Color(red: 0.3, green: 0.3, blue: 0.3) // dark gray
+ case .lizard: Color(red: 0.45, green: 0.75, blue: 0.35) // lime
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ AnimalCard(animal: .cat)
+ .frame(width: 200, height: 260)
+ }
+ .padding()
+}
diff --git a/AnimalsView.swift b/AnimalsView.swift
new file mode 100644
index 0000000..d3c5a2d
--- /dev/null
+++ b/AnimalsView.swift
@@ -0,0 +1,30 @@
+//
+// AnimalsView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays 12 animals as large, tappable cards in a scrollable grid.
+/// Adapts between 1 and 2 columns depending on available width.
+struct AnimalsView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnAnimal.allCases) { animal in
+ AnimalCard(animal: animal)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ AnimalsView()
+}
diff --git a/BodyPartCard.swift b/BodyPartCard.swift
new file mode 100644
index 0000000..a3693de
--- /dev/null
+++ b/BodyPartCard.swift
@@ -0,0 +1,114 @@
+//
+// BodyPartCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one body part.
+/// Tapping the card plays a bounce animation and speaks the body part name aloud.
+struct BodyPartCard: View {
+ let bodyPart: LearnBodyPart
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(bodyPart.color.gradient)
+ .shadow(color: bodyPart.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 16) {
+ Image(systemName: bodyPart.symbol)
+ .font(.system(size: 64))
+ .foregroundStyle(.white)
+ .symbolRenderingMode(.hierarchical)
+
+ Text(bodyPart.name)
+ .font(.system(size: 32, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(bodyPart.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(bodyPart.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Body part model
+
+enum LearnBodyPart: Int, CaseIterable, Identifiable {
+ case head, eyes, ears, nose, mouth, hands, fingers, arms, legs, feet, heart, belly
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .head: "Head"
+ case .eyes: "Eyes"
+ case .ears: "Ears"
+ case .nose: "Nose"
+ case .mouth: "Mouth"
+ case .hands: "Hands"
+ case .fingers: "Fingers"
+ case .arms: "Arms"
+ case .legs: "Legs"
+ case .feet: "Feet"
+ case .heart: "Heart"
+ case .belly: "Belly"
+ }
+ }
+
+ var symbol: String {
+ switch self {
+ case .head: "brain.head.profile"
+ case .eyes: "eye.fill"
+ case .ears: "ear.fill"
+ case .nose: "nose.fill"
+ case .mouth: "mouth.fill"
+ case .hands: "hand.raised.fill"
+ case .fingers: "hand.point.up.fill"
+ case .arms: "figure.arms.open"
+ case .legs: "figure.walk"
+ case .feet: "shoeprints.fill"
+ case .heart: "heart.fill"
+ case .belly: "circle.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .head: Color(red: 0.9, green: 0.5, blue: 0.3) // warm orange
+ case .eyes: Color(red: 0.3, green: 0.65, blue: 0.9) // blue
+ case .ears: Color(red: 0.6, green: 0.45, blue: 0.85) // purple
+ case .nose: Color(red: 0.9, green: 0.4, blue: 0.5) // rose
+ case .mouth: Color(red: 0.9, green: 0.3, blue: 0.35) // red
+ case .hands: Color(red: 0.95, green: 0.7, blue: 0.3) // golden
+ case .fingers: Color(red: 0.35, green: 0.75, blue: 0.5) // green
+ case .arms: Color(red: 0.25, green: 0.7, blue: 0.7) // teal
+ case .legs: Color(red: 0.5, green: 0.4, blue: 0.8) // indigo
+ case .feet: Color(red: 0.7, green: 0.5, blue: 0.3) // brown
+ case .heart: Color(red: 0.85, green: 0.3, blue: 0.45) // deep pink
+ case .belly: Color(red: 1.0, green: 0.6, blue: 0.4) // peach
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ BodyPartCard(bodyPart: .head)
+ .frame(width: 200, height: 260)
+ BodyPartCard(bodyPart: .hands)
+ .frame(width: 200, height: 260)
+ }
+ .padding()
+}
diff --git a/BodyPartsView.swift b/BodyPartsView.swift
new file mode 100644
index 0000000..73019f5
--- /dev/null
+++ b/BodyPartsView.swift
@@ -0,0 +1,29 @@
+//
+// BodyPartsView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays all 12 body parts as large, tappable cards in a scrollable grid.
+struct BodyPartsView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 280), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnBodyPart.allCases) { part in
+ BodyPartCard(bodyPart: part)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ BodyPartsView()
+}
diff --git a/ColorCard.swift b/ColorCard.swift
new file mode 100644
index 0000000..0bbe2b9
--- /dev/null
+++ b/ColorCard.swift
@@ -0,0 +1,86 @@
+//
+// ColorCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large card filled with a color. Tapping speaks the color name aloud.
+struct ColorCard: View {
+ let learnColor: LearnColor
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(learnColor.color.gradient)
+ .shadow(color: learnColor.color.opacity(0.4), radius: 8, y: 4)
+
+ Text(learnColor.name)
+ .font(.system(size: 36, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ .shadow(color: .black.opacity(0.3), radius: 4, y: 2)
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(learnColor.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(learnColor.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Color model
+
+enum LearnColor: Int, CaseIterable, Identifiable {
+ case red, orange, yellow, green, blue, purple, pink, brown, black, white
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .red: "Red"
+ case .orange: "Orange"
+ case .yellow: "Yellow"
+ case .green: "Green"
+ case .blue: "Blue"
+ case .purple: "Purple"
+ case .pink: "Pink"
+ case .brown: "Brown"
+ case .black: "Black"
+ case .white: "White"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .red: Color(red: 0.9, green: 0.2, blue: 0.2)
+ case .orange: Color(red: 1.0, green: 0.55, blue: 0.15)
+ case .yellow: Color(red: 1.0, green: 0.82, blue: 0.15)
+ case .green: Color(red: 0.2, green: 0.75, blue: 0.35)
+ case .blue: Color(red: 0.2, green: 0.45, blue: 0.9)
+ case .purple: Color(red: 0.55, green: 0.3, blue: 0.85)
+ case .pink: Color(red: 0.95, green: 0.4, blue: 0.6)
+ case .brown: Color(red: 0.55, green: 0.35, blue: 0.2)
+ case .black: Color(red: 0.15, green: 0.15, blue: 0.15)
+ case .white: Color(red: 0.92, green: 0.92, blue: 0.92)
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ ColorCard(learnColor: .red)
+ .frame(width: 200, height: 200)
+ ColorCard(learnColor: .blue)
+ .frame(width: 200, height: 200)
+ }
+ .padding()
+}
diff --git a/ColorsView.swift b/ColorsView.swift
new file mode 100644
index 0000000..9b935e5
--- /dev/null
+++ b/ColorsView.swift
@@ -0,0 +1,30 @@
+//
+// ColorsView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays 10 colors as large, tappable cards in a scrollable grid.
+/// Adapts between 1 and 2 columns depending on available width.
+struct ColorsView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnColor.allCases) { color in
+ ColorCard(learnColor: color)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ ColorsView()
+}
diff --git a/DayByDay.xcodeproj/project.pbxproj b/DayByDay.xcodeproj/project.pbxproj
index 3ff18e9..9e2876c 100644
--- a/DayByDay.xcodeproj/project.pbxproj
+++ b/DayByDay.xcodeproj/project.pbxproj
@@ -7,17 +7,64 @@
objects = {
/* Begin PBXBuildFile section */
+ 8B1B4FF92F659AD1005C246F /* WeatherCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */; };
+ 8B1B4FFB2F659AD1005C246F /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */; };
+ 8B1B4FFD2F659AD2005C246F /* BodyPartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */; };
+ 8B1B4FFF2F659AD2005C246F /* BodyPartsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */; };
+ 8B1B50012F659AD3005C246F /* FoodCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B50002F659AD3005C246F /* FoodCard.swift */; };
+ 8B1B50032F659AD4005C246F /* FoodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B1B50022F659AD4005C246F /* FoodView.swift */; };
+ 8B57F7282F63BBB4001F6F5E /* TodayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7272F63BBB4001F6F5E /* TodayView.swift */; };
+ 8B57F72A2F63BBB5001F6F5E /* NumberCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */; };
+ 8B57F72C2F63BBB6001F6F5E /* NumbersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */; };
+ 8B57F72E2F63BBB7001F6F5E /* ColorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */; };
+ 8B57F7302F63BBB8001F6F5E /* ColorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */; };
+ 8B57F7322F63BBB9001F6F5E /* ShapeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */; };
+ 8B57F7342F63BBBA001F6F5E /* ShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */; };
+ 8B57F7362F63BBBA001F6F5E /* AnimalCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */; };
+ 8B57F7382F63BBBB001F6F5E /* AnimalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */; };
+ 8B57F73A2F63BBBD001F6F5E /* LetterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */; };
+ 8B57F73C2F63BBBD001F6F5E /* AlphabetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */; };
8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
+ 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherCard.swift; sourceTree = "<group>"; };
+ 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = "<group>"; };
+ 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyPartCard.swift; sourceTree = "<group>"; };
+ 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BodyPartsView.swift; sourceTree = "<group>"; };
+ 8B1B50002F659AD3005C246F /* FoodCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoodCard.swift; sourceTree = "<group>"; };
+ 8B1B50022F659AD4005C246F /* FoodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoodView.swift; sourceTree = "<group>"; };
+ 8B57F7272F63BBB4001F6F5E /* TodayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayView.swift; sourceTree = "<group>"; };
+ 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberCard.swift; sourceTree = "<group>"; };
+ 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumbersView.swift; sourceTree = "<group>"; };
+ 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorCard.swift; sourceTree = "<group>"; };
+ 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsView.swift; sourceTree = "<group>"; };
+ 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeCard.swift; sourceTree = "<group>"; };
+ 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesView.swift; sourceTree = "<group>"; };
+ 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalCard.swift; sourceTree = "<group>"; };
+ 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimalsView.swift; sourceTree = "<group>"; };
+ 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterCard.swift; sourceTree = "<group>"; };
+ 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetView.swift; sourceTree = "<group>"; };
8B7051B22F5F908800491D58 /* DayByDay.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DayByDay.app; sourceTree = BUILT_PRODUCTS_DIR; };
8B7051C02F5F90C800491D58 /* claude.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = claude.md; sourceTree = "<group>"; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ 8B5214E62F621E0200E1D35A /* Exceptions for "DayByDay" folder in "DayByDay" target */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = 8B7051B12F5F908800491D58 /* DayByDay */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
/* Begin PBXFileSystemSynchronizedRootGroup section */
8B7051B42F5F908800491D58 /* DayByDay */ = {
isa = PBXFileSystemSynchronizedRootGroup;
+ exceptions = (
+ 8B5214E62F621E0200E1D35A /* Exceptions for "DayByDay" folder in "DayByDay" target */,
+ );
path = DayByDay;
sourceTree = "<group>";
};
@@ -40,6 +87,23 @@
8B7051C02F5F90C800491D58 /* claude.md */,
8B7051B42F5F908800491D58 /* DayByDay */,
8B7051B32F5F908800491D58 /* Products */,
+ 8B57F7272F63BBB4001F6F5E /* TodayView.swift */,
+ 8B57F7292F63BBB5001F6F5E /* NumberCard.swift */,
+ 8B57F72B2F63BBB6001F6F5E /* NumbersView.swift */,
+ 8B57F72D2F63BBB7001F6F5E /* ColorCard.swift */,
+ 8B57F72F2F63BBB8001F6F5E /* ColorsView.swift */,
+ 8B57F7312F63BBB9001F6F5E /* ShapeCard.swift */,
+ 8B57F7332F63BBBA001F6F5E /* ShapesView.swift */,
+ 8B57F7352F63BBBA001F6F5E /* AnimalCard.swift */,
+ 8B57F7372F63BBBB001F6F5E /* AnimalsView.swift */,
+ 8B57F7392F63BBBD001F6F5E /* LetterCard.swift */,
+ 8B57F73B2F63BBBD001F6F5E /* AlphabetView.swift */,
+ 8B1B4FF82F659AD1005C246F /* WeatherCard.swift */,
+ 8B1B4FFA2F659AD1005C246F /* WeatherView.swift */,
+ 8B1B4FFC2F659AD2005C246F /* BodyPartCard.swift */,
+ 8B1B4FFE2F659AD2005C246F /* BodyPartsView.swift */,
+ 8B1B50002F659AD3005C246F /* FoodCard.swift */,
+ 8B1B50022F659AD4005C246F /* FoodView.swift */,
);
sourceTree = "<group>";
};
@@ -126,6 +190,23 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 8B1B50032F659AD4005C246F /* FoodView.swift in Sources */,
+ 8B57F72E2F63BBB7001F6F5E /* ColorCard.swift in Sources */,
+ 8B57F73C2F63BBBD001F6F5E /* AlphabetView.swift in Sources */,
+ 8B57F73A2F63BBBD001F6F5E /* LetterCard.swift in Sources */,
+ 8B57F7382F63BBBB001F6F5E /* AnimalsView.swift in Sources */,
+ 8B57F72C2F63BBB6001F6F5E /* NumbersView.swift in Sources */,
+ 8B57F72A2F63BBB5001F6F5E /* NumberCard.swift in Sources */,
+ 8B1B50012F659AD3005C246F /* FoodCard.swift in Sources */,
+ 8B1B4FF92F659AD1005C246F /* WeatherCard.swift in Sources */,
+ 8B1B4FFB2F659AD1005C246F /* WeatherView.swift in Sources */,
+ 8B1B4FFD2F659AD2005C246F /* BodyPartCard.swift in Sources */,
+ 8B1B4FFF2F659AD2005C246F /* BodyPartsView.swift in Sources */,
+ 8B57F7342F63BBBA001F6F5E /* ShapesView.swift in Sources */,
+ 8B57F7362F63BBBA001F6F5E /* AnimalCard.swift in Sources */,
+ 8B57F7322F63BBB9001F6F5E /* ShapeCard.swift in Sources */,
+ 8B57F7282F63BBB4001F6F5E /* TodayView.swift in Sources */,
+ 8B57F7302F63BBB8001F6F5E /* ColorsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -259,21 +340,21 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DayByDay/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -292,21 +373,21 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = DayByDay/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Day By Day";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
- INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/DayByDay/Assets.xcassets/LaunchBackground.colorset/Contents.json b/DayByDay/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 0000000..0425637
--- /dev/null
+++ b/DayByDay/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.000",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/DayByDay/ContentView.swift b/DayByDay/ContentView.swift
index c3ede7a..9924c5d 100644
--- a/DayByDay/ContentView.swift
+++ b/DayByDay/ContentView.swift
@@ -7,62 +7,180 @@
import SwiftUI
+// MARK: - Category model
+
+enum Category: Int, CaseIterable, Identifiable {
+ case today, days, months, seasons, numbers, colors, shapes, animals, alphabet,
+ weather, bodyParts, food
+
+ var id: Int { rawValue }
+
+ var label: String {
+ switch self {
+ case .today: "Today"
+ case .days: "Days"
+ case .months: "Months"
+ case .seasons: "Seasons"
+ case .numbers: "Numbers"
+ case .colors: "Colors"
+ case .shapes: "Shapes"
+ case .animals: "Animals"
+ case .alphabet: "Alphabet"
+ case .weather: "Weather"
+ case .bodyParts: "Body"
+ case .food: "Food"
+ }
+ }
+
+ var symbol: String {
+ switch self {
+ case .today: "sun.horizon.fill"
+ case .days: "calendar"
+ case .months: "calendar.badge.clock"
+ case .seasons: "leaf.fill"
+ case .numbers: "123.rectangle.fill"
+ case .colors: "paintpalette.fill"
+ case .shapes: "pentagon.fill"
+ case .animals: "pawprint.fill"
+ case .alphabet: "abc"
+ case .weather: "cloud.sun.rain.fill"
+ case .bodyParts: "figure.stand"
+ case .food: "carrot.fill"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .today: .orange
+ case .days: .blue
+ case .months: .purple
+ case .seasons: .green
+ case .numbers: .red
+ case .colors: .pink
+ case .shapes: .teal
+ case .animals: Color(red: 0.7, green: 0.45, blue: 0.2)
+ case .alphabet: .indigo
+ case .weather: Color(red: 0.4, green: 0.75, blue: 0.95)
+ case .bodyParts: Color(red: 0.9, green: 0.5, blue: 0.6)
+ case .food: Color(red: 0.95, green: 0.6, blue: 0.2)
+ }
+ }
+
+ /// Whether this tile uses a special gradient background instead of the solid color.
+ var usesGradientBackground: Bool {
+ self == .colors
+ }
+}
+
+// MARK: - Home grid tile
+
+struct CategoryTile: View {
+ let category: Category
+
+ var body: some View {
+ ZStack {
+ if category.usesGradientBackground {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(
+ LinearGradient(
+ colors: [.red, .orange, .yellow, .green, .blue, .purple],
+ startPoint: .leading,
+ endPoint: .trailing
+ )
+ )
+ .shadow(color: category.color.opacity(0.4), radius: 8, y: 4)
+ } else {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(category.color.gradient)
+ .shadow(color: category.color.opacity(0.4), radius: 8, y: 4)
+ }
+
+ VStack(spacing: 12) {
+ Image(systemName: category.symbol)
+ .font(.system(size: 48))
+ .foregroundStyle(.white)
+ .symbolRenderingMode(.hierarchical)
+
+ Text(category.label)
+ .font(.system(size: 24, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .accessibilityLabel(category.label)
+ }
+}
+
+// MARK: - Content view
+
struct ContentView: View {
- @State private var selectedTab = 0
@AppStorage("hasSeenVoiceTip") private var hasSeenVoiceTip = false
- private let tabs: [(label: String, symbol: String)] = [
- ("Days", "calendar"),
- ("Months", "calendar.badge.clock"),
- ("Seasons", "leaf.fill"),
+ private let columns = [
+ GridItem(.flexible(), spacing: 24),
+ GridItem(.flexible(), spacing: 24)
]
var body: some View {
- VStack(spacing: 0) {
- Group {
- switch selectedTab {
- case 0: DaysOfWeekView()
- case 1: MonthsOfYearView()
- default: SeasonsView()
- }
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
-
- Divider()
-
- // Bottom tab bar with large tap targets
- HStack(spacing: 0) {
- ForEach(Array(tabs.enumerated()), id: \.offset) { index, tab in
- Button {
- selectedTab = index
- } label: {
- VStack(spacing: 6) {
- Image(systemName: tab.symbol)
- .font(.system(size: 32))
- Text(tab.label)
- .font(.system(size: 18, weight: .semibold, design: .rounded))
+ NavigationStack {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(Category.allCases) { category in
+ NavigationLink(value: category) {
+ CategoryTile(category: category)
+ .aspectRatio(1, contentMode: .fit)
}
- .foregroundStyle(selectedTab == index ? Color.accentColor : Color.secondary)
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(24)
+
+ Button {
+ hasSeenVoiceTip = false
+ } label: {
+ Label("Voice Quality Tips", systemImage: "speaker.wave.2.fill")
+ .font(.system(size: 16, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .accessibilityLabel(tab.label)
+ .background(Color(.systemGray6))
+ .cornerRadius(16)
}
+ .buttonStyle(.plain)
+ .padding(.horizontal, 24)
+ .padding(.bottom, 24)
}
- .padding(.horizontal, 24)
- .padding(.bottom, 8)
- .background(.bar)
- }
- .overlay {
- if !hasSeenVoiceTip {
- VoiceTipOverlay {
- withAnimation { hasSeenVoiceTip = true }
+ .navigationTitle("DayByDay")
+ .navigationDestination(for: Category.self) { category in
+ destinationView(for: category)
+ .navigationTitle(category.label)
+ }
+ .overlay {
+ if !hasSeenVoiceTip {
+ VoiceTipOverlay {
+ withAnimation { hasSeenVoiceTip = true }
+ }
}
}
}
}
+
+ @ViewBuilder
+ private func destinationView(for category: Category) -> some View {
+ switch category {
+ case .today: TodayView()
+ case .days: DaysOfWeekView()
+ case .months: MonthsOfYearView()
+ case .seasons: SeasonsView()
+ case .numbers: NumbersView()
+ case .colors: ColorsView()
+ case .shapes: ShapesView()
+ case .animals: AnimalsView()
+ case .alphabet: AlphabetView()
+ case .weather: WeatherView()
+ case .bodyParts: BodyPartsView()
+ case .food: FoodView()
+ }
+ }
}
/// A one-time informational overlay for parents explaining how to download
@@ -83,7 +201,7 @@ struct VoiceTipOverlay: View {
Text("Better Voices Available")
.font(.system(size: 28, weight: .bold, design: .rounded))
- 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.")
+ 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.")
.font(.system(size: 18, design: .rounded))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
diff --git a/DayByDay/DayByDayApp.swift b/DayByDay/DayByDayApp.swift
index 2cce4db..860d747 100644
--- a/DayByDay/DayByDayApp.swift
+++ b/DayByDay/DayByDayApp.swift
@@ -5,10 +5,27 @@
// Created by cmc on 2026-03-09.
//
+import AVFoundation
import SwiftUI
@main
struct DayByDayApp: App {
+ init() {
+ Task.detached(priority: .background) {
+ // Activate the audio session early so the first real tap doesn't block.
+ try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
+ try? AVAudioSession.sharedInstance().setActive(true)
+
+ // Force singleton init (voice selection) and prime the TTS engine
+ // with a silent utterance so subsequent speaks are instant.
+ await MainActor.run {
+ let warmup = AVSpeechUtterance(string: "")
+ warmup.volume = 0
+ SpeechSynthesizer.shared.speakUtterance(warmup)
+ }
+ }
+ }
+
var body: some Scene {
WindowGroup {
ContentView()
diff --git a/DayByDay/Info.plist b/DayByDay/Info.plist
new file mode 100644
index 0000000..6aa377b
--- /dev/null
+++ b/DayByDay/Info.plist
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>UILaunchScreen</key>
+ <dict>
+ <key>UIColorName</key>
+ <string>LaunchBackground</string>
+ <key>UIImageName</key>
+ <string>AppIcon</string>
+ <key>UIImageRespectsSafeAreaInsets</key>
+ <false/>
+ </dict>
+</dict>
+</plist>
diff --git a/DayByDay/SpeechSynthesizer.swift b/DayByDay/SpeechSynthesizer.swift
index cac5caf..ab6421b 100644
--- a/DayByDay/SpeechSynthesizer.swift
+++ b/DayByDay/SpeechSynthesizer.swift
@@ -18,7 +18,16 @@ final class SpeechSynthesizer {
self.voice = Self.bestAvailableVoice()
}
+ /// Speaks a pre-configured utterance directly. Used for warmup at launch.
+ func speakUtterance(_ utterance: AVSpeechUtterance) {
+ synthesizer.speak(utterance)
+ }
+
func speak(_ text: String) {
+ // Ensure speech plays through the speaker even when the silent switch is on.
+ try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
+ try? AVAudioSession.sharedInstance().setActive(true)
+
if synthesizer.isSpeaking {
synthesizer.stopSpeaking(at: .immediate)
}
@@ -32,21 +41,19 @@ final class SpeechSynthesizer {
}
/// Picks the best en-US voice on the device.
- /// Preference order: premium > enhanced > default quality.
- /// Skips novelty voices so the child always hears a natural-sounding voice.
+ /// Filters for en-US, excludes novelty voices, then sorts by quality
+ /// descending so premium (3) > enhanced (2) > default (1).
private static func bestAvailableVoice() -> AVSpeechSynthesisVoice? {
- let candidates = AVSpeechSynthesisVoice.speechVoices().filter { voice in
- voice.language.hasPrefix("en-US")
- && !voice.voiceTraits.contains(.isNoveltyVoice)
- }
-
- if let premium = candidates.first(where: { $0.quality == .premium }) {
- return premium
- }
- if let enhanced = candidates.first(where: { $0.quality == .enhanced }) {
- return enhanced
- }
- // Fall back to the best default-quality voice.
- return candidates.first ?? AVSpeechSynthesisVoice(language: "en-US")
+ let best = AVSpeechSynthesisVoice.speechVoices()
+ .filter { voice in
+ voice.language.hasPrefix("en-US")
+ && !voice.voiceTraits.contains(.isNoveltyVoice)
+ }
+ .sorted { $0.quality.rawValue > $1.quality.rawValue }
+ .first
+
+ let selected = best ?? AVSpeechSynthesisVoice(language: "en-US")
+ print("[DayByDay] Selected voice: \(selected?.name ?? "nil"), quality: \(selected?.quality.rawValue ?? -1)")
+ return selected
}
}
diff --git a/FoodCard.swift b/FoodCard.swift
new file mode 100644
index 0000000..957137e
--- /dev/null
+++ b/FoodCard.swift
@@ -0,0 +1,115 @@
+//
+// FoodCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one fruit or vegetable.
+/// Tapping the card plays a bounce animation and speaks the food name aloud.
+/// Each food displays its emoji at large size for instant recognition.
+struct FoodCard: View {
+ let food: LearnFood
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(food.color.gradient)
+ .shadow(color: food.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 16) {
+ Text(food.emoji)
+ .font(.system(size: 72))
+
+ Text(food.name)
+ .font(.system(size: 28, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(food.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(food.name)
+ .accessibilityAddTraits(.isButton)
+ }
+
+}
+
+// MARK: - Food model
+
+enum LearnFood: Int, CaseIterable, Identifiable {
+ case apple, banana, carrot, strawberry, grapes, lemon,
+ broccoli, watermelon, cherry, orange, corn, pear
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .apple: "Apple"
+ case .banana: "Banana"
+ case .carrot: "Carrot"
+ case .strawberry: "Strawberry"
+ case .grapes: "Grapes"
+ case .lemon: "Lemon"
+ case .broccoli: "Broccoli"
+ case .watermelon: "Watermelon"
+ case .cherry: "Cherry"
+ case .orange: "Orange"
+ case .corn: "Corn"
+ case .pear: "Pear"
+ }
+ }
+
+ var emoji: String {
+ switch self {
+ case .apple: "🍎"
+ case .banana: "🍌"
+ case .carrot: "🥕"
+ case .strawberry: "🍓"
+ case .grapes: "🍇"
+ case .lemon: "🍋"
+ case .broccoli: "🥦"
+ case .watermelon: "🍉"
+ case .cherry: "🍒"
+ case .orange: "🍊"
+ case .corn: "🌽"
+ case .pear: "🍐"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .apple: Color(red: 0.85, green: 0.2, blue: 0.2) // red
+ case .banana: Color(red: 0.95, green: 0.8, blue: 0.2) // yellow
+ case .carrot: Color(red: 1.0, green: 0.55, blue: 0.15) // orange
+ case .strawberry: Color(red: 0.9, green: 0.25, blue: 0.3) // strawberry red
+ case .grapes: Color(red: 0.55, green: 0.3, blue: 0.7) // purple
+ case .lemon: Color(red: 0.9, green: 0.8, blue: 0.2) // lemon yellow
+ case .broccoli: Color(red: 0.3, green: 0.65, blue: 0.3) // green
+ case .watermelon: Color(red: 0.35, green: 0.7, blue: 0.4) // watermelon green
+ case .cherry: Color(red: 0.7, green: 0.15, blue: 0.2) // dark red
+ case .orange: Color(red: 1.0, green: 0.6, blue: 0.15) // orange
+ case .corn: Color(red: 0.9, green: 0.75, blue: 0.2) // golden yellow
+ case .pear: Color(red: 0.55, green: 0.75, blue: 0.3) // pear green
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ FoodCard(food: .apple)
+ .frame(width: 200, height: 260)
+ FoodCard(food: .carrot)
+ .frame(width: 200, height: 260)
+ }
+ .padding()
+}
diff --git a/FoodView.swift b/FoodView.swift
new file mode 100644
index 0000000..4ee09d2
--- /dev/null
+++ b/FoodView.swift
@@ -0,0 +1,29 @@
+//
+// FoodView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays all 12 fruits and vegetables as large, tappable cards in a scrollable grid.
+struct FoodView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 280), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnFood.allCases) { food in
+ FoodCard(food: food)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ FoodView()
+}
diff --git a/LetterCard.swift b/LetterCard.swift
new file mode 100644
index 0000000..06183ae
--- /dev/null
+++ b/LetterCard.swift
@@ -0,0 +1,84 @@
+//
+// LetterCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one letter of the alphabet.
+/// Tapping the card plays a bounce animation and speaks the letter aloud.
+struct LetterCard: View {
+ let letter: LearnLetter
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(letter.color.gradient)
+ .shadow(color: letter.color.opacity(0.4), radius: 8, y: 4)
+
+ Text(letter.character)
+ .font(.system(size: 64, weight: .heavy, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(letter.spokenName)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(letter.spokenName)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Letter model
+
+enum LearnLetter: Int, CaseIterable, Identifiable {
+ case a, b, c, d, e, f, g, h, i, j, k, l, m,
+ n, o, p, q, r, s, t, u, v, w, x, y, z
+
+ var id: Int { rawValue }
+
+ var character: String {
+ String(Character(UnicodeScalar(65 + rawValue)!))
+ }
+
+ /// Speak the letter name clearly for a child.
+ /// Lowercase prevents TTS from saying "Capital A".
+ var spokenName: String {
+ character.lowercased()
+ }
+
+ /// Cycle through 8 bright colors to give variety without unique-per-letter mapping.
+ var color: Color {
+ let palette: [Color] = [
+ Color(red: 0.9, green: 0.3, blue: 0.3), // red
+ Color(red: 1.0, green: 0.55, blue: 0.2), // orange
+ Color(red: 1.0, green: 0.8, blue: 0.2), // yellow
+ Color(red: 0.35, green: 0.75, blue: 0.4), // green
+ Color(red: 0.2, green: 0.65, blue: 0.9), // blue
+ Color(red: 0.55, green: 0.4, blue: 0.85), // purple
+ Color(red: 0.85, green: 0.4, blue: 0.6), // pink
+ Color(red: 0.2, green: 0.75, blue: 0.75), // teal
+ ]
+ return palette[rawValue % palette.count]
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ LetterCard(letter: .a)
+ .frame(width: 140, height: 160)
+ LetterCard(letter: .b)
+ .frame(width: 140, height: 160)
+ LetterCard(letter: .c)
+ .frame(width: 140, height: 160)
+ }
+ .padding()
+}
diff --git a/NumberCard.swift b/NumberCard.swift
new file mode 100644
index 0000000..9ae1c39
--- /dev/null
+++ b/NumberCard.swift
@@ -0,0 +1,136 @@
+//
+// NumberCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one number (1–10).
+/// Tapping the card plays a bounce animation and speaks the number name aloud.
+struct NumberCard: View {
+ let number: LearnNumber
+ var isHighlighted = false
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(number.color.gradient)
+ .shadow(color: number.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 10) {
+ Text("\(number.numeral)")
+ .font(.system(size: 60, weight: .heavy, design: .rounded))
+ .foregroundStyle(.white)
+
+ Text(number.name)
+ .font(.system(size: 28, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+
+ // Dot pattern showing the quantity
+ DotPattern(count: number.numeral)
+ .padding(.top, 4)
+ }
+ }
+ .overlay {
+ if isHighlighted {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .strokeBorder(.white, lineWidth: 5)
+ }
+ }
+ .shadow(color: isHighlighted ? .white.opacity(0.8) : .clear, radius: 12)
+ .scaleEffect(isTapped ? 1.12 : (isHighlighted ? 1.04 : 1.0))
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(number.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(number.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Number model
+
+enum LearnNumber: Int, CaseIterable, Identifiable {
+ case one = 1, two, three, four, five, six, seven, eight, nine, ten
+
+ var id: Int { rawValue }
+
+ /// Highlight the number matching today's day-of-month if it falls in 1–10.
+ static var current: LearnNumber? {
+ let day = Calendar.current.component(.day, from: Date())
+ return LearnNumber(rawValue: day)
+ }
+
+ var numeral: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .one: "One"
+ case .two: "Two"
+ case .three: "Three"
+ case .four: "Four"
+ case .five: "Five"
+ case .six: "Six"
+ case .seven: "Seven"
+ case .eight: "Eight"
+ case .nine: "Nine"
+ case .ten: "Ten"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .one: Color(red: 0.9, green: 0.3, blue: 0.3) // red
+ case .two: Color(red: 1.0, green: 0.55, blue: 0.2) // orange
+ case .three: Color(red: 1.0, green: 0.8, blue: 0.2) // yellow
+ case .four: Color(red: 0.35, green: 0.75, blue: 0.4) // green
+ case .five: Color(red: 0.2, green: 0.7, blue: 0.85) // sky blue
+ case .six: Color(red: 0.4, green: 0.45, blue: 0.9) // blue
+ case .seven: Color(red: 0.6, green: 0.4, blue: 0.85) // purple
+ case .eight: Color(red: 0.85, green: 0.35, blue: 0.6) // pink
+ case .nine: Color(red: 0.5, green: 0.75, blue: 0.45) // lime
+ case .ten: Color(red: 0.25, green: 0.8, blue: 0.75) // teal
+ }
+ }
+}
+
+// MARK: - Dot pattern
+
+/// Displays a grid of filled circles representing a quantity.
+/// 1–5 dots use centered rows; 6–10 use a 5-column grid that wraps naturally.
+struct DotPattern: View {
+ let count: Int
+
+ private var columns: [GridItem] {
+ let maxCols = count <= 5 ? min(count, 5) : 5
+ return Array(repeating: GridItem(.fixed(14), spacing: 6), count: maxCols)
+ }
+
+ var body: some View {
+ LazyVGrid(columns: columns, spacing: 6) {
+ ForEach(0..<count, id: \.self) { _ in
+ Circle()
+ .fill(.white.opacity(0.8))
+ .frame(width: 14, height: 14)
+ }
+ }
+ .frame(maxWidth: .infinity)
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ NumberCard(number: .three, isHighlighted: true)
+ .frame(width: 280, height: 300)
+ NumberCard(number: .seven)
+ .frame(width: 280, height: 300)
+ }
+ .padding()
+}
diff --git a/NumbersView.swift b/NumbersView.swift
new file mode 100644
index 0000000..b6d5de6
--- /dev/null
+++ b/NumbersView.swift
@@ -0,0 +1,33 @@
+//
+// NumbersView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays numbers 1–10 as large, tappable cards in a scrollable grid.
+/// Adapts between 1 and 2 columns depending on available width.
+struct NumbersView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnNumber.allCases) { number in
+ NumberCard(
+ number: number,
+ isHighlighted: number == .current
+ )
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ NumbersView()
+}
diff --git a/Screenshots/ipad.png b/Screenshots/ipad.png
index b840167..b68d40f 100644
--- a/Screenshots/ipad.png
+++ b/Screenshots/ipad.png
Binary files differ
diff --git a/Screenshots/iphone.png b/Screenshots/iphone.png
index b767322..0818b5c 100644
--- a/Screenshots/iphone.png
+++ b/Screenshots/iphone.png
Binary files differ
diff --git a/ShapeCard.swift b/ShapeCard.swift
new file mode 100644
index 0000000..ebc71e8
--- /dev/null
+++ b/ShapeCard.swift
@@ -0,0 +1,130 @@
+//
+// ShapeCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one shape.
+/// Tapping the card plays a bounce animation and speaks the shape name aloud.
+struct ShapeCard: View {
+ let shape: LearnShape
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(shape.color.gradient)
+ .shadow(color: shape.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 16) {
+ shape.shapeView
+ .frame(width: 80, height: 80)
+ .foregroundStyle(.white)
+
+ Text(shape.name)
+ .font(.system(size: 32, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(shape.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(shape.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Shape model
+
+enum LearnShape: Int, CaseIterable, Identifiable {
+ case circle, square, triangle, star, heart, oval, diamond, rectangle
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .circle: "Circle"
+ case .square: "Square"
+ case .triangle: "Triangle"
+ case .star: "Star"
+ case .heart: "Heart"
+ case .oval: "Oval"
+ case .diamond: "Diamond"
+ case .rectangle: "Rectangle"
+ }
+ }
+
+ @ViewBuilder
+ var shapeView: some View {
+ switch self {
+ case .circle:
+ Circle().fill(.white)
+ case .square:
+ SwiftUI.Rectangle().fill(.white)
+ case .triangle:
+ TriangleShape().fill(.white)
+ case .star:
+ Image(systemName: "star.fill")
+ .font(.system(size: 72))
+ .foregroundStyle(.white)
+ case .heart:
+ Image(systemName: "heart.fill")
+ .font(.system(size: 72))
+ .foregroundStyle(.white)
+ case .oval:
+ Ellipse().fill(.white)
+ .frame(width: 90, height: 60)
+ case .diamond:
+ SwiftUI.Rectangle().fill(.white)
+ .frame(width: 60, height: 60)
+ .rotationEffect(.degrees(45))
+ case .rectangle:
+ SwiftUI.Rectangle().fill(.white)
+ .frame(width: 90, height: 55)
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .circle: Color(red: 0.9, green: 0.3, blue: 0.3) // red
+ case .square: Color(red: 0.3, green: 0.5, blue: 0.9) // blue
+ case .triangle: Color(red: 0.35, green: 0.75, blue: 0.45) // green
+ case .star: Color(red: 1.0, green: 0.75, blue: 0.15) // gold
+ case .heart: Color(red: 0.9, green: 0.35, blue: 0.5) // pink
+ case .oval: Color(red: 0.55, green: 0.4, blue: 0.85) // purple
+ case .diamond: Color(red: 0.2, green: 0.75, blue: 0.8) // teal
+ case .rectangle: Color(red: 1.0, green: 0.55, blue: 0.2) // orange
+ }
+ }
+}
+
+/// A simple triangle drawn with a Path.
+struct TriangleShape: Shape {
+ func path(in rect: CGRect) -> Path {
+ Path { path in
+ path.move(to: CGPoint(x: rect.midX, y: rect.minY))
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
+ path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
+ path.closeSubpath()
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ ShapeCard(shape: .triangle)
+ .frame(width: 200, height: 260)
+ ShapeCard(shape: .star)
+ .frame(width: 200, height: 260)
+ }
+ .padding()
+}
diff --git a/ShapesView.swift b/ShapesView.swift
new file mode 100644
index 0000000..347dbad
--- /dev/null
+++ b/ShapesView.swift
@@ -0,0 +1,30 @@
+//
+// ShapesView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays 8 shapes as large, tappable cards in a scrollable grid.
+/// Adapts between 1 and 2 columns depending on available width.
+struct ShapesView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnShape.allCases) { shape in
+ ShapeCard(shape: shape)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ ShapesView()
+}
diff --git a/TodayView.swift b/TodayView.swift
new file mode 100644
index 0000000..4fa0492
--- /dev/null
+++ b/TodayView.swift
@@ -0,0 +1,84 @@
+//
+// TodayView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single-screen summary of today: day of the week, date, month, and season.
+/// The child taps the large card to hear "Today is [day], [month] [date]. It is [season]."
+struct TodayView: View {
+ @State private var isTapped = false
+
+ private var todayText: String {
+ let day = DayOfWeek.current.name
+ let month = MonthOfYear.current.name
+ let date = Calendar.current.component(.day, from: Date())
+ let season = Season.current.name
+ return "Today is \(day), \(month) \(date). It is \(season)."
+ }
+
+ var body: some View {
+ ScrollView {
+ VStack(spacing: 32) {
+ // Main today card
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(Color.orange.gradient)
+ .shadow(color: Color.orange.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 20) {
+ Image(systemName: "sun.horizon.fill")
+ .font(.system(size: 80))
+ .foregroundStyle(.white)
+ .symbolRenderingMode(.hierarchical)
+
+ // Day of the week
+ Text(DayOfWeek.current.name)
+ .font(.system(size: 48, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+
+ // Date number
+ let dayNumber = Calendar.current.component(.day, from: Date())
+ Text("\(dayNumber)")
+ .font(.system(size: 72, weight: .heavy, design: .rounded))
+ .foregroundStyle(.white.opacity(0.9))
+
+ // Month and season
+ HStack(spacing: 24) {
+ Label(MonthOfYear.current.name, systemImage: MonthOfYear.current.symbol)
+ Label(Season.current.name, systemImage: Season.current.symbol)
+ }
+ .font(.system(size: 24, weight: .semibold, design: .rounded))
+ .foregroundStyle(.white.opacity(0.9))
+ }
+ .padding(.vertical, 32)
+ }
+ .frame(height: 420)
+ .scaleEffect(isTapped ? 1.08 : 1.0)
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(todayText)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(todayText)
+ .accessibilityAddTraits(.isButton)
+
+ // Tap hint
+ Label("Tap to hear today", systemImage: "speaker.wave.2.fill")
+ .font(.system(size: 22, weight: .medium, design: .rounded))
+ .foregroundStyle(.secondary)
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ TodayView()
+}
diff --git a/WeatherCard.swift b/WeatherCard.swift
new file mode 100644
index 0000000..8b12786
--- /dev/null
+++ b/WeatherCard.swift
@@ -0,0 +1,102 @@
+//
+// WeatherCard.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// A single large, colorful card representing one weather type.
+/// Tapping the card plays a bounce animation and speaks the weather name aloud.
+struct WeatherCard: View {
+ let weather: LearnWeather
+ @State private var isTapped = false
+
+ var body: some View {
+ ZStack {
+ RoundedRectangle(cornerRadius: 32, style: .continuous)
+ .fill(weather.color.gradient)
+ .shadow(color: weather.color.opacity(0.4), radius: 8, y: 4)
+
+ VStack(spacing: 16) {
+ Image(systemName: weather.symbol)
+ .font(.system(size: 64))
+ .foregroundStyle(.white)
+ .symbolRenderingMode(.hierarchical)
+
+ Text(weather.name)
+ .font(.system(size: 32, weight: .bold, design: .rounded))
+ .foregroundStyle(.white)
+ }
+ }
+ .scaleEffect(isTapped ? 1.12 : 1.0)
+ .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
+ .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
+ .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
+ .onTapGesture {
+ isTapped = true
+ SpeechSynthesizer.shared.speak(weather.name)
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
+ isTapped = false
+ }
+ }
+ .accessibilityLabel(weather.name)
+ .accessibilityAddTraits(.isButton)
+ }
+}
+
+// MARK: - Weather model
+
+enum LearnWeather: Int, CaseIterable, Identifiable {
+ case sunny, cloudy, rainy, snowy, windy, stormy, foggy, rainbow
+
+ var id: Int { rawValue }
+
+ var name: String {
+ switch self {
+ case .sunny: "Sunny"
+ case .cloudy: "Cloudy"
+ case .rainy: "Rainy"
+ case .snowy: "Snowy"
+ case .windy: "Windy"
+ case .stormy: "Stormy"
+ case .foggy: "Foggy"
+ case .rainbow: "Rainbow"
+ }
+ }
+
+ var symbol: String {
+ switch self {
+ case .sunny: "sun.max.fill"
+ case .cloudy: "cloud.fill"
+ case .rainy: "cloud.rain.fill"
+ case .snowy: "cloud.snow.fill"
+ case .windy: "wind"
+ case .stormy: "cloud.bolt.rain.fill"
+ case .foggy: "cloud.fog.fill"
+ case .rainbow: "rainbow"
+ }
+ }
+
+ var color: Color {
+ switch self {
+ case .sunny: Color(red: 1.0, green: 0.7, blue: 0.2) // orange
+ case .cloudy: Color(red: 0.6, green: 0.6, blue: 0.65) // gray
+ case .rainy: Color(red: 0.3, green: 0.6, blue: 0.9) // blue
+ case .snowy: Color(red: 0.55, green: 0.8, blue: 0.95) // light blue
+ case .windy: Color(red: 0.25, green: 0.7, blue: 0.7) // teal
+ case .stormy: Color(red: 0.2, green: 0.3, blue: 0.6) // dark blue
+ case .foggy: Color(red: 0.6, green: 0.5, blue: 0.7) // muted purple
+ case .rainbow: Color(red: 0.9, green: 0.45, blue: 0.6) // pink
+ }
+ }
+}
+
+#Preview {
+ HStack(spacing: 24) {
+ WeatherCard(weather: .sunny)
+ .frame(width: 200, height: 260)
+ WeatherCard(weather: .stormy)
+ .frame(width: 200, height: 260)
+ }
+ .padding()
+}
diff --git a/WeatherView.swift b/WeatherView.swift
new file mode 100644
index 0000000..e3b848b
--- /dev/null
+++ b/WeatherView.swift
@@ -0,0 +1,29 @@
+//
+// WeatherView.swift
+// DayByDay
+//
+
+import SwiftUI
+
+/// Displays all 8 weather types as large, tappable cards in a scrollable grid.
+struct WeatherView: View {
+ private let columns = [
+ GridItem(.adaptive(minimum: 280), spacing: 24)
+ ]
+
+ var body: some View {
+ ScrollView {
+ LazyVGrid(columns: columns, spacing: 24) {
+ ForEach(LearnWeather.allCases) { weather in
+ WeatherCard(weather: weather)
+ .frame(height: 200)
+ }
+ }
+ .padding(32)
+ }
+ }
+}
+
+#Preview {
+ WeatherView()
+}
diff --git a/claude.md b/claude.md
index ae1f595..4be8e25 100644
--- a/claude.md
+++ b/claude.md
@@ -1,8 +1,9 @@
# DayByDay – Project Context
## What this is
-An iPad app for pre-K children (ages 4–5) that teaches days of the week, months
-of the year, and seasons through simple, tap-based interactions.
+An iOS app for pre-K children (ages 4–5) that teaches days of the week, months
+of the year, seasons, numbers, colors, shapes, animals, the alphabet, weather,
+body parts, and fruits & vegetables through simple, tap-based interactions.
## Target user
A single child, approximately 4–5 years old, preparing to start school. Cannot
@@ -13,15 +14,28 @@ independently.
- Large tap targets — nothing small or fiddly
- Bright, clean colors — simple and friendly, not cluttered
- No text-dependent interactions — the child cannot read
-- Audio is the primary feedback mechanism (day/month/season name read aloud on
- tap)
+- Audio is the primary feedback mechanism (names read aloud on tap)
- Simple animations on interaction — enough to delight, not enough to distract
- No timers, no failure states, no scores — purely exploratory
-## Content
+## Content (12 categories, accessible from a NavigationStack home grid)
+- Today: summary card with current day, date, month, season
- Days of the week: Sunday through Saturday
- Months of the year: January through December
- Seasons: Spring, Summer, Fall, Winter
+- Numbers: 1 through 10 (numeral + word + dot pattern)
+- Colors: 10 basic colors (Red, Orange, Yellow, Green, Blue, Purple, Pink,
+ Brown, Black, White)
+- Shapes: 8 shapes (Circle, Square, Triangle, Star, Heart, Oval, Diamond,
+ Rectangle)
+- Animals: 12 animals with SF Symbol icons
+- Alphabet: A through Z
+- Weather: 8 weather types (Sunny, Cloudy, Rainy, Snowy, Windy, Stormy,
+ Foggy, Rainbow)
+- Body Parts: 12 body parts (Head, Eyes, Ears, Nose, Mouth, Hands, Fingers,
+ Arms, Legs, Feet, Heart, Belly)
+- Fruits & Vegetables: 12 foods (Apple, Banana, Carrot, Strawberry, Grapes,
+ Lemon, Broccoli, Watermelon, Cherry, Orange, Corn, Pear)
## Technical constraints
- SwiftUI, iOS only
@@ -30,6 +44,7 @@ independently.
- Targets latest iOS
## What good looks like
-Each concept (day, month, season) should feel like its own tactile card or
-object the child can tap and explore. Transitions should be smooth. Audio should
-play immediately on tap with no perceptible delay.
+Each concept should feel like its own tactile card or object the child can tap
+and explore. Transitions should be smooth. Audio should play immediately on tap
+with no perceptible delay. The home screen is a colorful 2-column grid of square category
+tiles that navigate into each section.