feat: make the home timeline readable for VoiceOver

This commit is contained in:
CMK 2022-02-10 20:01:52 +08:00
parent 54e84ed814
commit ab4d525cec
8 changed files with 184 additions and 5 deletions

View File

@ -223,3 +223,32 @@ extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
}
}
// MARK: a11y
extension NotificationTableViewCellDelegate where Self: DataSourceProvider {
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void) {
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
return
}
switch item {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .notification(let notification):
assertionFailure("TODO")
default:
assertionFailure("TODO")
}
} // end Task
}
}

View File

@ -431,3 +431,31 @@ extension StatusTableViewCellDelegate where Self: DataSourceProvider {
}
// MARK: a11y
extension StatusTableViewCellDelegate where Self: DataSourceProvider {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void) {
Task {
let source = DataSourceItem.Source(tableViewCell: cell, indexPath: nil)
guard let item = await item(from: source) else {
return
}
switch item {
case .status(let status):
await DataSourceFacade.coordinateToStatusThreadScene(
provider: self,
target: .status, // remove reblog wrapper
status: status
)
case .user(let user):
await DataSourceFacade.coordinateToProfileScene(
provider: self,
user: user
)
case .notification(let notification):
assertionFailure("TODO")
default:
assertionFailure("TODO")
}
}
}
}

View File

@ -29,6 +29,7 @@ protocol NotificationTableViewCellDelegate: AnyObject, AutoGenerateProtocolDeleg
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, statusView: StatusView, actionToolbarContainer: ActionToolbarContainer, buttonDidPressed button: UIButton, action: ActionToolbarContainer.Action)
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
func tableViewCell(_ cell: UITableViewCell, notificationView: NotificationView, accessibilityActivate: Void)
// sourcery:end
}
@ -59,5 +60,9 @@ extension NotificationViewDelegate where Self: NotificationViewContainerTableVie
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta) {
delegate?.tableViewCell(self, notificationView: notificationView, quoteStatusView: quoteStatusView, metaText: metaText, didSelectMeta: meta)
}
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void) {
delegate?.tableViewCell(self, notificationView: notificationView, accessibilityActivate: accessibilityActivate)
}
// sourcery:end
}

View File

@ -20,6 +20,7 @@ final class StatusTableViewCell: UITableViewCell {
weak var delegate: StatusTableViewCellDelegate?
var disposeBag = Set<AnyCancellable>()
var _disposeBag = Set<AnyCancellable>()
let statusView = StatusView()
let separatorLine = UIView.separatorLine
@ -89,6 +90,16 @@ extension StatusTableViewCell {
])
statusView.delegate = self
isAccessibilityElement = true
accessibilityElements = [statusView]
statusView.viewModel.$groupedAccessibilityLabel
.receive(on: DispatchQueue.main)
.sink { [weak self] accessibilityLabel in
guard let self = self else { return }
self.accessibilityLabel = accessibilityLabel
}
.store(in: &_disposeBag)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -96,6 +107,11 @@ extension StatusTableViewCell {
updateContainerViewMarginConstraints()
}
override func accessibilityActivate() -> Bool {
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: Void())
return true
}
}

View File

@ -34,6 +34,7 @@ protocol StatusTableViewCellDelegate: AnyObject, AutoGenerateProtocolDelegate {
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
func tableViewCell(_ cell: UITableViewCell, statusView: StatusView, accessibilityActivate: Void)
// sourcery:end
}
@ -85,5 +86,9 @@ extension StatusViewDelegate where Self: StatusViewContainerTableViewCell {
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton) {
delegate?.tableViewCell(self, statusView: statusView, mediaGridContainerView: mediaGridContainerView, mediaSensitiveButtonDidPressed: button)
}
func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
delegate?.tableViewCell(self, statusView: statusView, accessibilityActivate: accessibilityActivate)
}
// sourcery:end
}

View File

