Skip to content

Commit 71bf0dd

Browse files
Merge pull request #5 from markbattistella/accessibility-cleanup
Improve note accessibility
2 parents 07d1fdb + f9fa373 commit 71bf0dd

7 files changed

Lines changed: 94 additions & 26 deletions

Sources/Notelet/BulletListNoteItemView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct BulletListNoteItemView: View {
2626
.font(.system(size: 32).weight(.semibold))
2727
.foregroundStyle(accentColor)
2828
.frame(width: 48, alignment: .center)
29+
.accessibilityHidden(true)
2930

3031
VStack(alignment: .leading, spacing: 2) {
3132
Text(row.title)
@@ -36,6 +37,7 @@ struct BulletListNoteItemView: View {
3637
.foregroundStyle(.secondary)
3738
}
3839
}
40+
.accessibilityElement(children: .combine)
3941
}
4042
}
4143
}

Sources/Notelet/MediaNoteItemImageView.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,37 @@ import SwiftUI
99

1010
struct MediaNoteItemImageView: View {
1111
let imageUrl: URL
12+
let onLoadStateChange: (MediaNoteItemLoadState) -> Void
1213

1314
var body: some View {
1415
AsyncImage(url: imageUrl) { phase in
1516
switch phase {
1617
case .empty:
1718
ProgressView()
19+
.onAppear {
20+
onLoadStateChange(.loading)
21+
}
1822
case .success(let image):
1923
image
2024
.resizable()
2125
.scaledToFill()
26+
.onAppear {
27+
onLoadStateChange(.loaded)
28+
}
2229
case .failure:
2330
Image(systemName: "photo.on.rectangle.angled")
2431
.resizable()
2532
.scaledToFit()
2633
.padding(40)
2734
.foregroundStyle(.secondary)
35+
.onAppear {
36+
onLoadStateChange(.failed)
37+
}
2838
@unknown default:
2939
ProgressView()
40+
.onAppear {
41+
onLoadStateChange(.loading)
42+
}
3043
}
3144
}
3245
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// MediaNoteItemLoadState.swift
3+
// Notelet
4+
//
5+
6+
enum MediaNoteItemLoadState {
7+
case loading
8+
case loaded
9+
case failed
10+
}

Sources/Notelet/MediaNoteItemVideoView.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ import UIKit
1212
struct MediaNoteItemVideoView: View {
1313
let videoURL: URL
1414
let isPlaying: Bool
15-
15+
let onLoadStateChange: (MediaNoteItemLoadState) -> Void
16+
1617
@State private var player: AVQueuePlayer?
1718
@State private var playerLooper: AVPlayerLooper?
1819
@State private var videoStatusObserver: NSKeyValueObservation?
1920
@State private var isVideoLoading: Bool = true
20-
21+
2122
var body: some View {
2223
ZStack {
2324
AspectFillVideoPlayer(player: player)
@@ -45,6 +46,7 @@ struct MediaNoteItemVideoView: View {
4546
videoStatusObserver = nil
4647

4748
isVideoLoading = true
49+
onLoadStateChange(.loading)
4850

4951
let asset = AVURLAsset(url: videoURL)
5052
let playerItem = AVPlayerItem(asset: asset)
@@ -57,8 +59,12 @@ struct MediaNoteItemVideoView: View {
5759
videoStatusObserver = queuePlayer.observe(\.currentItem?.status, options: [.initial, .new]) { observedPlayer, _ in
5860
Task { @MainActor in
5961
switch observedPlayer.currentItem?.status {
60-
case .readyToPlay, .failed:
62+
case .readyToPlay:
6163
isVideoLoading = false
64+
onLoadStateChange(.loaded)
65+
case .failed:
66+
isVideoLoading = false
67+
onLoadStateChange(.failed)
6268
default:
6369
break
6470
}
@@ -88,7 +94,7 @@ fileprivate struct AspectFillVideoPlayer: UIViewRepresentable {
8894
layer as! AVPlayerLayer
8995
}
9096
}
91-
97+
9298
let player: AVPlayer?
9399

94100
func makeUIView(context: Context) -> PlayerView {

Sources/Notelet/NoteItemView.swift

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ struct NoteItemView: View {
1111
let item: NoteletVersionNoteItem
1212
let isCurrent: Bool
1313
let configuration: NoteletConfiguration
14-
14+
1515
@State private var containerSize: CGSize = .zero
16-
16+
@State private var mediaLoadState: MediaNoteItemLoadState = .loading
17+
1718
private var clipShapeRadius: Double {
1819
if #available(iOS 26, *) {
1920
24
2021
} else {
2122
12
2223
}
2324
}
24-
25+
2526
var body: some View {
2627
VStack(alignment: .leading, spacing: 0) {
2728
switch item {
@@ -35,17 +36,19 @@ struct NoteItemView: View {
3536
* iPads and iPhones in landscape orientation.
3637
*/
3738
let containerWidth = min(max(containerSize.width, 300), 440)
38-
39+
3940
ZStack {
4041
switch mediaKind {
4142
case .image:
4243
MediaNoteItemImageView(
43-
imageUrl: url
44+
imageUrl: url,
45+
onLoadStateChange: { mediaLoadState = $0 }
4446
)
4547
case .video:
4648
MediaNoteItemVideoView(
4749
videoURL: url,
48-
isPlaying: isCurrent
50+
isPlaying: isCurrent,
51+
onLoadStateChange: { mediaLoadState = $0 }
4952
)
5053
}
5154
}
@@ -56,14 +59,23 @@ struct NoteItemView: View {
5659
.clipShape(.rect(cornerRadius: clipShapeRadius))
5760
.shadow(color: .black.opacity(0.15), radius: 20, x: 0, y: 0)
5861
.padding(mediaPadding)
59-
62+
6063
MediaNoteItemDetailsView(
6164
title: title,
6265
description: description
6366
)
64-
67+
6568
Spacer()
6669
}
70+
.accessibilityElement(children: .ignore)
71+
.accessibilityLabel(
72+
mediaAccessibilityLabel(
73+
kind: mediaKind,
74+
loadState: mediaLoadState,
75+
title: title,
76+
description: description
77+
)
78+
)
6779
case .list(let title, let rows):
6880
BulletListNoteItemView(
6981
title: title,
@@ -79,4 +91,26 @@ struct NoteItemView: View {
7991
containerSize = newSize
8092
}
8193
}
94+
95+
private func mediaAccessibilityLabel(
96+
kind: NoteletVersionNoteItem.MediaKind,
97+
loadState: MediaNoteItemLoadState,
98+
title: LocalizedStringResource,
99+
description: LocalizedStringResource
100+
) -> Text {
101+
switch (kind, loadState) {
102+
case (.image, .loading):
103+
Text("Loading image. \(title). \(description)")
104+
case (.image, .loaded):
105+
Text("Image. \(title). \(description)")
106+
case (.image, .failed):
107+
Text("Image failed to load. \(title). \(description)")
108+
case (.video, .loading):
109+
Text("Loading video. \(title). \(description)")
110+
case (.video, .loaded):
111+
Text("Video. \(title). \(description)")
112+
case (.video, .failed):
113+
Text("Video failed to load. \(title). \(description)")
114+
}
115+
}
82116
}

Sources/Notelet/NoteletSheetContentView.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import SwiftUI
1010
struct NoteletSheetContentView: View {
1111
let versionNotes: [NoteletVersionNoteItem]
1212
let configuration: NoteletConfiguration
13-
13+
1414
@State private var selectedPageID: Int? = 0
1515

1616
@Environment(\.dismiss) private var dismiss
@@ -19,24 +19,24 @@ struct NoteletSheetContentView: View {
1919
private var currentPage: Int {
2020
return selectedPageID ?? 0
2121
}
22-
22+
2323
private var isIPad: Bool {
2424
UIDevice.current.userInterfaceIdiom == .pad
2525
}
2626

2727
private var sheetBackgroundStyle: AnyShapeStyle {
2828
if #available(iOS 26, *) {
2929
let color: Color = colorScheme == .dark ? .black : .white
30-
30+
3131
return AnyShapeStyle(color.opacity(0.55))
3232
}
3333

3434
return AnyShapeStyle(.regularMaterial)
3535
}
36-
36+
3737
var body: some View {
3838
let isOnLastPage = isOnLastPage(versionNotes: versionNotes, currentPage: currentPage)
39-
39+
4040
NavigationStack {
4141
ScrollView(.horizontal) {
4242
HStack(alignment: .top, spacing: 0) {
@@ -63,18 +63,21 @@ struct NoteletSheetContentView: View {
6363
VStack(spacing: 16) {
6464
if versionNotes.count > 1 {
6565
let selectedIndicatorColor = colorScheme == .light ? Color.black : Color.white
66-
66+
let pageIndicatorAccessibilityLabel: LocalizedStringResource = "Page \(currentPage + 1) of \(versionNotes.count)"
67+
6768
HStack(spacing: 6) {
6869
ForEach(versionNotes.indices, id: \.self) { index in
6970
Capsule()
7071
.fill(index == currentPage ? selectedIndicatorColor.opacity(0.35) : Color.secondary.opacity(0.35))
7172
.frame(width: index == currentPage ? 14 : 7, height: 7)
7273
}
7374
}
75+
.accessibilityElement(children: .ignore)
76+
.accessibilityLabel(Text(pageIndicatorAccessibilityLabel))
7477
.padding(.top, 14)
7578
.animation(.easeInOut(duration: 0.2), value: currentPage)
7679
}
77-
80+
7881
if !versionNotes.isEmpty {
7982
Button {
8083
if isOnLastPage {
@@ -88,7 +91,7 @@ struct NoteletSheetContentView: View {
8891
let buttonTitle: LocalizedStringResource = isOnLastPage
8992
? configuration.doneButtonLabel
9093
: configuration.nextButtonLabel
91-
94+
9295
Text(buttonTitle)
9396
.fontWeight(.bold)
9497
.foregroundStyle(.primary)
@@ -109,11 +112,11 @@ struct NoteletSheetContentView: View {
109112
.presentationDragIndicator(.visible)
110113
.presentationBackground(sheetBackgroundStyle)
111114
}
112-
115+
113116
private func onDoneTap() {
114117
dismiss()
115118
}
116-
119+
117120
private func isOnLastPage(versionNotes: [NoteletVersionNoteItem], currentPage: Int) -> Bool {
118121
guard !versionNotes.isEmpty else {
119122
return true

Sources/Notelet/SafeAreaView.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@
77

88
import SwiftUI
99

10-
struct SafeAreaView<SafeAreContent: View>: ViewModifier {
11-
@ViewBuilder var safeAreContent: () -> SafeAreContent
10+
struct SafeAreaView<SafeAreaContent: View>: ViewModifier {
11+
@ViewBuilder var safeAreaContent: () -> SafeAreaContent
1212

1313
func body(content: Content) -> some View {
1414
if #available(iOS 26, *) {
1515
content
1616
.safeAreaBar(edge: .bottom) {
17-
safeAreContent()
17+
safeAreaContent()
1818
}
1919
} else {
2020
content
2121
.safeAreaInset(edge: .bottom, spacing: 0) {
22-
safeAreContent()
22+
safeAreaContent()
2323
.frame(maxWidth: .infinity)
2424
.background {
2525
Rectangle()

0 commit comments

Comments
 (0)