Add missing accessibility identifiers to all interactive UI elements

Audit found ~50+ interactive elements (buttons, toggles, pickers, alerts,
links) missing accessibility identifiers across 13 view files. Added
centralized ID definitions and applied them to every entry detail button,
guided reflection control, settings toggle, paywall unlock button,
subscription/IAP button, lock screen control, and photo action dialog.
This commit is contained in:
Trey T
2026-03-26 07:59:52 -05:00
parent 8cc9400d65
commit e7648ddd8a
13 changed files with 131 additions and 6 deletions

View File

@@ -31,7 +31,8 @@ struct GuidedReflectionInfoView: View {
title: String(localized: "guided_reflection_about_cbt_title"),
body: String(localized: "guided_reflection_about_cbt_body"),
citation: "Beck, J. S. (2020). Cognitive Behavior Therapy: Basics and Beyond, 3rd ed.",
url: cbtURL
url: cbtURL,
linkID: AccessibilityID.GuidedReflection.cbtLearnMoreLink
)
// ACT
@@ -41,7 +42,8 @@ struct GuidedReflectionInfoView: View {
title: String(localized: "guided_reflection_about_act_title"),
body: String(localized: "guided_reflection_about_act_body"),
citation: "Harris, R. (2009). ACT Made Simple. New Harbinger Publications.",
url: actURL
url: actURL,
linkID: AccessibilityID.GuidedReflection.actLearnMoreLink
)
// BA
@@ -51,7 +53,8 @@ struct GuidedReflectionInfoView: View {
title: String(localized: "guided_reflection_about_ba_title"),
body: String(localized: "guided_reflection_about_ba_body"),
citation: "Martell, C. R., Dimidjian, S., & Herman-Dunn, R. (2010). Behavioral Activation for Depression.",
url: baURL
url: baURL,
linkID: AccessibilityID.GuidedReflection.baLearnMoreLink
)
// Disclaimer
@@ -69,6 +72,7 @@ struct GuidedReflectionInfoView: View {
Button(String(localized: "Done")) {
dismiss()
}
.accessibilityIdentifier(AccessibilityID.GuidedReflection.infoDoneButton)
}
}
}
@@ -83,7 +87,8 @@ struct GuidedReflectionInfoView: View {
title: String,
body: String,
citation: String,
url: URL
url: URL,
linkID: String
) -> some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 10) {
@@ -113,6 +118,7 @@ struct GuidedReflectionInfoView: View {
}
.font(.caption)
}
.accessibilityIdentifier(linkID)
}
}
.padding()

View File

@@ -101,7 +101,9 @@ struct GuidedReflectionView: View {
Button(String(localized: "guided_reflection_discard"), role: .destructive) {
dismiss()
}
.accessibilityIdentifier(AccessibilityID.GuidedReflection.discardButton)
Button(String(localized: "Cancel"), role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.GuidedReflection.keepEditingButton)
} message: {
Text(String(localized: "guided_reflection_unsaved_message"))
}
@@ -180,6 +182,7 @@ struct GuidedReflectionView: View {
Image(systemName: "info.circle")
}
.accessibilityLabel(String(localized: "guided_reflection_about_title"))
.accessibilityIdentifier(AccessibilityID.GuidedReflection.infoButton)
}
}

View File

@@ -49,6 +49,7 @@ struct IAPWarningView: View {
.padding(.vertical, 12)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.pink))
}
.accessibilityIdentifier(AccessibilityID.IAPWarning.subscribeButton)
}
.padding()
.background(theme.currentTheme.secondaryBGColor)

View File

@@ -218,6 +218,7 @@ struct InsightsView: View {
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.accessibilityIdentifier(AccessibilityID.Paywall.insightsUnlockButton)
Spacer()
}

View File

@@ -332,6 +332,7 @@ struct ReportsView: View {
viewModel.generateReport()
}
.font(.subheadline.weight(.medium))
.accessibilityIdentifier(AccessibilityID.Reports.retryButton)
}
.padding()
.background(
@@ -403,6 +404,7 @@ struct ReportsView: View {
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.accessibilityIdentifier(AccessibilityID.Paywall.reportsUnlockButton)
Spacer()
}

View File

@@ -1670,6 +1670,7 @@ struct LockScreenView: View {
.opacity(showContent ? 1 : 0)
.offset(y: showContent ? 0 : 30)
.padding(.horizontal, 32)
.accessibilityIdentifier(AccessibilityID.LockScreen.unlockButton)
.accessibilityLabel("Unlock")
.accessibilityHint("Double tap to authenticate with \(authManager.biometricName)")
@@ -1705,7 +1706,9 @@ struct LockScreenView: View {
await authManager.authenticate()
}
}
.accessibilityIdentifier(AccessibilityID.LockScreen.tryAgainButton)
Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.LockScreen.cancelButton)
} message: {
Text("Unable to verify your identity. Please try again.")
}

