Skip to content

Commit ebe7f58

Browse files
committed
Add photo library deletion to frame delete functionality
🗑️ Enhanced Delete Feature: - Delete button now removes frames from BOTH app AND Photos library - Long-press delete removes from Photos library too - Multi-select delete removes all selected frames from Photos library - Stores photo asset identifiers when saving frames 🔧 Technical Implementation: - Updated ExtractedFrame to store photoAssetIdentifier - Modified saveImageToPhotos() to return asset identifier - Added deleteFromPhotoLibrary() method with proper authorization - Enhanced delete methods to handle photo library deletion - Supports both regular photos and custom album deletion - Added proper error handling for photo library operations 📱 User Experience: - Seamless deletion from both app and Photos library - Maintains haptic feedback for all delete operations - Handles permission requests for photo library write access - Graceful handling when assets are already deleted ⚡ Performance: - Asynchronous photo library deletion to avoid UI blocking - Proper continuation-based async/await implementation - Background deletion tasks for bulk operations 📱 Version: 1.5.1
1 parent 1643262 commit ebe7f58

File tree

2 files changed

+87
-55
lines changed

2 files changed

+87
-55
lines changed

FrameExtractionTool/SettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct SettingsView: View {
2727
VStack(alignment: .leading) {
2828
Text("Frame Extractor")
2929
.font(.headline)
30-
Text("Version 1.5.0")
30+
Text("Version 1.5.1")
3131
.font(.caption)
3232
.foregroundStyle(.secondary)
3333
}

FrameExtractionTool/VideoManager.swift

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)