Add onboarding Next buttons and fix accessibility for paged TabView
App-side changes: - Added "Get Started" / "Continue" next buttons to all onboarding pages (Welcome, Day, Time, Style) with onboarding_next_button accessibility ID - Added onNext callback plumbing from OnboardingMain to each page - OnboardingMain now uses TabView(selection:) for programmatic page navigation - Added .accessibilityElement(children: .contain) to all onboarding pages to fix iOS 26 paged TabView not exposing child elements - Added settings_segmented_picker accessibility ID to Settings Picker - Reduced padding on onboarding pages to keep buttons in visible area Test-side changes: - OnboardingScreen: replaced unreliable swipeToNext() with tapNext() that taps the accessibility-identified next button - OnboardingScreen: multi-strategy skip button detection for subscription page - SettingsScreen: scoped segment tap to picker element to avoid tab bar collision - CustomizeScreen: simplified horizontal scroll to plain app.swipeLeft() - OnboardingVotingTests: uses tapNext() to advance to Day page Passing: OnboardingTests.CompleteFlow, OnboardingVotingTests Remaining: OnboardingTests.DoesNotRepeat (session state issue), Settings scroll (deep elements), Customize horizontal pickers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,7 @@ enum AccessibilityID {
|
||||
// MARK: - Settings
|
||||
enum Settings {
|
||||
static let header = "settings_header"
|
||||
static let segmentedPicker = "settings_segmented_picker"
|
||||
static let customizeTab = "settings_tab_customize"
|
||||
static let settingsTab = "settings_tab_settings"
|
||||
static let upgradeBanner = "upgrade_banner"
|
||||
@@ -170,6 +171,7 @@ enum AccessibilityID {
|
||||
static let subscriptionScreen = "onboarding_subscription"
|
||||
static let subscribeButton = "onboarding_subscribe_button"
|
||||
static let skipButton = "onboarding_skip_button"
|
||||
static let nextButton = "onboarding_next_button"
|
||||
}
|
||||
|
||||
// MARK: - Reports
|
||||
|
||||
@@ -23,6 +23,7 @@ enum DayOptions: Int, CaseIterable, RawRepresentable, Codable {
|
||||
|
||||
struct OnboardingDay: View {
|
||||
@ObservedObject var onboardingData: OnboardingData
|
||||
var onNext: (() -> Void)? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
@@ -89,6 +90,22 @@ struct OnboardingDay: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
// Continue button
|
||||
Button(action: { onNext?() }) {
|
||||
Text("Continue")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(Color(hex: "4facfe"))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 30)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.nextButton)
|
||||
|
||||
// Tip
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "lightbulb.fill")
|
||||
@@ -101,7 +118,7 @@ struct OnboardingDay: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 80)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(
|
||||
@@ -111,6 +128,7 @@ struct OnboardingDay: View {
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.dayScreen)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,22 +11,27 @@ struct OnboardingMain: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@State var onboardingData: OnboardingData
|
||||
@EnvironmentObject var iapManager: IAPManager
|
||||
@State private var currentPage: Int = 0
|
||||
|
||||
let updateBoardingDataClosure: ((OnboardingData) -> Void)
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
TabView(selection: $currentPage) {
|
||||
// 1. Welcome screen
|
||||
OnboardingWelcome()
|
||||
OnboardingWelcome(onNext: nextPage)
|
||||
.tag(0)
|
||||
|
||||
// 2. Which day to rate
|
||||
OnboardingDay(onboardingData: onboardingData)
|
||||
OnboardingDay(onboardingData: onboardingData, onNext: nextPage)
|
||||
.tag(1)
|
||||
|
||||
// 3. Reminder time
|
||||
OnboardingTime(onboardingData: onboardingData)
|
||||
OnboardingTime(onboardingData: onboardingData, onNext: nextPage)
|
||||
.tag(2)
|
||||
|
||||
// 4. Style customization
|
||||
OnboardingStyle(onboardingData: onboardingData)
|
||||
OnboardingStyle(onboardingData: onboardingData, onNext: nextPage)
|
||||
.tag(3)
|
||||
|
||||
// 5. Subscription benefits & completion
|
||||
OnboardingSubscription(
|
||||
@@ -35,6 +40,7 @@ struct OnboardingMain: View {
|
||||
updateBoardingDataClosure(data)
|
||||
}
|
||||
)
|
||||
.tag(4)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.tabViewStyle(.page)
|
||||
@@ -44,6 +50,12 @@ struct OnboardingMain: View {
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
private func nextPage() {
|
||||
withAnimation {
|
||||
currentPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
func setupAppearance() {
|
||||
UIPageControl.appearance().currentPageIndicatorTintColor = .white
|
||||
UIPageControl.appearance().pageIndicatorTintColor = UIColor.white.withAlphaComponent(0.3)
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct OnboardingStyle: View {
|
||||
@ObservedObject var onboardingData: OnboardingData
|
||||
var onNext: (() -> Void)? = nil
|
||||
@State private var selectedTheme: AppTheme = .celestial
|
||||
|
||||
var body: some View {
|
||||
@@ -65,6 +66,22 @@ struct OnboardingStyle: View {
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// Continue button
|
||||
Button(action: { onNext?() }) {
|
||||
Text("Continue")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.white.opacity(0.25))
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 24)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.nextButton)
|
||||
|
||||
// Hint
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "arrow.left.arrow.right")
|
||||
@@ -74,8 +91,8 @@ struct OnboardingStyle: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 80)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 40)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
@@ -91,6 +108,7 @@ struct OnboardingStyle: View {
|
||||
// Apply default theme on appear
|
||||
selectedTheme.apply()
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.styleScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ struct OnboardingSubscription: View {
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 50)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
.background(
|
||||
LinearGradient(
|
||||
@@ -137,6 +137,7 @@ struct OnboardingSubscription: View {
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.subscriptionScreen)
|
||||
.sheet(isPresented: $showSubscriptionStore, onDismiss: {
|
||||
// After subscription store closes, complete onboarding
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct OnboardingTime: View {
|
||||
@ObservedObject var onboardingData: OnboardingData
|
||||
var onNext: (() -> Void)? = nil
|
||||
|
||||
var formatter: DateFormatter {
|
||||
let dateFormatter = DateFormatter()
|
||||
@@ -78,6 +79,22 @@ struct OnboardingTime: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
// Continue button
|
||||
Button(action: { onNext?() }) {
|
||||
Text("Continue")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(Color(hex: "f5576c"))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 16)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.nextButton)
|
||||
|
||||
// Info text
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "info.circle.fill")
|
||||
@@ -91,10 +108,11 @@ struct OnboardingTime: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 80)
|
||||
.padding(.bottom, 40)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.timeScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingWelcome: View {
|
||||
var onNext: (() -> Void)? = nil
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Gradient background
|
||||
@@ -54,28 +56,32 @@ struct OnboardingWelcome: View {
|
||||
Spacer()
|
||||
|
||||
// Feature highlights
|
||||
VStack(spacing: 20) {
|
||||
VStack(spacing: 16) {
|
||||
FeatureRow(icon: "bell.badge.fill", title: "Daily Reminders", description: "Never forget to log your mood")
|
||||
FeatureRow(icon: "chart.bar.fill", title: "Beautiful Insights", description: "See your mood patterns over time")
|
||||
FeatureRow(icon: "paintpalette.fill", title: "Fully Customizable", description: "Make it yours with themes & colors")
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 40)
|
||||
.padding(.bottom, 24)
|
||||
|
||||
// Swipe hint
|
||||
HStack(spacing: 8) {
|
||||
Text("Swipe to get started")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
// Continue button
|
||||
Button(action: { onNext?() }) {
|
||||
Text("Get Started")
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundColor(Color(hex: "667eea"))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 60)
|
||||
.accessibilityLabel(String(localized: "Swipe right to continue"))
|
||||
.accessibilityHint(String(localized: "Swipe to the next onboarding step"))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.bottom, 40)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.nextButton)
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityIdentifier(AccessibilityID.Onboarding.welcomeScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ struct SettingsTabView: View {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.accessibilityIdentifier(AccessibilityID.Settings.segmentedPicker)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
Reference in New Issue
Block a user