View File

@@ -54,6 +54,7 @@ struct MonthDetailView: View {
Image(systemName: "square.and.arrow.up")
.foregroundColor(textColor)
.padding(.trailing)
.accessibilityIdentifier(AccessibilityID.MonthDetail.shareButton)
.onTapGesture {
let impactMed = UIImpactFeedbackGenerator(style: .heavy)
impactMed.impactOccurred()
@@ -97,6 +98,7 @@ struct MonthDetailView: View {
showUpdateEntryAlert = false
selectedEntry = nil
})
.accessibilityIdentifier(AccessibilityID.MonthDetail.moodButton(mood.strValue))
}
if let selectedEntry = selectedEntry,
@@ -107,6 +109,7 @@ struct MonthDetailView: View {
updateEntries()
showUpdateEntryAlert = false
})
.accessibilityIdentifier(AccessibilityID.MonthDetail.deleteButton)
}
Button(String(localized: "content_view_fill_in_missing_entry_cancel"), role: .cancel, action: {

View File

@@ -324,6 +324,7 @@ struct MonthView: View {
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.accessibilityIdentifier(AccessibilityID.Paywall.monthUnlockButton)
}
.padding(.vertical, 24)
.frame(maxWidth: .infinity)

View File

@@ -81,6 +81,7 @@ struct NoteEditorView: View {
Button("Done") {
isTextFieldFocused = false
}
.accessibilityIdentifier(AccessibilityID.NoteEditor.keyboardDoneButton)
}
}
.onAppear {
@@ -226,7 +227,9 @@ struct EntryDetailView: View {
onDelete()
dismiss()
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.deleteConfirmButton)
Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.EntryDetail.deleteCancelButton)
} message: {
Text("Are you sure you want to delete this mood entry? This cannot be undone.")
}
@@ -461,6 +464,7 @@ struct EntryDetailView: View {
.font(.subheadline)
.fontWeight(.medium)
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.reflectionBeginButton)
}
Button {
@@ -507,6 +511,7 @@ struct EntryDetailView: View {
)
}
.buttonStyle(.plain)
.accessibilityIdentifier(AccessibilityID.EntryDetail.reflectionCard)
}
}
@@ -526,6 +531,7 @@ struct EntryDetailView: View {
.font(.subheadline)
.fontWeight(.medium)
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoButton)
}
.zIndex(1)
@@ -542,6 +548,7 @@ struct EntryDetailView: View {
.onTapGesture {
showFullScreenPhoto = true
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoImage)
} else {
Button {
showPhotoOptions = true
@@ -568,22 +575,27 @@ struct EntryDetailView: View {
)
}
.buttonStyle(.plain)
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoPlaceholder)
}
}
.confirmationDialog("Photo", isPresented: $showPhotoOptions, titleVisibility: .visible) {
Button("Take Photo") {
showCamera = true
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoTakeButton)
Button("Choose from Library") {
showPhotoPicker = true
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoChooseButton)
if let photoID = entry.photoID {
Button("Remove Photo", role: .destructive) {
_ = PhotoManager.shared.deletePhoto(id: photoID)
_ = DataController.shared.updatePhoto(forDate: entry.forDate, photoID: nil)
}
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoRemoveButton)
}
Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityID.EntryDetail.photoCancelButton)
}
}
@@ -634,9 +646,11 @@ struct FullScreenPhotoView: View {
.foregroundStyle(.white.opacity(0.8))
.padding()
}
.accessibilityIdentifier(AccessibilityID.FullScreenPhoto.closeButton)
}
.onTapGesture {
dismiss()
}
.accessibilityIdentifier(AccessibilityID.FullScreenPhoto.dismissArea)
}
}

View File

@@ -87,6 +87,7 @@ struct PurchaseButtonView: View {
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
}
.accessibilityIdentifier(AccessibilityID.Purchase.manageSubscriptionButton)
// Show other subscription options
if iapManager.sortedProducts.count > 1 {
@@ -98,6 +99,7 @@ struct PurchaseButtonView: View {
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
.accessibilityIdentifier(AccessibilityID.Purchase.changePlanButton)
}
}
}
@@ -184,6 +186,7 @@ struct PurchaseButtonView: View {
.font(.body)
.foregroundColor(.blue)
}
.accessibilityIdentifier(AccessibilityID.Purchase.restorePurchasesButton)
}
}

View File

