Refactor ZStack layouts to .background(), add Year View accessibility IDs, triage QA test plan

Replace ZStack-with-gradient patterns with idiomatic .background() modifier
across onboarding, customize, and settings views. Add accessibility identifiers
to Year View charts for UI test automation. Mark 67 impossible-to-automate
tests RED in QA plan and scaffold initial Year View and Settings onboarding tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-20 09:17:52 -06:00
parent ffc74f1a27
commit 5895b387be
22 changed files with 1469 additions and 1378 deletions

View File

@@ -41,8 +41,6 @@ struct AddMoodHeaderView: View {
Text(String(imagePack.rawValue))
.hidden()
theme.currentTheme.secondaryBGColor
VStack(spacing: 16) {
Text(ShowBasedOnVoteLogics.getVotingTitle(onboardingData: onboardingData))
.font(.title2.bold())
@@ -66,6 +64,7 @@ struct AddMoodHeaderView: View {
}
}
}
.background(theme.currentTheme.secondaryBGColor)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier(AccessibilityID.DayView.moodHeader)

View File

@@ -24,17 +24,17 @@ struct CustomizeContentView: View {
Button(action: { showThemePicker = true }) {
HStack(spacing: 16) {
// Emoji preview
ZStack {
LinearGradient(
colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)],
startPoint: .topLeading,
endPoint: .bottomTrailing
Text("🎨")
.font(.title)
.frame(width: 56, height: 56)
.background(
LinearGradient(
colors: [.purple.opacity(0.8), .blue.opacity(0.8), .cyan.opacity(0.8)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
Text("🎨")
.font(.title)
}
.frame(width: 56, height: 56)
.clipShape(RoundedRectangle(cornerRadius: 12))
.clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 4) {
Text("Browse Themes")

View File

@@ -259,26 +259,25 @@ struct AppThemePreviewSheet: View {
}
private var heroSection: some View {
ZStack {
// Gradient background
VStack(spacing: 16) {
Text(theme.emoji)
.font(.system(size: 72))
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
Text(theme.tagline)
.font(.title3.weight(.medium))
.foregroundColor(.white)
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
}
.frame(maxWidth: .infinity)
.frame(height: 200)
.background(
LinearGradient(
colors: theme.previewColors + [theme.previewColors[0].opacity(0.5)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
VStack(spacing: 16) {
Text(theme.emoji)
.font(.system(size: 72))
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
Text(theme.tagline)
.font(.title3.weight(.medium))
.foregroundColor(.white)
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
}
}
.frame(height: 200)
)
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding(.horizontal, 20)
.padding(.top, 16)

View File

@@ -22,40 +22,37 @@ struct DayFilterPickerView: View {
(Calendar.current.shortWeekdaySymbols[6], 7)]
var body: some View {
ZStack {
theme.currentTheme.secondaryBGColor
VStack {
HStack {
ForEach(weekdays.indices, id: \.self) { dayIdx in
let day = String(weekdays[dayIdx].0)
let value = weekdays[dayIdx].1
let isSelected = filteredDays.currentFilters.contains(value)
Button(action: {
if isSelected {
filteredDays.removeFilter(filter: value)
} else {
filteredDays.addFilter(newFilter: value)
}
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
}) {
Text(day.capitalized)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color(uiColor: .tertiarySystemBackground))
.foregroundColor(isSelected ? .green : .red)
.cornerRadius(8)
VStack {
HStack {
ForEach(weekdays.indices, id: \.self) { dayIdx in
let day = String(weekdays[dayIdx].0)
let value = weekdays[dayIdx].1
let isSelected = filteredDays.currentFilters.contains(value)
Button(action: {
if isSelected {
filteredDays.removeFilter(filter: value)
} else {
filteredDays.addFilter(newFilter: value)
}
.buttonStyle(.plain)
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
}) {
Text(day.capitalized)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color(uiColor: .tertiarySystemBackground))
.foregroundColor(isSelected ? .green : .red)
.cornerRadius(8)
}
.buttonStyle(.plain)
}
Text(String(localized: "day_picker_view_text"))
.padding(.top)
.foregroundColor(textColor)
}
.padding()
Text(String(localized: "day_picker_view_text"))
.padding(.top)
.foregroundColor(textColor)
}
.padding()
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}

View File

@@ -50,47 +50,45 @@ struct IconPickerView: View {
]
var body: some View {
ZStack {
theme.currentTheme.secondaryBGColor
VStack {
ScrollView(.horizontal) {
HStack {
VStack {
ScrollView(.horizontal) {
HStack {
Button(action: {
UIApplication.shared.setAlternateIconName(nil)
AnalyticsManager.shared.track(.appIconChanged(iconTitle: "default"))
}, label: {
Image("AppIconImage", bundle: .main)
.resizable()
.frame(width: 50, height:50)
.cornerRadius(10)
})
.accessibilityLabel(String(localized: "Default app icon"))
.accessibilityHint(String(localized: "Double tap to select"))
ForEach(iconSets, id: \.self.0){ iconSet in
Button(action: {
UIApplication.shared.setAlternateIconName(nil)
AnalyticsManager.shared.track(.appIconChanged(iconTitle: "default"))
UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in
// FIXME: Handle error
}
AnalyticsManager.shared.track(.appIconChanged(iconTitle: iconSet.1))
}, label: {
Image("AppIconImage", bundle: .main)
Image(iconSet.0, bundle: .main)
.resizable()
.frame(width: 50, height:50)
.cornerRadius(10)
})
.accessibilityLabel(String(localized: "Default app icon"))
.accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))"))
.accessibilityHint(String(localized: "Double tap to select"))
ForEach(iconSets, id: \.self.0){ iconSet in
Button(action: {
UIApplication.shared.setAlternateIconName(iconSet.1) { (error) in
// FIXME: Handle error
}
AnalyticsManager.shared.track(.appIconChanged(iconTitle: iconSet.1))
}, label: {
Image(iconSet.0, bundle: .main)
.resizable()
.frame(width: 50, height:50)
.cornerRadius(10)
})
.accessibilityLabel(String(localized: "App icon style \(iconSet.1.replacingOccurrences(of: "AppIcon", with: "").replacingOccurrences(of: "Image", with: ""))"))
.accessibilityHint(String(localized: "Double tap to select"))
}
}
.padding()
}
.background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor))
.padding()
.cornerRadius(10)
}
.background(RoundedRectangle(cornerRadius: 10).fill().foregroundColor(theme.currentTheme.bgColor))
.padding()
.cornerRadius(10)
}
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}

View File

@@ -16,58 +16,56 @@ struct PersonalityPackPickerView: View {
private var textColor: Color { theme.currentTheme.labelColor }
var body: some View {
ZStack {
theme.currentTheme.secondaryBGColor
VStack {
ForEach(PersonalityPack.allCases, id: \.self) { aPack in
VStack(spacing: 10) {
Text(String(aPack.title()))
.font(.body)
.foregroundColor(textColor)
Text(aPack.randomPushNotificationStrings().title)
.font(.body)
.foregroundColor(Color(UIColor.systemGray))
Text(aPack.randomPushNotificationStrings().body)
.font(.body)
.foregroundColor(Color(UIColor.systemGray))
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.contentShape(Rectangle())
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(personalityPack == aPack ? theme.currentTheme.bgColor : .clear)
.padding(5)
)
.onTapGesture {
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
personalityPack = aPack
AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title()))
LocalNotification.rescheduleNotifiations()
// }
}
// .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0)
.alert(isPresented: $showOver18Alert) {
let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) {
showNSFW = true
}
let secondaryButton = Alert.Button.cancel(Text(String(localized: "customize_view_over18alert_no"))) {
showNSFW = false
}
return Alert(title: Text(String(localized: "customize_view_over18alert_title")),
message: Text(String(localized: "customize_view_over18alert_body")),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
}
if aPack.rawValue != (PersonalityPack.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 {
Divider()
}
VStack {
ForEach(PersonalityPack.allCases, id: \.self) { aPack in
VStack(spacing: 10) {
Text(String(aPack.title()))
.font(.body)
.foregroundColor(textColor)
Text(aPack.randomPushNotificationStrings().title)
.font(.body)
.foregroundColor(Color(UIColor.systemGray))
Text(aPack.randomPushNotificationStrings().body)
.font(.body)
.foregroundColor(Color(UIColor.systemGray))
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.contentShape(Rectangle())
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(personalityPack == aPack ? theme.currentTheme.bgColor : .clear)
.padding(5)
)
.onTapGesture {
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
personalityPack = aPack
AnalyticsManager.shared.track(.personalityPackChanged(packTitle: aPack.title()))
LocalNotification.rescheduleNotifiations()
// }
}
// .blur(radius: aPack.rawValue == PersonalityPack.Rude.rawValue && !showNSFW ? 5 : 0)
.alert(isPresented: $showOver18Alert) {
let primaryButton = Alert.Button.default(Text(String(localized: "customize_view_over18alert_ok"))) {
showNSFW = true
}
let secondaryButton = Alert.Button.cancel(Text(String(localized: "customize_view_over18alert_no"))) {
showNSFW = false
}
return Alert(title: Text(String(localized: "customize_view_over18alert_title")),
message: Text(String(localized: "customize_view_over18alert_body")),
primaryButton: primaryButton,
secondaryButton: secondaryButton)
}
if aPack.rawValue != (PersonalityPack.allCases.sorted(by: { $0.rawValue > $1.rawValue }).first?.rawValue) ?? 0 {
Divider()
}
}
}
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}

View File

@@ -15,19 +15,17 @@ struct ThemePickerView: View {
private var textColor: Color { selectedTheme.currentTheme.labelColor }
var body: some View {
ZStack {
selectedTheme.currentTheme.secondaryBGColor
VStack {
HStack(spacing: 0) {
themeButton(for: .system)
themeButton(for: .iFeel)
themeButton(for: .dark)
themeButton(for: .light)
}
.padding(.top)
VStack {
HStack(spacing: 0) {
themeButton(for: .system)
themeButton(for: .iFeel)
themeButton(for: .dark)
themeButton(for: .light)
}
.padding()
.padding(.top)
}
.padding()
.background(selectedTheme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.onAppear {

View File

@@ -18,57 +18,54 @@ struct VotingLayoutPickerView: View {
}
var body: some View {
ZStack {
theme.currentTheme.secondaryBGColor
VStack(alignment: .leading, spacing: 12) {
Text("Voting Layout")
.font(.headline)
.foregroundColor(textColor)
.padding(.horizontal)
.padding(.top)
VStack(alignment: .leading, spacing: 12) {
Text("Voting Layout")
.font(.headline)
.foregroundColor(textColor)
.padding(.horizontal)
.padding(.top)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
Button(action: {
if UIAccessibility.isReduceMotionEnabled {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(VotingLayoutStyle.allCases, id: \.rawValue) { layout in
Button(action: {
if UIAccessibility.isReduceMotionEnabled {
votingLayoutStyle = layout.rawValue
} else {
withAnimation(.easeInOut(duration: 0.2)) {
votingLayoutStyle = layout.rawValue
} else {
withAnimation(.easeInOut(duration: 0.2)) {
votingLayoutStyle = layout.rawValue
}
}
AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName))
}) {
VStack(spacing: 6) {
layoutIcon(for: layout)
.frame(width: 44, height: 44)
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6))
Text(layout.displayName)
.font(.caption)
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8))
}
.frame(width: 70)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2)
)
}
.buttonStyle(.plain)
AnalyticsManager.shared.track(.votingLayoutChanged(layout: layout.displayName))
}) {
VStack(spacing: 6) {
layoutIcon(for: layout)
.frame(width: 44, height: 44)
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.6))
Text(layout.displayName)
.font(.caption)
.foregroundColor(currentLayout == layout ? .accentColor : textColor.opacity(0.8))
}
.frame(width: 70)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(currentLayout == layout ? Color.accentColor.opacity(0.15) : Color.clear)
)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(currentLayout == layout ? Color.accentColor : Color.clear, lineWidth: 2)
)
}
.buttonStyle(.plain)
}
.padding(.horizontal)
}
.padding(.bottom)
.padding(.horizontal)
}
.padding(.bottom)
}
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}

File diff suppressed because it is too large Load Diff

View File

@@ -522,6 +522,7 @@ struct YearCard: View, Equatable {
}
}
.buttonStyle(.plain)
.accessibilityIdentifier(AccessibilityID.YearView.cardHeader(year: year))
Spacer()
@@ -533,6 +534,7 @@ struct YearCard: View, Equatable {
.foregroundColor(textColor.opacity(0.6))
}
.buttonStyle(.plain)
.accessibilityIdentifier(AccessibilityID.YearView.shareButton)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
@@ -543,6 +545,7 @@ struct YearCard: View, Equatable {
// Donut Chart
MoodDonutChart(metrics: animatedMetrics, moodTint: moodTint)
.frame(width: 100, height: 100)
.accessibilityIdentifier(AccessibilityID.YearView.donutChart)
// Bar Chart
VStack(spacing: 6) {
@@ -574,10 +577,12 @@ struct YearCard: View, Equatable {
}
}
.frame(maxWidth: .infinity)
.accessibilityIdentifier(AccessibilityID.YearView.barChart)
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
.transition(.opacity.combined(with: .move(edge: .top)))
.accessibilityIdentifier(AccessibilityID.YearView.statsSection)
}
Divider()