@@ -71,8 +71,16 @@ final class VideoManager: ObservableObject {
7171 }
7272
7373 func deleteExtractedFrame( _ frame: ExtractedFrame ) {
74+ // Remove from app
7475 extractedFrames. removeAll { $0. id == frame. id }
7576
77+ // Delete from photo library if we have the asset identifier
78+ if let assetIdentifier = frame. photoAssetIdentifier {
79+ Task {
80+ try ? await deleteFromPhotoLibrary ( assetIdentifier: assetIdentifier)
81+ }
82+ }
83+
7684 // Provide haptic feedback if enabled
7785 if UserDefaults . standard. bool ( forKey: " hapticFeedback " ) {
7886 let impactFeedback = UIImpactFeedbackGenerator ( style: . light)
@@ -82,8 +90,19 @@ final class VideoManager: ObservableObject {
8290
8391 func deleteExtractedFrames( _ framesToDelete: [ ExtractedFrame ] ) {
8492 let idsToDelete = Set ( framesToDelete. map { $0. id } )
93+
94+ // Remove from app
8595 extractedFrames. removeAll { idsToDelete. contains ( $0. id) }
8696
97+ // Delete from photo library
98+ Task {
99+ for frame in framesToDelete {
100+ if let assetIdentifier = frame. photoAssetIdentifier {
101+ try ? await deleteFromPhotoLibrary ( assetIdentifier: assetIdentifier)
102+ }
103+ }
104+ }
105+
87106 // Provide haptic feedback if enabled
88107 if UserDefaults . standard. bool ( forKey: " hapticFeedback " ) {
89108 let impactFeedback = UIImpactFeedbackGenerator ( style: . medium)
@@ -92,8 +111,20 @@ final class VideoManager: ObservableObject {
92111 }
93112
94113 func clearAllExtractedFrames( ) {
114+ let framesToDelete = extractedFrames
115+
116+ // Remove from app
95117 extractedFrames. removeAll ( )
96118
119+ // Delete from photo library
120+ Task {
121+ for frame in framesToDelete {
122+ if let assetIdentifier = frame. photoAssetIdentifier {
123+ try ? await deleteFromPhotoLibrary ( assetIdentifier: assetIdentifier)
124+ }
125+ }
126+ }
127+
97128 // Provide haptic feedback if enabled
98129 if UserDefaults . standard. bool ( forKey: " hapticFeedback " ) {
99130 let impactFeedback = UIImpactFeedbackGenerator ( style: . heavy)
@@ -125,15 +156,16 @@ final class VideoManager: ObservableObject {
125156 let cgImage = try await imageGenerator. image ( at: markedFrame. timeStamp) . image
126157 let uiImage = UIImage ( cgImage: cgImage)
127158
128- // Save to Photos
129- try await saveImageToPhotos ( uiImage)
159+ // Save to Photos and get asset identifier
160+ let assetIdentifier = try await saveImageToPhotos ( uiImage)
130161
131162 // Add to extracted frames list
132163 let extractedFrame = ExtractedFrame (
133164 id: UUID ( ) ,
134165 originalMarkedFrame: markedFrame,
135166 image: uiImage,
136- extractionDate: Date ( )
167+ extractionDate: Date ( ) ,
168+ photoAssetIdentifier: assetIdentifier
137169 )
138170
139171 extractedFrames. append ( extractedFrame)
@@ -158,83 +190,82 @@ final class VideoManager: ObservableObject {
158190 }
159191 }
160192
161- private func saveImageToPhotos( _ image: UIImage ) async throws {
193+ private func saveImageToPhotos( _ image: UIImage ) async throws -> String ? {
162194 let useCustomAlbum = UserDefaults . standard. bool ( forKey: " useCustomAlbum " )
163195 let customAlbumName = UserDefaults . standard. string ( forKey: " customAlbumName " ) ?? " Frame Extractor "
164196
165- try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
197+ return try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < String ? , Error > ) in
166198 PHPhotoLibrary . requestAuthorization ( for: . addOnly) { status in
167199 guard status == . authorized else {
168200 continuation. resume ( throwing: PhotoLibraryError . notAuthorized)
169201 return
170202 }
171203
172- if useCustomAlbum {
173- // Save to custom album
174- self . saveToCustomAlbum ( image: image, albumName: customAlbumName, continuation: continuation)
175- } else {
176- // Save directly to photo library
177- PHPhotoLibrary . shared ( ) . performChanges {
178- PHAssetChangeRequest . creationRequestForAsset ( from: image)
179- } completionHandler: { success, error in
180- if success {
181- continuation. resume ( )
182- } else {
183- continuation. resume ( throwing: error ?? PhotoLibraryError . saveFailed)
184- }
204+ var assetIdentifier : String ?
205+
206+ PHPhotoLibrary . shared ( ) . performChanges {
207+ let creationRequest = PHAssetChangeRequest . creationRequestForAsset ( from: image)
208+ assetIdentifier = creationRequest. placeholderForCreatedAsset? . localIdentifier
209+
210+ if useCustomAlbum {
211+ // Also add to custom album
212+ self . addToCustomAlbumHelper ( creationRequest: creationRequest, albumName: customAlbumName)
213+ }
214+ } completionHandler: { success, error in
215+ if success {
216+ continuation. resume ( returning: assetIdentifier)
217+ } else {
218+ continuation. resume ( throwing: error ?? PhotoLibraryError . saveFailed)
185219 }
186220 }
187221 }
188222 }
189223 }
190224
191- private func saveToCustomAlbum ( image : UIImage , albumName: String , continuation : CheckedContinuation < Void , Error > ) {
225+ private func addToCustomAlbumHelper ( creationRequest : PHAssetChangeRequest , albumName: String ) {
192226 // First, try to find existing album
193227 let fetchOptions = PHFetchOptions ( )
194228 fetchOptions. predicate = NSPredicate ( format: " title = %@ " , albumName)
195229 let collection = PHAssetCollection . fetchAssetCollections ( with: . album, subtype: . any, options: fetchOptions)
196230
197231 if let album = collection. firstObject {
198- // Album exists, add photo to it
199- addPhotoToAlbum ( image: image, album: album, continuation: continuation)
232+ // Album exists, add asset to it
233+ if let placeholder = creationRequest. placeholderForCreatedAsset {
234+ let albumChangeRequest = PHAssetCollectionChangeRequest ( for: album)
235+ albumChangeRequest? . addAssets ( [ placeholder] as NSArray )
236+ }
200237 } else {
201- // Album doesn't exist, create it first
202- var albumPlaceholder : PHObjectPlaceholder ?
203-
204- PHPhotoLibrary . shared ( ) . performChanges {
205- let createAlbumRequest = PHAssetCollectionChangeRequest . creationRequestForAssetCollection ( withTitle: albumName)
206- albumPlaceholder = createAlbumRequest. placeholderForCreatedAssetCollection
207- } completionHandler: { success, error in
208- if success, let placeholder = albumPlaceholder {
209- let fetchResult = PHAssetCollection . fetchAssetCollections ( withLocalIdentifiers: [ placeholder. localIdentifier] , options: nil )
210- if let album = fetchResult. firstObject {
211- self . addPhotoToAlbum ( image: image, album: album, continuation: continuation)
212- } else {
213- continuation. resume ( throwing: PhotoLibraryError . saveFailed)
214- }
215- } else {
216- continuation. resume ( throwing: error ?? PhotoLibraryError . saveFailed)
217- }
238+ // Create new album and add asset
239+ let albumCreationRequest = PHAssetCollectionChangeRequest . creationRequestForAssetCollection ( withTitle: albumName)
240+ if let placeholder = creationRequest. placeholderForCreatedAsset {
241+ albumCreationRequest. addAssets ( [ placeholder] as NSArray )
218242 }
219243 }
220244 }
221245
222- private func addPhotoToAlbum( image: UIImage , album: PHAssetCollection , continuation: CheckedContinuation < Void , Error > ) {
223- var assetPlaceholder : PHObjectPlaceholder ?
224-
225- PHPhotoLibrary . shared ( ) . performChanges {
226- let createAssetRequest = PHAssetChangeRequest . creationRequestForAsset ( from: image)
227- assetPlaceholder = createAssetRequest. placeholderForCreatedAsset
228-
229- if let albumChangeRequest = PHAssetCollectionChangeRequest ( for: album) ,
230- let placeholder = assetPlaceholder {
231- albumChangeRequest. addAssets ( [ placeholder] as NSArray )
232- }
233- } completionHandler: { success, error in
234- if success {
235- continuation. resume ( )
236- } else {
237- continuation. resume ( throwing: error ?? PhotoLibraryError . saveFailed)
246+ private func deleteFromPhotoLibrary( assetIdentifier: String ) async throws {
247+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation < Void , Error > ) in
248+ PHPhotoLibrary . requestAuthorization ( for: . readWrite) { status in
249+ guard status == . authorized else {
250+ continuation. resume ( throwing: PhotoLibraryError . notAuthorized)
251+ return
252+ }
253+
254+ let fetchResult = PHAsset . fetchAssets ( withLocalIdentifiers: [ assetIdentifier] , options: nil )
255+ guard let asset = fetchResult. firstObject else {
256+ continuation. resume ( ) // Asset already deleted or not found
257+ return
258+ }
259+
260+ PHPhotoLibrary . shared ( ) . performChanges {
261+ PHAssetChangeRequest . deleteAssets ( [ asset] as NSArray )
262+ } completionHandler: { success, error in
263+ if success {
264+ continuation. resume ( )
265+ } else {
266+ continuation. resume ( throwing: error ?? PhotoLibraryError . saveFailed)
267+ }
268+ }
238269 }
239270 }
240271 }
@@ -259,6 +290,7 @@ struct ExtractedFrame: Identifiable {
259290 let originalMarkedFrame : MarkedFrame
260291 let image : UIImage
261292 let extractionDate : Date
293+ let photoAssetIdentifier : String ? // Store the asset identifier for deletion
262294
263295 var imageURL : String {
264296 // For preview purposes - in a real app you might save thumbnails
0 commit comments