@@ -3,7 +3,23 @@ import PhotosUI
33
44struct ScreenshotPicker: View {
55
6- private let maxScreenshots = 5
6+ enum ViewState : Sendable {
7+ case ready
8+ case loading
9+ case error( Error )
10+
11+ var isLoadingMoreImages : Bool {
12+ guard case . loading = self else { return false }
13+ return true
14+ }
15+
16+ var error : Error ? {
17+ guard case . error( let error) = self else { return nil }
18+ return error
19+ }
20+ }
21+
22+ private let maxScreenshots = 10
723
824 @State
925 private var selectedPhotos : [ PhotosPickerItem ] = [ ]
@@ -12,78 +28,67 @@ struct ScreenshotPicker: View {
1228 private var attachedImages : [ UIImage ] = [ ]
1329
1430 @State
15- private var error : Error ?
31+ private var state : ViewState = . ready
1632
1733 @Binding
1834 var attachedImageUrls : [ URL ]
1935
36+ @State
37+ private var currentUploadSize : CGFloat = 0
38+
39+ let maximumUploadSize : CGFloat ?
40+
41+ @Binding
42+ var uploadLimitExceeded : Bool
43+
2044 var body : some View {
2145 Section {
22- VStack ( alignment: . leading, spacing: 12 ) {
23- Text ( Localization . screenshotsDescription)
24- . font ( . caption)
25- . foregroundColor ( . secondary)
26-
27- // Screenshots display
28- if !attachedImages. isEmpty {
29- ScrollView ( . horizontal, showsIndicators: false ) {
30- LazyHStack ( spacing: 12 ) {
31- ForEach ( Array ( attachedImages. enumerated ( ) ) , id: \. offset) { index, image in
32- ZStack ( alignment: . topTrailing) {
33- Image ( uiImage: image)
34- . resizable ( )
35- . aspectRatio ( contentMode: . fill)
36- . frame ( width: 80 , height: 80 )
37- . clipped ( )
38- . cornerRadius ( 8 )
39-
40- // Remove button
41- Button {
42- // attachedImages will be updated by changing `selectedPhotos`, but not immediately. This line is here to make the UI feel snappy
43- attachedImages. remove ( at: index)
44- selectedPhotos. remove ( at: index)
45- } label: {
46- Image ( systemName: " xmark.circle.fill " )
47- . foregroundColor ( . red)
48- . background ( Color . white, in: Circle ( ) )
49- }
50- . padding ( 4 )
51- }
52- }
53- }
54- . padding ( . horizontal, 2 )
55- }
56- }
46+ Text ( Localization . screenshotsDescription)
47+ . font ( . body)
48+ . foregroundColor ( . secondary)
5749
58- if let error {
50+ if let error = self . state . error {
5951 ErrorView (
6052 title: " Unable to load screenshot " ,
6153 message: error. localizedDescription
62- ) . frame ( maxWidth: . infinity)
54+ )
55+ }
56+
57+ if !attachedImages. isEmpty {
58+ imageGallery
59+ maxSizeIndicator
6360 }
6461
6562 // Add screenshots button
6663 PhotosPicker (
6764 selection: $selectedPhotos,
6865 maxSelectionCount: maxScreenshots,
6966 matching: . images
70- ) { [ imageCount = attachedImages. count] in
67+ ) { [ imageCount = attachedImages. count, isLoading = self . state . isLoadingMoreImages , uploadLimitExceeded = self . uploadLimitExceeded ] in
7168 HStack {
72- Image ( systemName: " camera.fill " )
69+ if isLoading {
70+ ProgressView ( )
71+ . tint ( Color . accentColor)
72+ } else {
73+ Image ( systemName: " camera.fill " )
74+ }
75+
7376 Text ( imageCount == 0 ? Localization . addScreenshots : Localization . addMoreScreenshots)
7477 }
7578 . frame ( maxWidth: . infinity)
7679 . padding ( )
7780 . background ( Color . accentColor. opacity ( 0.1 ) )
78- . foregroundColor ( Color . accentColor)
81+ . foregroundStyle ( uploadLimitExceeded ? Color . gray : Color . accentColor)
7982 . cornerRadius ( 8 )
8083 }
8184 . onChange ( of: selectedPhotos) { _, newItems in
8285 Task {
86+ self . state = . loading
8387 await loadSelectedPhotos ( newItems)
88+ self . state = . ready
8489 }
8590 }
86- }
91+ . disabled ( uploadLimitExceeded )
8792 } header: {
8893 HStack {
8994 Text ( Localization . screenshots)
@@ -92,32 +97,92 @@ struct ScreenshotPicker: View {
9297 . foregroundColor ( . secondary)
9398 }
9499 }
100+ . listRowSeparator ( . hidden)
101+ . selectionDisabled ( )
102+ }
103+
104+ @ViewBuilder
105+ var imageGallery : some View {
106+ // Screenshots display
107+ ScrollView ( . horizontal, showsIndicators: false ) {
108+ LazyHStack ( spacing: 12 ) {
109+ ForEach ( Array ( attachedImages. enumerated ( ) ) , id: \. offset) { index, image in
110+ ZStack ( alignment: . topTrailing) {
111+ Image ( uiImage: image)
112+ . resizable ( )
113+ . aspectRatio ( contentMode: . fill)
114+ . frame ( width: 80 , height: 80 )
115+ . clipped ( )
116+ . cornerRadius ( 8 )
117+
118+ // Remove button
119+ Button {
120+ // attachedImages will be updated by changing `selectedPhotos`, but not immediately. This line is here to make the UI feel snappy
121+ attachedImages. remove ( at: index)
122+ selectedPhotos. remove ( at: index)
123+ } label: {
124+ Image ( systemName: " xmark.circle.fill " )
125+ . foregroundColor ( . red)
126+ . background ( Color . white, in: Circle ( ) )
127+ }
128+ . padding ( 4 )
129+ }
130+ }
131+ }
132+ . padding ( . horizontal, 2 )
133+ }
134+ }
135+
136+ @ViewBuilder
137+ var maxSizeIndicator : some View {
138+ if let maximumUploadSize {
139+ VStack ( alignment: . leading) {
140+ ProgressView ( value: currentUploadSize, total: maximumUploadSize)
141+ . tint ( uploadLimitExceeded ? Color . red : Color . accentColor)
142+
143+ Text ( " Attachment Limit: \( format ( bytes: currentUploadSize) ) / \( format ( bytes: maximumUploadSize) ) " )
144+ . font ( . caption2)
145+ . foregroundStyle ( Color . secondary)
146+ }
147+ }
148+ }
149+
150+ private func format( bytes: CGFloat ) -> String {
151+ ByteCountFormatter ( ) . string ( fromByteCount: Int64 ( bytes) )
95152 }
96153
97154 /// Loads selected photos from PhotosPicker
98155 @MainActor
99156 func loadSelectedPhotos( _ items: [ PhotosPickerItem ] ) async {
100157 var newImages : [ UIImage ] = [ ]
101158 var newUrls : [ URL ] = [ ]
159+ var totalSize : CGFloat = 0
102160
103161 do {
104162 for item in items {
105163 if let data = try await item. loadTransferable ( type: Data . self) {
106164 if let image = UIImage ( data: data) {
107165 newImages. append ( image)
108166 }
167+
168+ totalSize += CGFloat ( data. count)
109169 }
110170
111171 if let file = try await item. loadTransferable ( type: ScreenshotFile . self) {
112172 newUrls. append ( file. url)
113173 }
114174 }
115175
116- attachedImages = newImages
117- attachedImageUrls = newUrls
176+ self. attachedImages = newImages
177+ self. attachedImageUrls = newUrls
178+
179+ withAnimation {
180+ self . currentUploadSize = totalSize
181+ self . uploadLimitExceeded = totalSize > maximumUploadSize ?? . infinity
182+ }
118183 } catch {
119184 withAnimation {
120- self . error = error
185+ self . state = . error( error )
121186 }
122187 }
123188 }
@@ -159,7 +224,11 @@ struct ScreenshotFile: Transferable {
159224
160225 var body : some View {
161226 Form {
162- ScreenshotPicker ( attachedImageUrls: $selectedPhotoUrls)
227+ ScreenshotPicker (
228+ attachedImageUrls: $selectedPhotoUrls,
229+ maximumUploadSize: 10_000_000 ,
230+ uploadLimitExceeded: . constant( false )
231+ )
163232 }
164233 . environmentObject ( SupportDataProvider . testing)
165234 }
0 commit comments