krz/daybyday

iOS app for children to learn days of the week, months, and seasons with audio assist.

clone: git clone https://gitbay.org/krz/daybyday.git

v1.3.2: BodyPartCard.swift · raw

  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}