Add tooltip coach mark tutorial for new users

- TutorialManager: tracks 4 steps (NLP, Calendar, Matrix, Workout)
  persisted via AppStorage; shown once then never again
- TutorialView: fullscreen overlay with even-odd spotlight cutout,
  animated accent border, ultraThinMaterial callout card, pill progress
  dots, Next/Get started button, and Skip
- TutorialFrameKey: PreferenceKey collects CGRect for each anchor index;
  .tutorialAnchor() modifier wired to FAB (step 0) and all 4 tab buttons
- Callout auto-positions above or below the spotlight based on screen half
- ContentView collects frames via onPreferenceChange and layers the overlay

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-06-01 02:23:12 +03:00
parent 409b4f8ee6
commit 8f8825ac04
5 changed files with 248 additions and 2 deletions

View File

@@ -14,6 +14,8 @@ struct ContentView: View {
@State private var showQuickAddTask = false
@StateObject private var tabState = FloatingTabState()
@ObservedObject private var shortcuts = ShortcutHandler.shared
@ObservedObject private var tutorial = TutorialManager.shared
@State private var tutorialFrames: [Int: CGRect] = [:]
@State private var iPadSelection: Int? = 0
@State private var calIconImage: Image = ContentView.buildCalIcon()
@@ -124,8 +126,7 @@ struct ContentView: View {
@ViewBuilder
private func iPhoneLayout() -> some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTab) {
TodayView()
TabView(selection: $selectedTab) { TodayView()
.environmentObject(taskVM)
.environmentObject(workoutVM)
.tag(0)
@@ -170,7 +171,14 @@ struct ContentView: View {
showMatrix: showMatrixTab,
showWorkout: showWorkoutTab
)
if tutorial.isActive {
TutorialOverlay(frames: tutorialFrames)
.transition(.opacity)
.zIndex(99)
}
}
.onPreferenceChange(TutorialFrameKey.self) { tutorialFrames = $0 }
.onAppear { UITabBar.appearance().isHidden = true }
}
@@ -259,6 +267,7 @@ struct KisaniTabBar: View {
}
.frame(maxWidth: .infinity)
.frame(height: 52)
.tutorialAnchor(entry.id)
}
.buttonStyle(PressButtonStyle(scale: 0.90))
}