krz/daybyday

iOS app for children to learn days of the week, months, and seasons with audio assist. education ios kids swift https://krz.sh/apps/daybyday/

Commit 2f5d6433d2

2f5d6433d2a56eee7c3df95747d4ca0eb5d86df2

parent: 47864264e7

Unsigned

cmc <hello@cleberg.net> · 2026-07-25 21:34 UTC

feat: add haptic feedback on card tap

Fire a rigid impact haptic in SpeechSynthesizer.speak(), which every
card tap routes through, giving consistent tactile feedback across all
categories.

Layout: unified · split

DayByDay.xcodeproj/project.pbxproj +4 −4
@@ -338,7 +338,7 @@
338 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
339 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 339 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
340 CODE_SIGN_STYLE = Automatic; 340 CODE_SIGN_STYLE = Automatic;
341 CURRENT_PROJECT_VERSION = 6; 341 CURRENT_PROJECT_VERSION = 7;
342 DEVELOPMENT_TEAM = ZCNAX3VL9D; 342 DEVELOPMENT_TEAM = ZCNAX3VL9D;
343 ENABLE_PREVIEWS = YES; 343 ENABLE_PREVIEWS = YES;
344 GENERATE_INFOPLIST_FILE = YES; 344 GENERATE_INFOPLIST_FILE = YES;
@@ -353,7 +353,7 @@
353 "$(inherited)", 353 "$(inherited)",
354 "@executable_path/Frameworks", 354 "@executable_path/Frameworks",
355 ); 355 );
356 MARKETING_VERSION = 1.3.2; 356 MARKETING_VERSION = 1.3.3;
357 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; 357 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
358 PRODUCT_NAME = "$(TARGET_NAME)"; 358 PRODUCT_NAME = "$(TARGET_NAME)";
359 STRING_CATALOG_GENERATE_SYMBOLS = YES; 359 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -372,7 +372,7 @@
372 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
373 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 373 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
374 CODE_SIGN_STYLE = Automatic; 374 CODE_SIGN_STYLE = Automatic;
375 CURRENT_PROJECT_VERSION = 6; 375 CURRENT_PROJECT_VERSION = 7;
376 DEVELOPMENT_TEAM = ZCNAX3VL9D; 376 DEVELOPMENT_TEAM = ZCNAX3VL9D;
377 ENABLE_PREVIEWS = YES; 377 ENABLE_PREVIEWS = YES;
378 GENERATE_INFOPLIST_FILE = YES; 378 GENERATE_INFOPLIST_FILE = YES;
@@ -387,7 +387,7 @@
387 "$(inherited)", 387 "$(inherited)",
388 "@executable_path/Frameworks", 388 "@executable_path/Frameworks",
389 ); 389 );
390 MARKETING_VERSION = 1.3.2; 390 MARKETING_VERSION = 1.3.3;
391 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay; 391 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DayByDay;
392 PRODUCT_NAME = "$(TARGET_NAME)"; 392 PRODUCT_NAME = "$(TARGET_NAME)";
393 STRING_CATALOG_GENERATE_SYMBOLS = YES; 393 STRING_CATALOG_GENERATE_SYMBOLS = YES;
DayByDay.xcodeproj/project.xcworkspace/xcuserdata/cmc.xcuserdatad/UserInterfaceState.xcuserstate added

Binary file not shown.

DayByDay/SpeechSynthesizer.swift +7
@@ -4,6 +4,7 @@
4// 4//
5 5
6import AVFoundation 6import AVFoundation
7import UIKit
7 8
8/// Wrapper around AVSpeechSynthesizer that selects the highest-quality 9/// Wrapper around AVSpeechSynthesizer that selects the highest-quality
9/// en-US neural voice available on the device. Falls back gracefully if 10/// en-US neural voice available on the device. Falls back gracefully if
@@ -13,6 +14,7 @@ final class SpeechSynthesizer {
13 14
14 private let synthesizer = AVSpeechSynthesizer() 15 private let synthesizer = AVSpeechSynthesizer()
15 private let voice: AVSpeechSynthesisVoice? 16 private let voice: AVSpeechSynthesisVoice?
17 private let haptics = UIImpactFeedbackGenerator(style: .rigid)
16 18
17 private init() { 19 private init() {
18 self.voice = Self.bestAvailableVoice() 20 self.voice = Self.bestAvailableVoice()
@@ -24,6 +26,11 @@ final class SpeechSynthesizer {
24 } 26 }
25 27
26 func speak(_ text: String) { 28 func speak(_ text: String) {
29 // Every speak() call is a card tap, so give a gentle tactile bump
30 // alongside the audio. Prepared just before firing for lowest latency.
31 haptics.prepare()
32 haptics.impactOccurred()
33
27 // Ensure speech plays through the speaker even when the silent switch is on. 34 // Ensure speech plays through the speaker even when the silent switch is on.
28 try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) 35 try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
29 try? AVAudioSession.sharedInstance().setActive(true) 36 try? AVAudioSession.sharedInstance().setActive(true)