diff --git a/Tiny/Components/CoachMarkView.swift b/Tiny/Components/CoachMarkView.swift index 4bcdf1f..926539f 100644 --- a/Tiny/Components/CoachMarkView.swift +++ b/Tiny/Components/CoachMarkView.swift @@ -180,7 +180,7 @@ struct CoachMarkView: View { // MARK: - Hold Animation private func runHoldAnimation() async throws { iconState = 3 - try await Task.sleep(for: .milliseconds(3000)) + try await Task.sleep(for: .milliseconds(1500)) iconState = 0 try await Task.sleep(for: .milliseconds(1000)) } @@ -229,7 +229,7 @@ extension CoachMarkView { animationType: animationType, showText: showText, customText: customText, - iconSize: 40, + iconSize: 60, textSize: .caption, spacing: 8 ) diff --git a/Tiny/Localizable.xcstrings b/Tiny/Localizable.xcstrings index ee2b33e..1180cc4 100644 --- a/Tiny/Localizable.xcstrings +++ b/Tiny/Localizable.xcstrings @@ -208,10 +208,18 @@ "comment" : "A label displayed above the filter mode selection options in the heartbeat analysis view.", "isCommentAutoGenerated" : true }, + "Finish session" : { + "comment" : "A label describing the action to finish a session.", + "isCommentAutoGenerated" : true + }, "Frequency Spectrum" : { "comment" : "A title for the frequency spectrum visualization.", "isCommentAutoGenerated" : true }, + "Hear Baby's Heartbeat" : { + "comment" : "The title of the tutorial overlay.", + "isCommentAutoGenerated" : true + }, "Heart Rate" : { "comment" : "A title for the section displaying the user's heart rate.", "isCommentAutoGenerated" : true @@ -240,8 +248,8 @@ "comment" : "A title at the top of the home view, followed by a user's name and a profile icon.", "isCommentAutoGenerated" : true }, - "Here's how to control your recording" : { - "comment" : "A description of how to control their recording.", + "Here's how to control your session" : { + "comment" : "A description of how to control a user's session.", "isCommentAutoGenerated" : true }, "High Quality Detections" : { @@ -249,7 +257,7 @@ "isCommentAutoGenerated" : true }, "Hold then drag the sphere" : { - "comment" : "A description of how to save or delete a recording.", + "comment" : "A description of an action to perform when a user holds and drags a sphere.", "isCommentAutoGenerated" : true }, "Hold to stop" : { @@ -326,7 +334,7 @@ }, "Play or Pause" : { - "comment" : "A label displayed next to a play/pause button in the tutorial overlay.", + "comment" : "A label describing the action of playing or pausing a recording.", "isCommentAutoGenerated" : true }, "Play Recording" : { @@ -343,6 +351,10 @@ "comment" : "A title that describes the user's pregnancy age.", "isCommentAutoGenerated" : true }, + "Press and hold the sphere" : { + "comment" : "A description of how to finish a session by holding a sphere.", + "isCommentAutoGenerated" : true + }, "Proximity Gain" : { "comment" : "A label displayed next to the value of the proximity gain slider.", "isCommentAutoGenerated" : true @@ -363,7 +375,7 @@ "isCommentAutoGenerated" : true }, "Replay Your Recording" : { - "comment" : "A title for a section that explains how to replay a user's recording.", + "comment" : "A label for a view that instructs the user to replay their recording.", "isCommentAutoGenerated" : true }, "S1 (Lub) Average" : { @@ -395,7 +407,7 @@ "isCommentAutoGenerated" : true }, "Save or Delete" : { - "comment" : "A caption displayed underneath a coach mark in the tutorial overlay.", + "comment" : "A label describing an action that saves or deletes a recording.", "isCommentAutoGenerated" : true }, "Search library" : { @@ -462,6 +474,10 @@ "comment" : "A button to start recording audio.", "isCommentAutoGenerated" : true }, + "Start session" : { + "comment" : "A label describing the action to start a session.", + "isCommentAutoGenerated" : true + }, "Statistics" : { "comment" : "A label displayed above the statistics section of the view.", "isCommentAutoGenerated" : true @@ -495,11 +511,19 @@ "isCommentAutoGenerated" : true }, "Tap the screen" : { - "comment" : "A description of how to play or pause a recording.", + "comment" : "A description of tapping the screen to play or pause a recording.", + "isCommentAutoGenerated" : true + }, + "Tap to Begin" : { + "comment" : "A call-to-action label displayed below the tutorial instructions.", "isCommentAutoGenerated" : true }, "Tap to Continue" : { - "comment" : "A call-to-action label that appears at the bottom of the tutorial overlay, instructing the user to tap to continue.", + "comment" : "A label displayed below the last step of a tutorial. It instructs the user to continue the tutorial.", + "isCommentAutoGenerated" : true + }, + "Tap twice" : { + "comment" : "A description of how to start a session.", "isCommentAutoGenerated" : true }, "Time" : { diff --git a/Tiny/Views/OrbLiveListenView.swift b/Tiny/Views/OrbLiveListenView.swift index 23ba974..b17259b 100644 --- a/Tiny/Views/OrbLiveListenView.swift +++ b/Tiny/Views/OrbLiveListenView.swift @@ -1,7 +1,7 @@ import SwiftUI struct OrbLiveListenView: View { - @State private var showTutorial = !UserDefaults.standard.bool(forKey: "hasShownTutorial") + @State private var activeTutorial: TutorialContext? @StateObject var heartbeatSoundManager = HeartbeatSoundManager() @StateObject private var audioPostProcessingManager = AudioPostProcessingManager() @StateObject private var physicsController = OrbPhysicsController() @@ -25,8 +25,8 @@ struct OrbLiveListenView: View { orbView(geometry: geometry) coachMarkView - if showTutorial { - TutorialOverlay(showTutorial: $showTutorial) + if let context = activeTutorial { + TutorialOverlay(activeTutorial: $activeTutorial, context: context) } } .sheet(isPresented: $showShareSheet) { @@ -35,6 +35,7 @@ struct OrbLiveListenView: View { } } .preferredColorScheme(.dark) + .onAppear(perform: showInitialTutorialIfNeeded) } } } @@ -192,6 +193,8 @@ extension OrbLiveListenView { heartbeatSoundManager.start() heartbeatSoundManager.startRecording() + + showListeningTutorialIfNeeded() } private func handleSingleTap() { @@ -266,6 +269,22 @@ extension OrbLiveListenView { heartbeatSoundManager.stopRecording() heartbeatSoundManager.stop() } + + // MARK: - Tutorial Logic + private func showInitialTutorialIfNeeded() { + if !UserDefaults.standard.bool(forKey: "hasShownInitialTutorial") { + activeTutorial = .initial + } + } + + private func showListeningTutorialIfNeeded() { + if !UserDefaults.standard.bool(forKey: "hasShownListeningTutorial") { + // Use a delay to allow the listening UI to appear first + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + activeTutorial = .listening + } + } + } } #Preview { diff --git a/Tiny/Views/TutorialOverlay.swift b/Tiny/Views/TutorialOverlay.swift index 180b911..78e517a 100644 --- a/Tiny/Views/TutorialOverlay.swift +++ b/Tiny/Views/TutorialOverlay.swift @@ -7,65 +7,130 @@ import SwiftUI +enum TutorialContext { + case initial, listening +} + struct TutorialOverlay: View { - @Binding var showTutorial: Bool + @Binding var activeTutorial: TutorialContext? + let context: TutorialContext var body: some View { ZStack { - Color.black.opacity(0.8) + Color.black.opacity(0.9) .ignoresSafeArea() - VStack(spacing: 32) { - VStack(spacing: 2) { - Text("Replay Your Recording") - .font(.body) - .fontWeight(.bold) - Text("Here's how to control your recording") - .font(.subheadline) - } - .foregroundColor(.white) - - .foregroundColor(.white) - VStack(alignment: .leading, spacing: -8) { - HStack(spacing: 12) { - CoachMarkView.small( - animationType: .singleTap, - showText: false - ) - VStack(alignment: .leading) { - Text("Play or Pause") - .font(.headline) - .fontWeight(.bold) - Text("Tap the screen") - } - .foregroundColor(.white) + + switch context { + case .initial: + initialTutorialView + case .listening: + listeningTutorialView + } + } + .onTapGesture { + switch context { + case .initial: + UserDefaults.standard.set(true, forKey: "hasShownInitialTutorial") + case .listening: + UserDefaults.standard.set(true, forKey: "hasShownListeningTutorial") + } + activeTutorial = nil + } + } + + private var initialTutorialView: some View { + VStack(spacing: 24) { + VStack(spacing: 2) { + Text("Hear Baby's Heartbeat") + .font(.body) + .fontWeight(.bold) + Text("Here's how to control your session") + .font(.subheadline) + } + .foregroundColor(.white) + + VStack(alignment: .leading, spacing: -28) { + HStack(spacing: 0) { + CoachMarkView.small( + animationType: .doubleTap, + showText: false + ) + VStack(alignment: .leading) { + Text("Start session") + .font(.headline) + .fontWeight(.bold) + Text("Tap twice") } - HStack(spacing: 12) { - CoachMarkView.small( - animationType: .holdAndDrag, - showText: false - ) - VStack(alignment: .leading) { - Text("Save or Delete") - .font(.headline) - .fontWeight(.bold) - Text("Hold then drag the sphere") - } - .foregroundColor(.white) + .foregroundColor(.white) + } + HStack(spacing: 0) { + CoachMarkView.small( + animationType: .hold, + showText: false + ) + VStack(alignment: .leading) { + Text("Finish session") + .font(.headline) + .fontWeight(.bold) + Text("Press and hold the sphere") } + .foregroundColor(.white) } - Text("Tap to Continue") - .font(.subheadline) + } + Text("Tap to Begin") + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.white) + } + } + + private var listeningTutorialView: some View { + VStack(spacing: 24) { + VStack(spacing: 2) { + Text("Replay Your Recording") + .font(.body) .fontWeight(.bold) + Text("Here's how to control your session") + .font(.subheadline) + } + .foregroundColor(.white) + + VStack(alignment: .leading, spacing: -28) { + HStack(spacing: 0) { + CoachMarkView.small( + animationType: .singleTap, + showText: false + ) + VStack(alignment: .leading) { + Text("Play or Pause") + .font(.headline) + .fontWeight(.bold) + Text("Tap the screen") + } + .foregroundColor(.white) + } + HStack(spacing: 0) { + CoachMarkView.small( + animationType: .holdAndDrag, + showText: false + ) + VStack(alignment: .leading) { + Text("Save or Delete") + .font(.headline) + .fontWeight(.bold) + Text("Hold then drag the sphere") + } .foregroundColor(.white) + } } - } - .onTapGesture { - showTutorial = false - UserDefaults.standard.set(true, forKey: "hasShownTutorial") + Text("Tap to Continue") + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(.white) } } } #Preview { - TutorialOverlay(showTutorial: .constant(true)) + TutorialOverlay(activeTutorial: .constant(.initial), context: .listening) }