@ -22,6 +22,9 @@ public protocol NotificationViewDelegate: AnyObject {
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, authorAvatarButtonDidPressed button: AvatarButton)
func notificationView(_ notificationView: NotificationView, quoteStatusView: StatusView, metaText: MetaText, didSelectMeta meta: Meta)
// a11y
func notificationView(_ notificationView: NotificationView, accessibilityActivate: Void)
}
public final class NotificationView: UIView {
@ -392,6 +395,10 @@ extension NotificationView: StatusViewDelegate {
assertionFailure()
}
public func statusView(_ statusView: StatusView, accessibilityActivate: Void) {
assertionFailure()
}
}
// MARK: - MastodonMenuDelegate

View File

@ -43,6 +43,7 @@ extension StatusView {
@Published public var timestamp: Date?
public var timestampFormatter: ((_ date: Date) -> String)?
@Published public var timestampText = ""
// Spoiler
@Published public var spoilerContent: MetaContent?
@ -90,6 +91,8 @@ extension StatusView {
public let isNeedsTableViewUpdate = PassthroughSubject<Void, Never>()
@Published public var groupedAccessibilityLabel = ""
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.share()
@ -176,6 +179,7 @@ extension StatusView.ViewModel {
bindToolbar(statusView: statusView)
bindMetric(statusView: statusView)
bindMenu(statusView: statusView)
bindAccessibility(statusView: statusView)
}
private func bindHeader(statusView: StatusView) {
@ -246,11 +250,14 @@ extension StatusView.ViewModel {
return text
}
.removeDuplicates()
.sink { [weak self] text in
guard let _ = self else { return }
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
}
.store(in: &disposeBag)
.assign(to: &$timestampText)
$timestampText
.sink { [weak self] text in
guard let _ = self else { return }
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
}
.store(in: &disposeBag)
}
private func bindContent(statusView: StatusView) {
@ -635,6 +642,84 @@ extension StatusView.ViewModel {
.store(in: &disposeBag)
}
private func bindAccessibility(statusView: StatusView) {
let authorAccessibilityLabel = Publishers.CombineLatest3(
$header,
$authorName,
$timestampText
)
.map { header, authorName, timestamp -> String? in
var strings: [String?] = []
switch header {
case .none:
break
case .reply(let info):
strings.append(info.header.string)
case .repost(let info):
strings.append(info.header.string)
}
strings.append(authorName?.string)
strings.append(timestamp)
return strings.compactMap { $0 }.joined(separator: ", ")
}
let contentAccessibilityLabel = Publishers.CombineLatest3(
$isContentReveal,
$spoilerContent,
$content
)
.map { isContentReveal, spoilerContent, content -> String? in
var strings: [String?] = []
if let spoilerContent = spoilerContent, !spoilerContent.string.isEmpty {
strings.append(L10n.Common.Controls.Status.contentWarning)
strings.append(spoilerContent.string)
}
if isContentReveal {
strings.append(content?.string)
}
return strings.compactMap { $0 }.joined(separator: ", ")
}
let meidaAccessibilityLabel = $mediaViewConfigurations
.map { configurations -> String? in
let count = configurations.count
// TODO: i18n
return count > 0 ? "\(count) media" : nil
}
// TODO: Toolbar
Publishers.CombineLatest3(
authorAccessibilityLabel,
contentAccessibilityLabel,
meidaAccessibilityLabel
)
.map { author, content, media in
let group = [
author,
content,
media
]
return group
.compactMap { $0 }
.joined(separator: ", ")
}
.assign(to: &$groupedAccessibilityLabel)
$groupedAccessibilityLabel
.sink { accessibilityLabel in
statusView.accessibilityLabel = accessibilityLabel
}
.store(in: &disposeBag)
}
}

View File

@ -25,6 +25,10 @@ public protocol StatusViewDelegate: AnyObject {
func statusView(_ statusView: StatusView, spoilerOverlayViewDidPressed overlayView: SpoilerOverlayView)
func statusView(_ statusView: StatusView, spoilerBannerViewDidPressed bannerView: SpoilerBannerView)
func statusView(_ statusView: StatusView, mediaGridContainerView: MediaGridContainerView, mediaSensitiveButtonDidPressed button: UIButton)
// a11y
func statusView(_ statusView: StatusView, accessibilityActivate: Void)
// func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
// func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView)
}