diff options
| author | Christian Cleberg <[email protected]> | 2026-03-09 20:30:56 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-09 20:31:43 -0500 |
| commit | 83479f9840f230c0c0dbee083b75735090ae9322 (patch) | |
| tree | f4c79dc64d3308df30965b72f66882d27faa022c | |
| parent | 84f1d5479a9fa884fd3d9119c5edcc4ca42810b5 (diff) | |
| download | daybyday-83479f9840f230c0c0dbee083b75735090ae9322.tar.gz daybyday-83479f9840f230c0c0dbee083b75735090ae9322.tar.bz2 daybyday-83479f9840f230c0c0dbee083b75735090ae9322.zip | |
initial 1.0 release
| -rw-r--r-- | DayByDay.xcodeproj/project.pbxproj | 346 | ||||
| -rw-r--r-- | DayByDay.xcodeproj/project.xcworkspace/contents.xcworkspacedata | 7 | ||||
| -rw-r--r-- | DayByDay.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist | 14 | ||||
| -rw-r--r-- | DayByDay/Assets.xcassets/AccentColor.colorset/Contents.json | 15 | ||||
| -rw-r--r-- | DayByDay/Assets.xcassets/AppIcon.appiconset/Contents.json | 37 | ||||
| -rw-r--r-- | DayByDay/Assets.xcassets/AppIcon.appiconset/logo_1024.png | bin | 0 -> 156349 bytes | |||
| -rw-r--r-- | DayByDay/Assets.xcassets/AppIcon.appiconset/logo_dark_1024.png | bin | 0 -> 164404 bytes | |||
| -rw-r--r-- | DayByDay/Assets.xcassets/Contents.json | 6 | ||||
| -rw-r--r-- | DayByDay/ContentView.swift | 110 | ||||
| -rw-r--r-- | DayByDay/DayByDayApp.swift | 17 | ||||
| -rw-r--r-- | DayByDay/DayCard.swift | 117 | ||||
| -rw-r--r-- | DayByDay/DaysOfWeekView.swift | 30 | ||||
| -rw-r--r-- | DayByDay/MonthCard.swift | 130 | ||||
| -rw-r--r-- | DayByDay/MonthsOfYearView.swift | 30 | ||||
| -rw-r--r-- | DayByDay/SeasonCard.swift | 111 | ||||
| -rw-r--r-- | DayByDay/SeasonsView.swift | 30 | ||||
| -rw-r--r-- | DayByDay/SpeechSynthesizer.swift | 52 | ||||
| -rw-r--r-- | claude.md | 29 |
18 files changed, 1081 insertions, 0 deletions
diff --git a/DayByDay.xcodeproj/project.pbxproj b/DayByDay.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3ff18e9 --- /dev/null +++ b/DayByDay.xcodeproj/project.pbxproj @@ -0,0 +1,346 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 8B7051C12F5F90CD00491D58 /* claude.md in Resources */ = {isa = PBXBuildFile; fileRef = 8B7051C02F5F90C800491D58 /* claude.md */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 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 PBXFileSystemSynchronizedRootGroup section */ + 8B7051B42F5F908800491D58 /* DayByDay */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DayByDay; + sourceTree = "<group>"; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8B7051AF2F5F908800491D58 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8B7051A92F5F908800491D58 = { + isa = PBXGroup; + children = ( + 8B7051C02F5F90C800491D58 /* claude.md */, + 8B7051B42F5F908800491D58 /* DayByDay */, + 8B7051B32F5F908800491D58 /* Products */, + ); + sourceTree = "<group>"; + }; + 8B7051B32F5F908800491D58 /* Products */ = { + isa = PBXGroup; + children = ( + 8B7051B22F5F908800491D58 /* DayByDay.app */, + ); + name = Products; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8B7051B12F5F908800491D58 /* DayByDay */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8B7051BD2F5F908900491D58 /* Build configuration list for PBXNativeTarget "DayByDay" */; + buildPhases = ( + 8B7051AE2F5F908800491D58 /* Sources */, + 8B7051AF2F5F908800491D58 /* Frameworks */, + 8B7051B02F5F908800491D58 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 8B7051B42F5F908800491D58 /* DayByDay */, + ); + name = DayByDay; + packageProductDependencies = ( + ); + productName = DayByDay; + productReference = 8B7051B22F5F908800491D58 /* DayByDay.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B7051AA2F5F908800491D58 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2630; + LastUpgradeCheck = 2630; + TargetAttributes = { + 8B7051B12F5F908800491D58 = { + CreatedOnToolsVersion = 26.3; + }; + }; + }; + buildConfigurationList = 8B7051AD2F5F908800491D58 /* Build configuration list for PBXProject "DayByDay" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B7051A92F5F908800491D58; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 8B7051B32F5F908800491D58 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8B7051B12F5F908800491D58 /* DayByDay */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8B7051B02F5F908800491D58 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8B7051C12F5F90CD00491D58 /* claude.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8B7051AE2F5F908800491D58 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8B7051BB2F5F908900491D58 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8B7051BC2F5F908900491D58 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8B7051BE2F5F908900491D58 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + 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; + PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8B7051BF2F5F908900491D58 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZCNAX3VL9D; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + 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; + PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B7051AD2F5F908800491D58 /* Build configuration list for PBXProject "DayByDay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B7051BB2F5F908900491D58 /* Debug */, + 8B7051BC2F5F908900491D58 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8B7051BD2F5F908900491D58 /* Build configuration list for PBXNativeTarget "DayByDay" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B7051BE2F5F908900491D58 /* Debug */, + 8B7051BF2F5F908900491D58 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B7051AA2F5F908800491D58 /* Project object */; +} diff --git a/DayByDay.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/DayByDay.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/DayByDay.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:"> + </FileRef> +</Workspace> diff --git a/DayByDay.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist b/DayByDay.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..7d731a2 --- /dev/null +++ b/DayByDay.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ +<?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>SchemeUserState</key> + <dict> + <key>DayByDay.xcscheme_^#shared#^_</key> + <dict> + <key>orderHint</key> + <integer>0</integer> + </dict> + </dict> +</dict> +</plist> diff --git a/DayByDay/Assets.xcassets/AccentColor.colorset/Contents.json b/DayByDay/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..d90b065 --- /dev/null +++ b/DayByDay/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "color" : { + "platform" : "universal", + "reference" : "systemOrangeColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DayByDay/Assets.xcassets/AppIcon.appiconset/Contents.json b/DayByDay/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..da79705 --- /dev/null +++ b/DayByDay/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,37 @@ +{ + "images" : [ + { + "filename" : "logo_1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo_dark_1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_1024.png b/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_1024.png Binary files differnew file mode 100644 index 0000000..efbe0ea --- /dev/null +++ b/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_1024.png diff --git a/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_dark_1024.png b/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_dark_1024.png Binary files differnew file mode 100644 index 0000000..8ec4266 --- /dev/null +++ b/DayByDay/Assets.xcassets/AppIcon.appiconset/logo_dark_1024.png diff --git a/DayByDay/Assets.xcassets/Contents.json b/DayByDay/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DayByDay/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DayByDay/ContentView.swift b/DayByDay/ContentView.swift new file mode 100644 index 0000000..c3ede7a --- /dev/null +++ b/DayByDay/ContentView.swift @@ -0,0 +1,110 @@ +// +// ContentView.swift +// DayByDay +// +// Created by cmc on 2026-03-09. +// + +import SwiftUI + +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"), + ] + + 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)) + } + .foregroundStyle(selectedTab == index ? Color.accentColor : Color.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(tab.label) + } + } + .padding(.horizontal, 24) + .padding(.bottom, 8) + .background(.bar) + } + .overlay { + if !hasSeenVoiceTip { + VoiceTipOverlay { + withAnimation { hasSeenVoiceTip = true } + } + } + } + } +} + +/// A one-time informational overlay for parents explaining how to download +/// a higher-quality voice for the best experience. Dismissed with a single tap. +struct VoiceTipOverlay: View { + let onDismiss: () -> Void + + var body: some View { + ZStack { + Color.black.opacity(0.4) + .ignoresSafeArea() + + VStack(spacing: 20) { + Image(systemName: "speaker.wave.3.fill") + .font(.system(size: 48)) + .foregroundStyle(.blue) + + 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.") + .font(.system(size: 18, design: .rounded)) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + + Text("Tap anywhere to dismiss") + .font(.system(size: 16, weight: .medium, design: .rounded)) + .foregroundStyle(.tertiary) + .padding(.top, 8) + } + .padding(40) + .frame(maxWidth: 520) + .background( + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(.regularMaterial) + ) + } + .contentShape(Rectangle()) + .onTapGesture(perform: onDismiss) + } +} + +#Preview { + ContentView() +} diff --git a/DayByDay/DayByDayApp.swift b/DayByDay/DayByDayApp.swift new file mode 100644 index 0000000..2cce4db --- /dev/null +++ b/DayByDay/DayByDayApp.swift @@ -0,0 +1,17 @@ +// +// DayByDayApp.swift +// DayByDay +// +// Created by cmc on 2026-03-09. +// + +import SwiftUI + +@main +struct DayByDayApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/DayByDay/DayCard.swift b/DayByDay/DayCard.swift new file mode 100644 index 0000000..4ee3614 --- /dev/null +++ b/DayByDay/DayCard.swift @@ -0,0 +1,117 @@ +// +// DayCard.swift +// DayByDay +// + +import SwiftUI + +/// A single large, colorful card representing one day of the week. +/// Tapping the card plays a bounce animation and speaks the day name aloud. +struct DayCard: View { + let day: DayOfWeek + var isHighlighted = false + @State private var isTapped = false + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(day.color.gradient) + .shadow(color: day.color.opacity(0.4), radius: 8, y: 4) + + VStack(spacing: 16) { + Image(systemName: day.symbol) + .font(.system(size: 64)) + .foregroundStyle(.white) + .symbolRenderingMode(.hierarchical) + + Text(day.name) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + } + .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(day.name) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + isTapped = false + } + } + .accessibilityLabel(day.name) + .accessibilityAddTraits(.isButton) + } +} + +// MARK: - Day model + +enum DayOfWeek: Int, CaseIterable, Identifiable { + case sunday, monday, tuesday, wednesday, thursday, friday, saturday + + var id: Int { rawValue } + + /// The day of the week for today's date. + static var current: DayOfWeek { + // Calendar weekday: 1 = Sunday, 7 = Saturday + let weekday = Calendar.current.component(.weekday, from: Date()) + return DayOfWeek(rawValue: weekday - 1)! + } + + var name: String { + switch self { + case .sunday: "Sunday" + case .monday: "Monday" + case .tuesday: "Tuesday" + case .wednesday: "Wednesday" + case .thursday: "Thursday" + case .friday: "Friday" + case .saturday: "Saturday" + } + } + + /// A recognisable SF Symbol icon for each day so children who cannot read + /// still get a unique visual cue. + var symbol: String { + switch self { + case .sunday: "sun.max.fill" + case .monday: "moon.fill" + case .tuesday: "star.fill" + case .wednesday: "cloud.fill" + case .thursday: "bolt.fill" + case .friday: "heart.fill" + case .saturday: "sparkles" + } + } + + /// Bright, child-friendly colour for each card. + var color: Color { + switch self { + case .sunday: Color(red: 1.0, green: 0.6, blue: 0.2) // orange + case .monday: Color(red: 0.35, green: 0.5, blue: 0.9) // blue + case .tuesday: Color(red: 0.9, green: 0.3, blue: 0.4) // red-pink + case .wednesday: Color(red: 0.3, green: 0.75, blue: 0.45) // green + case .thursday: Color(red: 0.55, green: 0.4, blue: 0.9) // purple + case .friday: Color(red: 1.0, green: 0.8, blue: 0.2) // yellow + case .saturday: Color(red: 0.2, green: 0.8, blue: 0.8) // teal + } + } +} + +#Preview { + HStack(spacing: 24) { + DayCard(day: .wednesday, isHighlighted: true) + .frame(width: 200, height: 260) + DayCard(day: .thursday) + .frame(width: 200, height: 260) + } + .padding() +} diff --git a/DayByDay/DaysOfWeekView.swift b/DayByDay/DaysOfWeekView.swift new file mode 100644 index 0000000..aed32cb --- /dev/null +++ b/DayByDay/DaysOfWeekView.swift @@ -0,0 +1,30 @@ +// +// DaysOfWeekView.swift +// DayByDay +// + +import SwiftUI + +/// Displays all seven days of the week as large, tappable cards in a scrollable grid. +/// Adapts between 1 and 2 columns depending on available width. +struct DaysOfWeekView: View { + private let columns = [ + GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24) + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(DayOfWeek.allCases) { day in + DayCard(day: day, isHighlighted: day == .current) + .frame(height: 200) + } + } + .padding(32) + } + } +} + +#Preview { + DaysOfWeekView() +} diff --git a/DayByDay/MonthCard.swift b/DayByDay/MonthCard.swift new file mode 100644 index 0000000..f462fd4 --- /dev/null +++ b/DayByDay/MonthCard.swift @@ -0,0 +1,130 @@ +// +// MonthCard.swift +// DayByDay +// + +import SwiftUI + +/// A single large, colorful card representing one month of the year. +/// Tapping the card plays a bounce animation and speaks the month name aloud. +struct MonthCard: View { + let month: MonthOfYear + var isHighlighted = false + @State private var isTapped = false + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(month.color.gradient) + .shadow(color: month.color.opacity(0.4), radius: 8, y: 4) + + VStack(spacing: 16) { + Image(systemName: month.symbol) + .font(.system(size: 64)) + .foregroundStyle(.white) + .symbolRenderingMode(.hierarchical) + + Text(month.name) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + } + .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(month.name) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + isTapped = false + } + } + .accessibilityLabel(month.name) + .accessibilityAddTraits(.isButton) + } +} + +// MARK: - Month model + +enum MonthOfYear: Int, CaseIterable, Identifiable { + case january, february, march, april, may, june, + july, august, september, october, november, december + + var id: Int { rawValue } + + /// The month for today's date. + static var current: MonthOfYear { + // Calendar month: 1 = January, 12 = December + let month = Calendar.current.component(.month, from: Date()) + return MonthOfYear(rawValue: month - 1)! + } + + var name: String { + switch self { + case .january: "January" + case .february: "February" + case .march: "March" + case .april: "April" + case .may: "May" + case .june: "June" + case .july: "July" + case .august: "August" + case .september: "September" + case .october: "October" + case .november: "November" + case .december: "December" + } + } + + var symbol: String { + switch self { + case .january: "snowflake" + case .february: "heart.fill" + case .march: "wind" + case .april: "cloud.rain.fill" + case .may: "leaf.fill" + case .june: "sun.max.fill" + case .july: "fireworks" + case .august: "beach.umbrella.fill" + case .september: "book.fill" + case .october: "moon.stars.fill" + case .november: "fork.knife" + case .december: "gift.fill" + } + } + + var color: Color { + switch self { + case .january: Color(red: 0.55, green: 0.75, blue: 0.95) // ice blue + case .february: Color(red: 0.9, green: 0.35, blue: 0.5) // pink + case .march: Color(red: 0.45, green: 0.8, blue: 0.55) // spring green + case .april: Color(red: 0.5, green: 0.65, blue: 0.9) // rain blue + case .may: Color(red: 0.95, green: 0.7, blue: 0.2) // golden + case .june: Color(red: 1.0, green: 0.55, blue: 0.25) // warm orange + case .july: Color(red: 0.85, green: 0.25, blue: 0.3) // red + case .august: Color(red: 0.2, green: 0.75, blue: 0.8) // ocean teal + case .september: Color(red: 0.6, green: 0.45, blue: 0.85) // purple + case .october: Color(red: 0.9, green: 0.5, blue: 0.15) // pumpkin + case .november: Color(red: 0.7, green: 0.5, blue: 0.3) // brown + case .december: Color(red: 0.3, green: 0.5, blue: 0.85) // winter blue + } + } +} + +#Preview { + HStack(spacing: 24) { + MonthCard(month: .march, isHighlighted: true) + .frame(width: 200, height: 260) + MonthCard(month: .april) + .frame(width: 200, height: 260) + } + .padding() +} diff --git a/DayByDay/MonthsOfYearView.swift b/DayByDay/MonthsOfYearView.swift new file mode 100644 index 0000000..fb8e002 --- /dev/null +++ b/DayByDay/MonthsOfYearView.swift @@ -0,0 +1,30 @@ +// +// MonthsOfYearView.swift +// DayByDay +// + +import SwiftUI + +/// Displays all twelve months as large, tappable cards in a scrollable grid. +/// Adapts between 1 and 2 columns depending on available width. +struct MonthsOfYearView: View { + private let columns = [ + GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24) + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(MonthOfYear.allCases) { month in + MonthCard(month: month, isHighlighted: month == .current) + .frame(height: 200) + } + } + .padding(32) + } + } +} + +#Preview { + MonthsOfYearView() +} diff --git a/DayByDay/SeasonCard.swift b/DayByDay/SeasonCard.swift new file mode 100644 index 0000000..c84dfb6 --- /dev/null +++ b/DayByDay/SeasonCard.swift @@ -0,0 +1,111 @@ +// +// SeasonCard.swift +// DayByDay +// + +import SwiftUI + +/// A single large, colorful card representing one season. +/// Tapping the card plays a bounce animation and speaks the season name aloud. +struct SeasonCard: View { + let season: Season + var isHighlighted = false + @State private var isTapped = false + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 32, style: .continuous) + .fill(season.color.gradient) + .shadow(color: season.color.opacity(0.4), radius: 8, y: 4) + + VStack(spacing: 16) { + Image(systemName: season.symbol) + .font(.system(size: 64)) + .foregroundStyle(.white) + .symbolRenderingMode(.hierarchical) + + Text(season.name) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + } + .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(season.name) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + isTapped = false + } + } + .accessibilityLabel(season.name) + .accessibilityAddTraits(.isButton) + } +} + +// MARK: - Season model + +enum Season: Int, CaseIterable, Identifiable { + case spring, summer, fall, winter + + var id: Int { rawValue } + + /// The season for today's date. + /// Spring: March–May, Summer: June–August, + /// Fall: September–November, Winter: December–February. + static var current: Season { + let month = Calendar.current.component(.month, from: Date()) + switch month { + case 3...5: return .spring + case 6...8: return .summer + case 9...11: return .fall + default: return .winter + } + } + + var name: String { + switch self { + case .spring: "Spring" + case .summer: "Summer" + case .fall: "Fall" + case .winter: "Winter" + } + } + + var symbol: String { + switch self { + case .spring: "leaf.fill" + case .summer: "sun.max.fill" + case .fall: "wind" + case .winter: "snowflake" + } + } + + var color: Color { + switch self { + case .spring: Color(red: 0.45, green: 0.8, blue: 0.5) // fresh green + case .summer: Color(red: 1.0, green: 0.6, blue: 0.15) // sunny orange + case .fall: Color(red: 0.85, green: 0.45, blue: 0.2) // autumn orange + case .winter: Color(red: 0.4, green: 0.65, blue: 0.95) // cool blue + } + } +} + +#Preview { + HStack(spacing: 24) { + SeasonCard(season: .spring, isHighlighted: true) + .frame(width: 200, height: 260) + SeasonCard(season: .summer) + .frame(width: 200, height: 260) + } + .padding() +} diff --git a/DayByDay/SeasonsView.swift b/DayByDay/SeasonsView.swift new file mode 100644 index 0000000..848bb13 --- /dev/null +++ b/DayByDay/SeasonsView.swift @@ -0,0 +1,30 @@ +// +// SeasonsView.swift +// DayByDay +// + +import SwiftUI + +/// Displays all four seasons as large, tappable cards in a scrollable grid. +/// Adapts between 1 and 2 columns depending on available width. +struct SeasonsView: View { + private let columns = [ + GridItem(.adaptive(minimum: 300, maximum: 500), spacing: 24) + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: columns, spacing: 24) { + ForEach(Season.allCases) { season in + SeasonCard(season: season, isHighlighted: season == .current) + .frame(height: 200) + } + } + .padding(32) + } + } +} + +#Preview { + SeasonsView() +} diff --git a/DayByDay/SpeechSynthesizer.swift b/DayByDay/SpeechSynthesizer.swift new file mode 100644 index 0000000..cac5caf --- /dev/null +++ b/DayByDay/SpeechSynthesizer.swift @@ -0,0 +1,52 @@ +// +// SpeechSynthesizer.swift +// DayByDay +// + +import AVFoundation + +/// Wrapper around AVSpeechSynthesizer that selects the highest-quality +/// en-US neural voice available on the device. Falls back gracefully if +/// premium/enhanced voices haven't been downloaded yet. +final class SpeechSynthesizer { + static let shared = SpeechSynthesizer() + + private let synthesizer = AVSpeechSynthesizer() + private let voice: AVSpeechSynthesisVoice? + + private init() { + self.voice = Self.bestAvailableVoice() + } + + func speak(_ text: String) { + if synthesizer.isSpeaking { + synthesizer.stopSpeaking(at: .immediate) + } + + let utterance = AVSpeechUtterance(string: text) + utterance.voice = voice + utterance.rate = 0.4 + utterance.pitchMultiplier = 1.0 + utterance.preUtteranceDelay = 0 + synthesizer.speak(utterance) + } + + /// 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. + 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") + } +} diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..6e01587 --- /dev/null +++ b/claude.md @@ -0,0 +1,29 @@ +# 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. + +## Target user +A single child, approximately 4–5 years old, preparing to start school. Cannot read yet. Parent may sit alongside but the child should be able to use the app independently. + +## Design principles +- 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) +- Simple animations on interaction — enough to delight, not enough to distract +- No timers, no failure states, no scores — purely exploratory + +## Content +- Days of the week: Sunday through Saturday +- Months of the year: January through December +- Seasons: Spring, Summer, Fall, Winter + +## Technical constraints +- SwiftUI, iPad only +- Fully offline — no network requests, no accounts, no analytics +- No in-app purchases +- Targets latest iPadOS + +## 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. |
