#import "PhotoAlbum.h" @interface SCIPhotoAlbumWatcher : NSObject @property (nonatomic, strong) PHFetchResult *baseline; @property (nonatomic, strong) NSTimer *timeoutTimer; @end static SCIPhotoAlbumWatcher *sciActiveWatcher = nil; @implementation SCIPhotoAlbum + (NSString *)albumName { return @"RyukGram"; } + (void)fetchOrCreateAlbumWithCompletion:(void (^)(PHAssetCollection *, NSError *))completion { PHFetchOptions *opts = [[PHFetchOptions alloc] init]; opts.predicate = [NSPredicate predicateWithFormat:@"title = %@", [self albumName]]; PHFetchResult *result = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:opts]; if (result.count > 0) { if (completion) completion(result.firstObject, nil); return; } __block NSString *placeholderId = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetCollectionChangeRequest *req = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:[self albumName]]; placeholderId = req.placeholderForCreatedAssetCollection.localIdentifier; } completionHandler:^(BOOL success, NSError *error) { if (!success || !placeholderId) { if (completion) completion(nil, error); return; } PHFetchResult *fetched = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[placeholderId] options:nil]; if (completion) completion(fetched.firstObject, nil); }]; } + (void)saveFileToAlbum:(NSURL *)fileURL completion:(void (^)(BOOL, NSError *))completion { [self fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) { if (!album) { dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(NO, err); }); return; } __block NSString *assetId = nil; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ NSString *ext = [[fileURL pathExtension] lowercaseString]; BOOL isVideo = [@[@"mp4", @"mov", @"m4v"] containsObject:ext]; PHAssetCreationRequest *req = [PHAssetCreationRequest creationRequestForAsset]; PHAssetResourceCreationOptions *opts = [[PHAssetResourceCreationOptions alloc] init]; opts.shouldMoveFile = YES; [req addResourceWithType:(isVideo ? PHAssetResourceTypeVideo : PHAssetResourceTypePhoto) fileURL:fileURL options:opts]; req.creationDate = [NSDate date]; assetId = req.placeholderForCreatedAsset.localIdentifier; PHAssetCollectionChangeRequest *albumReq = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:album]; [albumReq addAssets:@[req.placeholderForCreatedAsset]]; } completionHandler:^(BOOL success, NSError *changeErr) { dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(success, changeErr); }); }]; }]; } + (void)watchForNextSavedAsset { // Replace any existing watcher — only the most recent share sheet matters if (sciActiveWatcher) { [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:sciActiveWatcher]; [sciActiveWatcher.timeoutTimer invalidate]; sciActiveWatcher = nil; } if ([PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusAuthorized && [PHPhotoLibrary authorizationStatus] != PHAuthorizationStatusLimited) { return; } SCIPhotoAlbumWatcher *watcher = [[SCIPhotoAlbumWatcher alloc] init]; PHFetchOptions *opts = [[PHFetchOptions alloc] init]; opts.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]]; watcher.baseline = [PHAsset fetchAssetsWithOptions:opts]; [[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:watcher]; // Auto-unregister after 60s in case the user dismisses without saving watcher.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:60.0 repeats:NO block:^(NSTimer *t) { if (sciActiveWatcher == watcher) { [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:watcher]; sciActiveWatcher = nil; } }]; sciActiveWatcher = watcher; } @end @implementation SCIPhotoAlbumWatcher - (void)photoLibraryDidChange:(PHChange *)changeInstance { PHFetchResultChangeDetails *details = [changeInstance changeDetailsForFetchResult:self.baseline]; if (!details || details.insertedObjects.count == 0) return; NSArray *inserted = details.insertedObjects; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ [SCIPhotoAlbum fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) {}]; } completionHandler:^(BOOL success, NSError *error) { // Add the inserted assets to the album in a separate transaction so the // album exists by the time we reference it. [SCIPhotoAlbum fetchOrCreateAlbumWithCompletion:^(PHAssetCollection *album, NSError *err) { if (!album) return; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ PHAssetCollectionChangeRequest *req = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:album]; [req addAssets:inserted]; } completionHandler:nil]; }]; }]; // One-shot [[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self]; [self.timeoutTimer invalidate]; self.timeoutTimer = nil; if (sciActiveWatcher == self) sciActiveWatcher = nil; } @end