@@ -814,6 +814,7 @@ struct SettingsContentView: View {
}
))
.labelsHidden()
.accessibilityIdentifier(AccessibilityID.Settings.privacyLockToggle)
.accessibilityLabel(String(localized: "Privacy Lock"))
.accessibilityHint(String(localized: "Require biometric authentication to open app"))
}
@@ -913,6 +914,7 @@ struct SettingsContentView: View {
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
.accessibilityIdentifier(AccessibilityID.Settings.healthSyncToggle)
.accessibilityLabel(String(localized: "Apple Health"))
.accessibilityHint(String(localized: "Sync mood data with Apple Health"))
} else {
@@ -1012,6 +1014,7 @@ struct SettingsContentView: View {
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
.accessibilityIdentifier(AccessibilityID.Settings.weatherToggle)
.accessibilityLabel(String(localized: "Weather"))
.accessibilityHint(String(localized: "Show weather details for each day"))
}
@@ -1128,6 +1131,7 @@ struct SettingsContentView: View {
.onChange(of: hapticFeedbackEnabled) { _, newValue in
AnalyticsManager.shared.track(.hapticFeedbackToggled(enabled: newValue))
}
.accessibilityIdentifier(AccessibilityID.Settings.hapticFeedbackToggle)
.accessibilityLabel(String(localized: "Haptic Feedback"))
.accessibilityHint(String(localized: "Toggle vibration feedback when voting"))
}
@@ -1145,6 +1149,7 @@ struct SettingsContentView: View {
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
}
.foregroundColor(textColor)
.accessibilityIdentifier(AccessibilityID.Settings.deleteToggle)
.accessibilityHint(String(localized: "Allow deleting mood entries by swiping"))
.padding()
}
@@ -1274,6 +1279,7 @@ struct ReminderTimePickerView: View {
)
.datePickerStyle(.wheel)
.labelsHidden()
.accessibilityIdentifier(AccessibilityID.Settings.reminderTimePicker)
Spacer()
}
@@ -1285,12 +1291,14 @@ struct ReminderTimePickerView: View {
Button("Cancel") {
dismiss()
}
.accessibilityIdentifier(AccessibilityID.Settings.reminderCancelButton)
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
saveReminderTime()
dismiss()
}
.accessibilityIdentifier(AccessibilityID.Settings.reminderSaveButton)
.fontWeight(.semibold)
}
}
@@ -1545,6 +1553,7 @@ struct SettingsView: View {
}
))
.labelsHidden()
.accessibilityIdentifier(AccessibilityID.Settings.privacyLockToggle)
}
.padding()
.background(theme.currentTheme.secondaryBGColor)
@@ -1614,6 +1623,7 @@ struct SettingsView: View {
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
.accessibilityIdentifier(AccessibilityID.Settings.healthSyncToggle)
} else {
Text("Not Available")
.font(.caption)
@@ -1705,6 +1715,7 @@ struct SettingsView: View {
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
.accessibilityIdentifier(AccessibilityID.Settings.weatherToggle)
}
.padding()
@@ -1963,13 +1974,14 @@ struct SettingsView: View {
Text(String(localized: "settings_view_show_onboarding"))
.foregroundColor(textColor)
})
.accessibilityIdentifier(AccessibilityID.Settings.showOnboardingButton)
.padding()
.frame(maxWidth: .infinity)
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var eulaButton: some View {
Button(action: {
AnalyticsManager.shared.track(.eulaViewed)
@@ -1978,13 +1990,14 @@ struct SettingsView: View {
Text(String(localized: "settings_view_show_eula"))
.foregroundColor(textColor)
})
.accessibilityIdentifier(AccessibilityID.Settings.eulaButton)
.padding()
.frame(maxWidth: .infinity)
.background(theme.currentTheme.secondaryBGColor)
.fixedSize(horizontal: false, vertical: true)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
}
private var privacyButton: some View {
Button(action: {
AnalyticsManager.shared.track(.privacyPolicyViewed)
@@ -1993,6 +2006,7 @@ struct SettingsView: View {
Text(String(localized: "settings_view_show_privacy"))
.foregroundColor(textColor)
})
.accessibilityIdentifier(AccessibilityID.Settings.privacyPolicyButton)
.padding()
.frame(maxWidth: .infinity)
.background(theme.currentTheme.secondaryBGColor)
@@ -2062,6 +2076,7 @@ struct SettingsView: View {
.onChange(of: hapticFeedbackEnabled) { _, newValue in
AnalyticsManager.shared.track(.hapticFeedbackToggled(enabled: newValue))
}
.accessibilityIdentifier(AccessibilityID.Settings.hapticFeedbackToggle)
.accessibilityLabel(String(localized: "Haptic Feedback"))
.accessibilityHint(String(localized: "Toggle vibration feedback when voting"))
}
@@ -2079,6 +2094,7 @@ struct SettingsView: View {
AnalyticsManager.shared.track(.deleteToggleChanged(enabled: newValue))
}
.foregroundColor(textColor)
.accessibilityIdentifier(AccessibilityID.Settings.deleteToggle)
.padding()
}
.background(theme.currentTheme.secondaryBGColor)

View File

@@ -260,6 +260,7 @@ struct YearView: View {
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal, 24)
.accessibilityIdentifier(AccessibilityID.Paywall.yearUnlockButton)
}
.padding(.vertical, 24)
.frame(maxWidth: .infinity)