From 2bd83bf46b99f0e029c7f9c1ea2a62d3d0fe8d83 Mon Sep 17 00:00:00 2001 From: Feli Bernutz Date: Wed, 15 May 2024 19:13:03 +0200 Subject: [PATCH] Improve VoiceOver and Voice Control experience (#211) * Settings: Add button trait * Improve VoiceOver on Detail ViewController * Use formatter for duration when possible * Add button accessibility trait to all movie cells * Add more granular context for release date in VoiceOver announcement * Split between accessibilityLabel and accessibilityValue * Fix Xcode warning * Fix condition in formatter --- Cineaste/Base.lproj/Localizable.strings | Bin 11668 -> 12354 bytes Cineaste/Custom UI/Button.swift | 2 + Cineaste/Extension/String+Cineaste.swift | 6 ++- Cineaste/Models/Movie+Formatted.swift | 39 +++++++++++++++++- .../MovieDetailViewController.swift | 17 ++++++++ .../ViewController/Movies/SeenMovieCell.swift | 3 +- .../Movies/WatchlistMovieCell.swift | 14 +++++-- .../SearchMovies/SearchMoviesCell.swift | 25 +++++------ .../Settings/SettingsCell.swift | 1 + Cineaste/de.lproj/Localizable.strings | Bin 11668 -> 12402 bytes Cineaste/en.lproj/Localizable.strings | Bin 11168 -> 11856 bytes 11 files changed, 87 insertions(+), 20 deletions(-) diff --git a/Cineaste/Base.lproj/Localizable.strings b/Cineaste/Base.lproj/Localizable.strings index f10015ab782c84441cebe8e86a4eb51cfd1d10b7..6d99ed8f7cba9f9158dfa3f2b3d69e064423d008 100644 GIT binary patch delta 625 zcmbOdeJEjr0bjj4Ln=caLlKZvU`S+02EtT^Vj#?9NCM&E|6UYR0Yzn#h}l?#h}EH4&*`1R04|H0H>0^v0T@0Y6Coi&VF)TggAm=Jo delta 19 bcmX? String { + static func votingAccessibilityLabel(for vote: String) -> String { String.localizedStringWithFormat(NSLocalizedString("%@ of 10", comment: "Voting description"), vote) } diff --git a/Cineaste/Models/Movie+Formatted.swift b/Cineaste/Models/Movie+Formatted.swift index 937a5bdd..49a8f3a1 100644 --- a/Cineaste/Models/Movie+Formatted.swift +++ b/Cineaste/Models/Movie+Formatted.swift @@ -23,6 +23,14 @@ extension Movie { return release.formatted } + var accessibilityFormattedReleaseDate: String { + guard let release = releaseDate else { + return String.unknownReleaseDate + } + + return String.releasedOnDateAccessibilityLabel + " " + release.formatted + } + var formattedRelativeReleaseInformation: String { guard let release = releaseDate else { return String.unknownReleaseDate @@ -36,12 +44,39 @@ extension Movie { } } + // swiftlint:disable:next identifier_name + var accessibilityFormattedRelativeReleaseInformation: String { + guard let release = releaseDate else { + return String.unknownReleaseDate + } + + let isCurrentYear = release.formattedOnlyYear == Current.date().formattedOnlyYear + if isCurrentYear { + if soonAvailable { + // Release on May 4, 2030 + return String.releaseOnDateAccessibilityLabel + " " + release.formatted + } else { + // Released on May 4, 2023 + return String.releasedOnDateAccessibilityLabel + " " + release.formatted + } + } else { + // Released in 2024 + return String.releasedInYearAccessibilityLabel + " " + release.formattedOnlyYear + } + } + var formattedRuntime: String { - guard runtime != 0 else { + guard let runtime, runtime != 0 else { return "\(String.unknownRuntime) min" } - return "\(runtime?.formatted ?? String.unknownRuntime) min" + if #available(iOS 16.0, *) { + let duration = Duration.seconds(runtime * 60) + let format = duration.formatted(.units(allowed: [.minutes], width: .abbreviated)) + return format + } else { + return "\(runtime.formatted ?? String.unknownRuntime) min" + } } var formattedWatchedDate: String? { diff --git a/Cineaste/ViewController/MovieDetail/MovieDetailViewController.swift b/Cineaste/ViewController/MovieDetail/MovieDetailViewController.swift index 3564376b..7bb898ec 100644 --- a/Cineaste/ViewController/MovieDetail/MovieDetailViewController.swift +++ b/Cineaste/ViewController/MovieDetail/MovieDetailViewController.swift @@ -222,18 +222,35 @@ class MovieDetailViewController: UIViewController { else { return } titleLabel.text = movie.title + titleLabel.accessibilityTraits.insert(.header) + releaseDateAndRuntimeLabel.text = movie.formattedReleaseDate + " ∙ " + movie.formattedRuntime + releaseDateAndRuntimeLabel.accessibilityLabel = movie.accessibilityFormattedReleaseDate + + ", " + + movie.formattedRuntime if !movie.formattedGenres.isEmpty { genreLabel.isHidden = false genreLabel.text = movie.formattedGenres + genreLabel.accessibilityLabel = String.genreAccessibilityLabel + genreLabel.accessibilityValue = movie.formattedGenres } else { genreLabel.isHidden = true } + moreInformationStackView.isAccessibilityElement = true + moreInformationStackView.accessibilityTraits.insert(.link) + moreInformationStackView.accessibilityLabel = [ + moreInformationButton.accessibilityLabel, + buttonInfoLabel.text + ] + .compactMap { $0 } + .joined(separator: " ") + votingLabel.text = movie.formattedVoteAverage + votingLabel.accessibilityLabel = String.votingAccessibilityLabel(for: movie.formattedVoteAverage) descriptionTextView.text = movie.overview posterImageView.loadingImage(from: movie.posterPath, in: .original) } diff --git a/Cineaste/ViewController/Movies/SeenMovieCell.swift b/Cineaste/ViewController/Movies/SeenMovieCell.swift index 64dc2a92..a76fc130 100644 --- a/Cineaste/ViewController/Movies/SeenMovieCell.swift +++ b/Cineaste/ViewController/Movies/SeenMovieCell.swift @@ -43,11 +43,12 @@ class SeenMovieCell: UITableViewCell { private func applyAccessibility(for movie: Movie) { isAccessibilityElement = true + accessibilityTraits.insert(.button) accessibilityLabel = movie.title if let watchedDate = movie.formattedWatchedDate { - accessibilityLabel?.append(", \(watchedDate)") + accessibilityValue = watchedDate } } diff --git a/Cineaste/ViewController/Movies/WatchlistMovieCell.swift b/Cineaste/ViewController/Movies/WatchlistMovieCell.swift index 87077b3e..d1e5ea1a 100644 --- a/Cineaste/ViewController/Movies/WatchlistMovieCell.swift +++ b/Cineaste/ViewController/Movies/WatchlistMovieCell.swift @@ -52,13 +52,19 @@ class WatchlistMovieCell: UITableViewCell { private func applyAccessibility(for movie: Movie) { isAccessibilityElement = true + accessibilityTraits.insert(.button) accessibilityLabel = movie.title - let voting = String.voting(for: movie.formattedVoteAverage) - accessibilityLabel?.append(", \(voting)") - accessibilityLabel?.append(", \(movie.formattedRelativeReleaseInformation)") - accessibilityLabel?.append(", \(movie.formattedRuntime)") + let value = [ + String.votingAccessibilityLabel(for: movie.formattedVoteAverage), + movie.accessibilityFormattedRelativeReleaseInformation, + movie.formattedRuntime + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: ", ") + accessibilityValue = value } private func updatePosterWidthIfNeeded() { diff --git a/Cineaste/ViewController/SearchMovies/SearchMoviesCell.swift b/Cineaste/ViewController/SearchMovies/SearchMoviesCell.swift index 990e3fff..4a1618af 100644 --- a/Cineaste/ViewController/SearchMovies/SearchMoviesCell.swift +++ b/Cineaste/ViewController/SearchMovies/SearchMoviesCell.swift @@ -68,23 +68,24 @@ class SearchMoviesCell: UITableViewCell { private func applyAccessibility(for movie: Movie) { isAccessibilityElement = true + accessibilityTraits.insert(.button) accessibilityLabel = movie.title - if let state = String.state(for: movie.currentWatchState) { - accessibilityLabel?.append(", \(state)") - } - - let voting = String.voting(for: movie.formattedVoteAverage) - accessibilityLabel?.append(", \(voting)") - let isSoonAvailable = !soonHint.isHidden - accessibilityLabel?.append( + let value = [ + String.state(for: movie.currentWatchState), + String.votingAccessibilityLabel(for: movie.formattedVoteAverage), isSoonAvailable - ? ", \(String.soonReleaseInformationLong)" - : "" - ) - accessibilityLabel?.append(", \(movie.formattedRelativeReleaseInformation)") + ? String.soonReleaseInformationLong + : "", + movie.accessibilityFormattedRelativeReleaseInformation + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: ", ") + + accessibilityValue = value } private func updatePosterWidthIfNeeded() { diff --git a/Cineaste/ViewController/Settings/SettingsCell.swift b/Cineaste/ViewController/Settings/SettingsCell.swift index a1a5c1bb..f0230bd8 100644 --- a/Cineaste/ViewController/Settings/SettingsCell.swift +++ b/Cineaste/ViewController/Settings/SettingsCell.swift @@ -28,6 +28,7 @@ class SettingsCell: UITableViewCell { } else { descriptionLabel.isHidden = true } + accessibilityTraits.insert(.button) } } diff --git a/Cineaste/de.lproj/Localizable.strings b/Cineaste/de.lproj/Localizable.strings index f10015ab782c84441cebe8e86a4eb51cfd1d10b7..54b71c88c2db34a8ea9fe09c92469e2c35d2d58c 100644 GIT binary patch delta 629 zcmbOd{V8FC0bjj4Ln=caLlKZvU`S+02EtT^Vj#?9NCM&E|6UYR0Yzn#h}l?#h}EH4&*`1R04|H0rsP9 z0T@USPtYo;Py&h}f3l#Cq)|99+Ka%^QwDTn5yLlN!bk(M@_@k!N;t_389)}uMX;a* E0MQ(BhX4Qo delta 19 bcmeyAFeQ3}0pI2gd;)BY$(s*~1WN({Q3?kS diff --git a/Cineaste/en.lproj/Localizable.strings b/Cineaste/en.lproj/Localizable.strings index 6325e37607b3ddb9f86a6741f05f807f63321d26..f7fb1e0d0551810023456b3f3b3bf2e03b13a248 100644 GIT binary patch delta 691 zcmZ1wej#Q<6>q&eLn=caLlKZvU`S+02EtT^Vj#?9NCM&E|6UYR0Yzn#h}l?#h}EH4&*`1R04|H0?xr$pF>;Sc96N;}w9B0&_Y5DL8z1 delta 23 fcmcZ*vmks!74PH)tRkEF_*StpCU4#;oGt+Xa(xJg