diff --git a/cards/CardView/CardView.swift b/cards/CardView/CardView.swift index e2f9e43..565638e 100644 --- a/cards/CardView/CardView.swift +++ b/cards/CardView/CardView.swift @@ -24,10 +24,14 @@ struct CardView: View { return HStack{ Text(heading) .bold() + .accessibilityLabel(Text(heading)) Spacer() if !model.isAuthenticated { SecureField("", text: value) .multilineTextAlignment(.trailing) + .accessibilityLabel(Text(heading)) + .accessibilityHint(Text("Secure field")) + .accessibilityIdentifier("\(heading.lowercased())SecureField") } else { TextField("", text: value) .multilineTextAlignment(.trailing) @@ -43,6 +47,9 @@ struct CardView: View { Image(systemName: "doc.on.doc") } }) + .accessibilityLabel(Text(heading)) + .accessibilityHint(Text(model.isEditing ? "Editable field" : "Read only")) + .accessibilityIdentifier("\(heading.lowercased())TextField") } } .if (!model.isEditing, transform: { view in @@ -50,6 +57,8 @@ struct CardView: View { model.copyAction(with: value.wrappedValue) }) }) + .accessibilityElement(children: .combine) + .accessibilityIdentifier("\(heading.lowercased())ItemView") } fileprivate func getCardListView() -> some View { @@ -72,6 +81,7 @@ struct CardView: View { if heading == "Number" && !model.isEditing { view.popoverTip(tip, arrowEdge: .top) + .accessibilityHint(Text("Double tap to copy card number")) } else if heading == "Expiration" { view.onChange(of: model.card.expiration) { _ , newValue in if newValue.count == 2 && !newValue.contains("/") { @@ -92,19 +102,27 @@ struct CardView: View { Picker("Card Network", selection: $model.card.network) { ForEach(CardNetwork.allCases) { pref in Text(pref.rawValue) + .accessibilityLabel(Text(pref.rawValue)) } } .disabled(!model.isEditing) .bold() + .accessibilityLabel(Text("Card Network")) + .accessibilityHint(Text(model.isEditing ? "Select card network" : "Card network")) + .accessibilityIdentifier("cardNetworkPicker") } Picker("Card Type", selection: $model.card.type) { ForEach(CardType.allCases) { pref in Text(pref.rawValue) + .accessibilityLabel(Text(pref.rawValue)) } } .disabled(!model.isEditing) .bold() + .accessibilityLabel(Text("Card Type")) + .accessibilityHint(Text(model.isEditing ? "Select card type" : "Card type")) + .accessibilityIdentifier("cardTypePicker") } } @@ -117,6 +135,9 @@ struct CardView: View { view .blur(radius: 10, opaque: true) }) + .accessibilityLabel(Text("Card image")) + .accessibilityHint(Text(model.isAuthenticated ? "Card image preview" : "Card image is blurred until authenticated")) + .accessibilityIdentifier("cardImage") } } @@ -127,7 +148,9 @@ struct CardView: View { VStack(alignment: .leading){ HStack { Image(systemName: "photo") + .accessibilityHidden(true) Text(model.cardImage == nil ? "Add Card Image" : "Change Card Image") + .accessibilityLabel(Text(model.cardImage == nil ? "Add Card Image" : "Change Card Image")) } .padding(.bottom) @@ -163,9 +186,14 @@ struct CardView: View { } label: { HStack { Image(systemName: "trash") + .accessibilityHidden(true) Text("Remove Image") + .accessibilityLabel(Text("Remove Image")) } } + .accessibilityLabel(Text("Remove Image")) + .accessibilityHint(Text("Removes the selected card image")) + .accessibilityIdentifier("removeImageButton") } } } @@ -174,6 +202,9 @@ struct CardView: View { ShareLink(item: model.card.toShareString()) { Label("Click to share", systemImage: "square.and.arrow.up") } + .accessibilityLabel(Text("Share card details")) + .accessibilityHint(Text("Shares your card details")) + .accessibilityIdentifier("shareLink") Button(action: { model.isEditing.toggle() // if user is not editing, then he is done editing when button press @@ -183,6 +214,9 @@ struct CardView: View { }) { Text(model.isEditing ? "Done" : "Edit") } + .accessibilityLabel(Text(model.isEditing ? "Done editing" : "Edit card")) + .accessibilityHint(Text(model.isEditing ? "Finish editing card details" : "Edit card details")) + .accessibilityIdentifier("editDoneButton") } .disabled(!$model.isAuthenticated.wrappedValue) .toolbar { @@ -192,10 +226,14 @@ struct CardView: View { model.isShowingScanner = true }, label: { Image(systemName: "camera.on.rectangle") + .accessibilityHidden(true) }) .if(!model.isAddNewFlow, transform: { view in view.hidden() }) + .accessibilityLabel(Text("Scan card")) + .accessibilityHint(Text("Opens camera to scan your card")) + .accessibilityIdentifier("scanCardButton") // .screen was causing issues with camera session not closing .fullScreenCover(isPresented: $model.isShowingScanner) { diff --git a/cards/CreditCard.swift b/cards/CreditCard.swift index e79114b..65991f7 100644 --- a/cards/CreditCard.swift +++ b/cards/CreditCard.swift @@ -16,6 +16,7 @@ struct CreditCard: App { var body: some Scene { WindowGroup { HomeView() + .accessibilityIdentifier("HomeView") .task { try? Tips.configure([ .displayFrequency(.immediate), diff --git a/cards/Home/HomeView.swift b/cards/Home/HomeView.swift index 86d5dd2..a3f5926 100644 --- a/cards/Home/HomeView.swift +++ b/cards/Home/HomeView.swift @@ -19,6 +19,7 @@ struct HomeView: View { Section(header: Text("\(type.rawValue)s")){ ForEach(model.cardDataStore.cardsByType[type] ?? [], id: \.id) { card in getRowforCards(with: card) + .accessibilityIdentifier("CardRow_\(card.id.uuidString)") } .onDelete { offsets in model.deleteCard(at: offsets, inSection: type) @@ -26,6 +27,9 @@ struct HomeView: View { Button("Add a new \(type.rawValue)") { model.showingPopover.toggle() } + .accessibilityLabel(Text("Add a new \(type.rawValue) card")) + .accessibilityHint(Text("Adds a new \(type.rawValue) card")) + .accessibilityIdentifier("AddCardButton_\(type.rawValue)") .sheet(isPresented: $model.showingPopover) { NavigationView { CardView(model: CardViewModel( @@ -50,6 +54,7 @@ struct HomeView: View { } } .navigationTitle("Cards") + .accessibilityLabel(Text("Cards")) .onAppear { model.cardDataStore.loadCards() } @@ -57,14 +62,23 @@ struct HomeView: View { NavigationLink(destination: SettingsView(model: SettingsViewModel())){ Image(systemName: "gear") } + .accessibilityLabel(Text("Settings")) + .accessibilityHint(Text("Opens settings")) + .accessibilityIdentifier("SettingsButton") } .alert("Enable Biometrics",isPresented: model.$isFirstLaunch, actions: { Button("Yes", role: .cancel) { UserSettings.shared.isAuthEnabled = true } + .accessibilityLabel(Text("Enable biometrics")) + .accessibilityHint(Text("Enables biometric authentication")) + .accessibilityIdentifier("EnableBiometricsYesButton") Button("No", role: .destructive) { UserSettings.shared.isAuthEnabled = false } + .accessibilityLabel(Text("Do not enable biometrics")) + .accessibilityHint(Text("Disables biometric authentication")) + .accessibilityIdentifier("EnableBiometricsNoButton") }) } detail: { if let card = model.selectedCard { @@ -76,6 +90,8 @@ struct HomeView: View { } else { Text("Tap on a Card to view details") + .accessibilityLabel(Text("No card selected. Tap on a card to view details.")) + .accessibilityIdentifier("NoCardSelectedText") } } .whatsNewSheet() @@ -88,16 +104,31 @@ struct HomeView: View { .resizable() .scaledToFit() .frame(width: 36,height: 36) + .accessibilityLabel(Text("\(card.network.rawValue) logo")) + .accessibilityIdentifier("CardNetworkImage_\(card.id.uuidString)") VStack(alignment: .leading){ if card.description != "" { Text(card.description) + .accessibilityLabel(Text(card.description)) + .accessibilityIdentifier("CardDescription_\(card.id.uuidString)") + .minimumScaleFactor(0.8) } else { Text(card.name) + .accessibilityLabel(Text(card.name)) + .accessibilityIdentifier("CardName_\(card.id.uuidString)") + .minimumScaleFactor(0.8) } Text(card.number.toSecureCard()) + .accessibilityLabel(Text("Card number ending in \(card.number.suffix(4))")) + .accessibilityIdentifier("CardNumber_\(card.id.uuidString)") + .minimumScaleFactor(0.8) } } + .accessibilityElement(children: .combine) + .accessibilityLabel(Text("\(card.description != "" ? card.description : card.name), card ending in \(card.number.suffix(4)), \(card.network.rawValue)")) + .accessibilityHint(Text("Double tap to view card details")) + .accessibilityIdentifier("CardRow_\(card.id.uuidString)") } } } diff --git a/cards/Settings/AppSettingsView.swift b/cards/Settings/AppSettingsView.swift index 7e8c646..df92ff4 100644 --- a/cards/Settings/AppSettingsView.swift +++ b/cards/Settings/AppSettingsView.swift @@ -12,15 +12,28 @@ struct AppSettingsView: View { AnyView ( Section { Toggle("Toggle Biometrics", isOn: UserSettings.shared.$isAuthEnabled) + .accessibilityLabel("Enable biometric authentication") + .accessibilityHint("Double tap to enable or disable biometric authentication for app security") + .accessibilityIdentifier("toggleBiometrics") + .focusable(true) HStack(alignment: .center){ Text("Timeout (in seconds)") + .accessibilityLabel("Authentication timeout in seconds") + .accessibilityIdentifier("timeoutLabel") Spacer() TextField("", value: UserSettings.shared.$authTimeout, format: .number) .keyboardType(.numberPad) .fixedSize() + .accessibilityLabel("Timeout value") + .accessibilityHint("Enter the number of seconds before authentication times out") + .accessibilityIdentifier("timeoutTextField") + .focusable(true) } + .accessibilityElement(children: .combine) VStack(alignment: .leading){ Text("Number of card digits visible on home (Restart Required)") + .accessibilityLabel("Number of card digits visible on home. Restart required.") + .accessibilityIdentifier("cardDigitsLabel") Slider(value: UserSettings.shared.$showNumber, in: 1...10,step: 1) { Text("Steps") } minimumValueLabel: { @@ -28,9 +41,16 @@ struct AppSettingsView: View { } maximumValueLabel: { Text("10") } + .accessibilityLabel("Number of visible card digits") + .accessibilityHint("Adjust to set how many card digits are visible on the home screen. Restart required.") + .accessibilityIdentifier("cardDigitsSlider") + .focusable(true) } + .accessibilityElement(children: .combine) } header: { Text("App Settings") + .accessibilityAddTraits(.isHeader) + .accessibilityIdentifier("appSettingsHeader") } ) }