Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Tiny/Components/CoachMarkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -229,7 +229,7 @@ extension CoachMarkView {
animationType: animationType,
showText: showText,
customText: customText,
iconSize: 40,
iconSize: 60,
textSize: .caption,
spacing: 8
)
Expand Down
40 changes: 32 additions & 8 deletions Tiny/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -240,16 +248,16 @@
"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" : {
"comment" : "A label describing the number of high-quality heartbeat detections.",
"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" : {
Expand Down Expand Up @@ -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" : {
Expand All @@ -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
Expand All @@ -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" : {
Expand Down Expand Up @@ -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" : {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" : {
Expand Down
25 changes: 22 additions & 3 deletions Tiny/Views/OrbLiveListenView.swift
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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) {
Expand All @@ -35,6 +35,7 @@ struct OrbLiveListenView: View {
}
}
.preferredColorScheme(.dark)
.onAppear(perform: showInitialTutorialIfNeeded)
}
}
}
Expand Down Expand Up @@ -192,6 +193,8 @@ extension OrbLiveListenView {

heartbeatSoundManager.start()
heartbeatSoundManager.startRecording()

showListeningTutorialIfNeeded()
}

private func handleSingleTap() {
Expand Down Expand Up @@ -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 {
Expand Down
155 changes: 110 additions & 45 deletions Tiny/Views/TutorialOverlay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}