diff --git a/piwigo.xcodeproj/project.pbxproj b/piwigo.xcodeproj/project.pbxproj index 1cf40d100..fbf7a45ba 100644 --- a/piwigo.xcodeproj/project.pbxproj +++ b/piwigo.xcodeproj/project.pbxproj @@ -553,8 +553,6 @@ AD18C74127F22B1B002CD05C /* LockOptionsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LockOptionsViewController.storyboard; sourceTree = ""; }; AD196AA02438D2D5006C492A /* ShareMetadataViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareMetadataViewController.swift; sourceTree = ""; }; AD1AC30929746F9E00CEA7A9 /* ImageSessionErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSessionErrors.swift; sourceTree = ""; }; - AD1B92792703A78D0089C5F4 /* AlbumViewController+TransitioningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlbumViewController+TransitioningDelegate.swift"; sourceTree = ""; }; - AD1B927B2703AAD90089C5F4 /* ImageAnimatedTransitioning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageAnimatedTransitioning.swift; sourceTree = ""; }; AD1BB5EA29ADCE330041E24E /* DataModel 0B (Image).xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "DataModel 0B (Image).xcdatamodel"; sourceTree = ""; }; AD1BB5EB29AE82E50041E24E /* MappingModel_0A_to_0B.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = MappingModel_0A_to_0B.xcmappingmodel; sourceTree = ""; }; AD1BFE3D2745A15800858145 /* UIImageView+AppTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+AppTools.swift"; sourceTree = ""; }; @@ -1391,8 +1389,6 @@ ADC69F1A285A4C30009BF9AF /* AlbumViewController+Favorites.swift */, ADBABA352847F4FF00EF39B0 /* AlbumViewController+Search.swift */, AD98FB952843A2F100F6C0C5 /* AlbumViewController+Tagged.swift */, - AD1B92792703A78D0089C5F4 /* AlbumViewController+TransitioningDelegate.swift */, - AD1B927B2703AAD90089C5F4 /* ImageAnimatedTransitioning.swift */, ); path = Extensions; sourceTree = ""; @@ -1570,12 +1566,12 @@ ADD981F528737EFD007213AE /* DataModel.xcdatamodeld */, AD129E6328C34F4E007A2A52 /* Migration Tools */, AD129E6428C34F8E007A2A52 /* Extensions */, - AD41BEC8223E9BB200A5DEE3 /* Location Data */, ADF35E7628B2834F009F3ECE /* Server Data */, AD98E52F28BB8B34008C846A /* User Data */, ADDC36B928CCA46400E5AE4B /* Album Data */, ADDC36BA28CCC0C800E5AE4B /* Image Data */, ADF4001C266D6FBA004352B9 /* Tag Data */, + AD41BEC8223E9BB200A5DEE3 /* Location Data */, AD23A1EF2427C94000F662AB /* Upload Data */, AD018D7E28D7284B0003D5DB /* AttributeTransformers */, ); diff --git a/piwigo/Album/AlbumViewController.swift b/piwigo/Album/AlbumViewController.swift index b8347ba86..f931f4138 100644 --- a/piwigo/Album/AlbumViewController.swift +++ b/piwigo/Album/AlbumViewController.swift @@ -65,11 +65,6 @@ class AlbumViewController: UIViewController, UICollectionViewDelegate, UICollect var imageDetailView: ImageViewController? private var updateOperations = [BlockOperation]() - // See https://medium.com/@tungfam/custom-uiviewcontroller-transitions-in-swift-d1677e5aa0bf -//@property (nonatomic, strong) ImageCollectionViewCell *selectedCell; // Cell that was selected -//@property (nonatomic, strong) UIView *selectedCellImageViewSnapshot; // Snapshot of the image view -//@property (nonatomic, strong) ImageAnimatedTransitioning *animator; // Image cell animator - init(albumId: Int32) { super.init(nibName: nil, bundle: nil) @@ -129,6 +124,20 @@ class AlbumViewController: UIViewController, UICollectionViewDelegate, UICollect }() + // MARK: - Core Data Providers + private lazy var userProvider: UserProvider = { + return UserProvider.shared + }() + + lazy var albumProvider: AlbumProvider = { + return AlbumProvider.shared + }() + + lazy var imageProvider: ImageProvider = { + return ImageProvider.shared + }() + + // MARK: - Core Data Object Contexts lazy var mainContext: NSManagedObjectContext = { let context:NSManagedObjectContext = DataController.shared.mainContext @@ -141,23 +150,6 @@ class AlbumViewController: UIViewController, UICollectionViewDelegate, UICollect }() - // MARK: - Core Data Providers - private lazy var userProvider: UserProvider = { - let provider : UserProvider = UserProvider.shared - return provider - }() - - lazy var albumProvider: AlbumProvider = { - let provider : AlbumProvider = AlbumProvider.shared - return provider - }() - - lazy var imageProvider: ImageProvider = { - let provider : ImageProvider = ImageProvider.shared - return provider - }() - - // MARK: - Core Data Source lazy var user: User = { guard let user = userProvider.getUserAccount(inContext: mainContext) else { @@ -1443,15 +1435,63 @@ class AlbumViewController: UIViewController, UICollectionViewDelegate, UICollect // MARK: - AlbumCollectionViewCellDelegate Methods (+ PushView:) - func deleteCategory(_ albumId: Int32, inParent parentID: Int32, - inMode mode: pwgAlbumDeletionMode) { - // Delete album, sub-albums and images from presistent cache - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - self.albumProvider.deleteAlbum(albumId, inParent: parentID, inMode: mode) + func didDeleteCategory(withError error: NSError?, viewController topViewController: UIViewController?) { + guard let error = error else { + // Remember that the app is fetching all album data + AlbumVars.shared.isFetchingAlbumData.insert(0) + + // Use the AlbumProvider to fetch album data. On completion, + // handle general UI updates and error alerts on the main queue. + let thumnailSize = pwgImageSize(rawValue: AlbumVars.shared.defaultAlbumThumbnailSize) ?? .medium + albumProvider.fetchAlbums(forUser: user, inParentWithId: 0, recursively: true, + thumbnailSize: thumnailSize) { [self] error in + // ► Remove current album from list of album being fetched + AlbumVars.shared.isFetchingAlbumData.remove(0) + + // Check error + guard let error = error as? NSError else { + // No error ► Hide HUD, update + DispatchQueue.main.async { [self] in + topViewController?.updatePiwigoHUDwithSuccess() { + topViewController?.hidePiwigoHUD(afterDelay: kDelayPiwigoHUD) { + // Update number of images in footer + self.updateNberOfImagesInFooter() + } + } + } + return + } + + // Show the error + DispatchQueue.main.async { [self] in + topViewController?.hidePiwigoHUD { + // Display error alert after trying to share image + self.deleteCategoryError(error, viewController: topViewController) + } + } + } + return + } + + // Show the error + DispatchQueue.main.async { [self] in + topViewController?.hidePiwigoHUD { + // Display error alert after trying to share image + self.deleteCategoryError(error, viewController: topViewController) + } + } + } + + private func deleteCategoryError(_ error: NSError, viewController topViewController: UIViewController?) { + DispatchQueue.main.async { + let title = NSLocalizedString("loadingHUD_label", comment: "Loading…") + let message = NSLocalizedString("CoreDataFetch_AlbumError", comment: "Fetch albums error!") + topViewController?.hidePiwigoHUD() { + topViewController?.dismissPiwigoError(withTitle: title, message: message, + errorMessage: error.localizedDescription) { + } + } } - - // Update number of images in footer - updateNberOfImagesInFooter() } @objc diff --git a/piwigo/Album/Cells/AlbumCollectionViewCell+Delete.swift b/piwigo/Album/Cells/AlbumCollectionViewCell+Delete.swift index 59567be92..b26a45fea 100644 --- a/piwigo/Album/Cells/AlbumCollectionViewCell+Delete.swift +++ b/piwigo/Album/Cells/AlbumCollectionViewCell+Delete.swift @@ -201,44 +201,36 @@ extension AlbumCollectionViewCell { guard let albumData = albumData else { return } // Delete the category - NetworkUtilities.checkSession(ofUser: user) { - AlbumUtilities.delete(albumData.pwgID, inMode: deletionMode) { - + NetworkUtilities.checkSession(ofUser: user) { [self] in + AlbumUtilities.delete(albumData.pwgID, inMode: deletionMode) { [self] in + // Hide swipe buttons + DispatchQueue.main.async { + completion(true) + } + // Remove this album from the auto-upload destination if UploadVars.autoUploadCategoryId == albumData.pwgID { UploadVars.autoUploadCategoryId = Int32.min } - // Close HUD, hide swipe button, remove album and images from cache - topViewController?.updatePiwigoHUDwithSuccess() { [self] in - topViewController?.hidePiwigoHUD(afterDelay: kDelayPiwigoHUD) { [self] in - // Hide swipe buttons - completion(true) - - // Delete album and images from cache and update UI - categoryDelegate?.deleteCategory(albumData.pwgID, inParent: albumData.parentId, - inMode: deletionMode) - } - } + // Delete album and images from cache and update UI + self.categoryDelegate?.didDeleteCategory(withError: nil, + viewController: topViewController) } failure: { error in - self.deleteCategoryError(error, viewController: topViewController, - completion: completion) + self.deleteCategoryError(error, viewController: topViewController) } } failure: { error in - self.deleteCategoryError(error, viewController: topViewController, - completion: completion) + self.deleteCategoryError(error, viewController: topViewController) } } - private func deleteCategoryError(_ error: NSError, viewController topViewController: UIViewController?, - completion: @escaping (Bool) -> Void) { + private func deleteCategoryError(_ error: NSError, viewController topViewController: UIViewController?) { DispatchQueue.main.async { let title = NSLocalizedString("deleteCategoryError_title", comment: "Delete Fail") let message = NSLocalizedString("deleteCategoryError_message", comment: "Failed to delete your album") topViewController?.hidePiwigoHUD() { topViewController?.dismissPiwigoError(withTitle: title, message: message, errorMessage: error.localizedDescription) { - completion(true) } } } diff --git a/piwigo/Album/Cells/AlbumCollectionViewCell.swift b/piwigo/Album/Cells/AlbumCollectionViewCell.swift index 5762fbbe9..334293cb5 100644 --- a/piwigo/Album/Cells/AlbumCollectionViewCell.swift +++ b/piwigo/Album/Cells/AlbumCollectionViewCell.swift @@ -15,8 +15,8 @@ import piwigoKit protocol AlbumCollectionViewCellDelegate: NSObjectProtocol { func pushCategoryView(_ viewController: UIViewController?, completion: @escaping (Bool) -> Void) - func deleteCategory(_ albumId: Int32, inParent parentID: Int32, - inMode mode: pwgAlbumDeletionMode) + func didDeleteCategory(withError error: NSError?, + viewController topViewController: UIViewController?) } class AlbumCollectionViewCell: UICollectionViewCell diff --git a/piwigo/Album/Extensions/AlbumViewController+Buttons.swift b/piwigo/Album/Extensions/AlbumViewController+Buttons.swift index 6b296fd60..2331282d8 100644 --- a/piwigo/Album/Extensions/AlbumViewController+Buttons.swift +++ b/piwigo/Album/Extensions/AlbumViewController+Buttons.swift @@ -547,44 +547,42 @@ extension AlbumViewController // Title is name of category setTitleViewFromAlbumData(whileUpdating: false) - // When using several scenes on iPad, buttons might have to be relocated. + // Buttons might have to be relocated: + /// - when using several scenes on iPad + /// - when launching the app in landscape mode on iPhone and returning to the root album in portrait mode if #available(iOS 13.0, *) { - let sizeOfScreen = UIScreen.main.bounds.size - let sizeOfView = view.bounds.size - if sizeOfView.equalTo(sizeOfScreen) == false { - // Calculate reference position - let xPos = view.bounds.size.width - 3 * kRadius - let yPos = view.bounds.size.height - 3 * kRadius - var newFrame = CGRect(x: xPos, y: yPos, width: 2 * kRadius, height: 2 * kRadius) - - // Relocate the "Add" button if needed - if addButton.frame.equalTo(newFrame) == false { - addButton.frame = newFrame - } - - // Relocate the "Upload Queue" button if needed - newFrame = getUploadQueueButtonFrame(isHidden: uploadQueueButton.isHidden) - if uploadQueueButton.frame.equalTo(newFrame) == false { - uploadQueueButton.frame = newFrame - } + // Calculate reference position + let xPos = view.bounds.size.width - 3 * kRadius + let yPos = view.bounds.size.height - 3 * kRadius + var newFrame = CGRect(x: xPos, y: yPos, width: 2 * kRadius, height: 2 * kRadius) + + // Relocate the "Add" button if needed + if addButton.frame.equalTo(newFrame) == false { + addButton.frame = newFrame + } + + // Relocate the "Upload Queue" button if needed + newFrame = getUploadQueueButtonFrame(isHidden: uploadQueueButton.isHidden) + if uploadQueueButton.frame.equalTo(newFrame) == false { + uploadQueueButton.frame = newFrame + } - // Relocate the "Home Album" button if needed - newFrame = getHomeAlbumButtonFrame(isHidden: homeAlbumButton.isHidden) - if homeAlbumButton.frame.equalTo(newFrame) == false { - homeAlbumButton.frame = newFrame - } - - // Relocate "Create Album" button if needed - newFrame = getCreateAlbumButtonFrame(isHidden: createAlbumButton.isHidden) - if createAlbumButton.frame.equalTo(newFrame) == false { - createAlbumButton.frame = newFrame - } - - // Relocate "Upload Images" button if needed - newFrame = getUploadImagesButtonFrame(isHidden: uploadImagesButton.isHidden) - if uploadImagesButton.frame.equalTo(newFrame) == false { - uploadImagesButton.frame = newFrame - } + // Relocate the "Home Album" button if needed + newFrame = getHomeAlbumButtonFrame(isHidden: homeAlbumButton.isHidden) + if homeAlbumButton.frame.equalTo(newFrame) == false { + homeAlbumButton.frame = newFrame + } + + // Relocate "Create Album" button if needed + newFrame = getCreateAlbumButtonFrame(isHidden: createAlbumButton.isHidden) + if createAlbumButton.frame.equalTo(newFrame) == false { + createAlbumButton.frame = newFrame + } + + // Relocate "Upload Images" button if needed + newFrame = getUploadImagesButtonFrame(isHidden: uploadImagesButton.isHidden) + if uploadImagesButton.frame.equalTo(newFrame) == false { + uploadImagesButton.frame = newFrame } } } diff --git a/piwigo/Album/Extensions/AlbumViewController+DataSource.swift b/piwigo/Album/Extensions/AlbumViewController+DataSource.swift index 14dafaa16..6eb05a271 100644 --- a/piwigo/Album/Extensions/AlbumViewController+DataSource.swift +++ b/piwigo/Album/Extensions/AlbumViewController+DataSource.swift @@ -332,16 +332,9 @@ extension AlbumViewController AlbumVars.shared.isFetchingAlbumData.remove(pwgSmartAlbum.favorites.rawValue) // Save changes - do { - try bckgContext.save() - DispatchQueue.main.async { - try? self.mainContext.save() - } - } - catch let error as NSError { - // Remove favorite album from list of album being fetched - AlbumVars.shared.isFetchingAlbumData.remove(pwgSmartAlbum.favorites.rawValue) - print("Could not fetch \(error), \(error.userInfo)") + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } } } diff --git a/piwigo/Album/Extensions/AlbumViewController+Favorites.swift b/piwigo/Album/Extensions/AlbumViewController+Favorites.swift index 7826dedef..d5a68e847 100644 --- a/piwigo/Album/Extensions/AlbumViewController+Favorites.swift +++ b/piwigo/Album/Extensions/AlbumViewController+Favorites.swift @@ -28,7 +28,7 @@ extension AlbumViewController func addImageToFavorites() { guard let imageId = selectedImageIds.first else { // Save changes - try? bckgContext.save() + bckgContext.saveIfNeeded() // Close HUD with success updatePiwigoHUDwithSuccess() { [self] in hidePiwigoHUD(afterDelay: kDelayPiwigoHUD) { [self] in @@ -105,7 +105,7 @@ extension AlbumViewController func removeImageFromFavorites() { guard let imageId = selectedImageIds.first else { // Save changes - try? bckgContext.save() + bckgContext.saveIfNeeded() // Close HUD with success updatePiwigoHUDwithSuccess() { [self] in hidePiwigoHUD(afterDelay: kDelayPiwigoHUD) { [self] in diff --git a/piwigo/Album/Extensions/AlbumViewController+TransitioningDelegate.swift b/piwigo/Album/Extensions/AlbumViewController+TransitioningDelegate.swift deleted file mode 100644 index 4c6a1f568..000000000 --- a/piwigo/Album/Extensions/AlbumViewController+TransitioningDelegate.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// AlbumViewController+TransitioningDelegate.swift -// piwigo -// -// Created by Eddy Lelièvre-Berna on 28/09/2021. -// Copyright © 2021 Piwigo.org. All rights reserved. -// - -//import Foundation -// -//extension AlbumViewController: UIViewControllerTransitioningDelegate { -// -// public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { -// // B2 - 16 -// guard let firstViewController = presenting as? AlbumViewController, -// let secondViewController = presented as? ImageViewController, -// let selectedCellImageViewSnapshot = selectedCellImageViewSnapshot -// else { return nil } -// -// animator = Animator(type: .present, -// firstViewController: firstViewController, -// secondViewController: secondViewController, -// selectedCellImageViewSnapshot: selectedCellImageViewSnapshot) -// return animator -// } -// -// public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { -// return nil -// } -//} diff --git a/piwigo/Album/extensions/ImageAnimatedTransitioning.swift b/piwigo/Album/extensions/ImageAnimatedTransitioning.swift deleted file mode 100644 index 8f67addd3..000000000 --- a/piwigo/Album/extensions/ImageAnimatedTransitioning.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ImageAnimatedTransitioning.swift -// piwigo -// -// Created by Eddy Lelièvre-Berna on 28/09/2021. -// Copyright © 2021 Piwigo.org. All rights reserved. -// - -//import Foundation -//import UIKit -// -//final class ImageAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { -// -// static let duration: TimeInterval = 1.25 -// -// private let type: PresentationType -// private let firstViewController: AlbumViewController -// private let secondViewController: ImageViewController -// private let selectedCellImageViewSnapshot: UIView -// private let cellImageViewRect: CGRect -// -// init?(type: PresentationType, -// firstViewController: AlbumViewController, -// secondViewController: ImageViewController, -// selectedCellImageViewSnapshot: UIView) -// { -// self.type = type -// self.firstViewController = firstViewController -// self.secondViewController = secondViewController -// self.selectedCellImageViewSnapshot = selectedCellImageViewSnapshot -// -// guard let window = firstViewController.view.window ?? secondViewController.view.window, -// let selectedCell = firstViewController.selectedCell -// else { return nil } // i.e. use default present/dismiss animation -// -// // Get frame of image view of the cell relative to the window’s frame -// self.cellImageViewRect = selectedCell.cellImage.convert(selectedCell.cellImage.bounds, to: window) -// } -// -// // Return duration -// func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { -// return Self.duration -// } -// -// // B2 - 13 -// func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { -// // steps 18-20 will be here later. -// } -//} -// -//enum PresentationType { -// case present -// case dismiss -// -// var isPresenting: Bool { -// return self == .present -// } -//} diff --git a/piwigo/In-App Intents/AutoUploadIntenthandler.swift b/piwigo/In-App Intents/AutoUploadIntenthandler.swift index aa9e0fb0b..e3de6a4c6 100644 --- a/piwigo/In-App Intents/AutoUploadIntenthandler.swift +++ b/piwigo/In-App Intents/AutoUploadIntenthandler.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Piwigo.org. All rights reserved. // +import CoreData import Intents import Photos import piwigoKit @@ -14,6 +15,12 @@ import uploadKit @available(iOS 14.0, *) class AutoUploadIntentHandler: NSObject, AutoUploadIntentHandling { + // MARK: - Core Data Object Contexts + private lazy var mainContext: NSManagedObjectContext = { + return DataController.shared.mainContext + }() + + // MARK: - Core Data Providers private lazy var uploadProvider: UploadProvider = { let provider : UploadProvider = UploadManager.shared.uploadProvider @@ -137,7 +144,7 @@ class AutoUploadIntentHandler: NSObject, AutoUploadIntentHandling { lastOperation.completionBlock = { // Save cached data in the main thread DispatchQueue.main.async { - DataController.shared.saveMainContext() + self.mainContext.saveIfNeeded() } debugPrint(" > In-app intent completed with success.") } diff --git a/piwigo/Info.plist b/piwigo/Info.plist index 95e3e7a41..2f6a97d1e 100644 --- a/piwigo/Info.plist +++ b/piwigo/Info.plist @@ -25,7 +25,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 531 + 533 INIntentsSupported AutoUploadIntent diff --git a/piwigo/Supporting Files/AppDelegate.swift b/piwigo/Supporting Files/AppDelegate.swift index 1eabf06dc..fc1f8c5dc 100644 --- a/piwigo/Supporting Files/AppDelegate.swift +++ b/piwigo/Supporting Files/AppDelegate.swift @@ -8,6 +8,7 @@ import AVFoundation import BackgroundTasks +import CoreData import CoreHaptics import Foundation import Intents @@ -31,6 +32,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var isAuthenticatingWithBiometrics = false var didCancelBiometricsAuthentication = false + // MARK: - Core Data Object Contexts + private lazy var mainContext: NSManagedObjectContext = { + return DataController.shared.mainContext + }() + + // MARK: - App Initialisation func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { @@ -307,7 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. // Save cached data in the main thread - DataController.shared.saveMainContext() + mainContext.saveIfNeeded() // Clean up /tmp directory cleanUpTemporaryDirectory(immediately: false) @@ -319,7 +326,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Save data if appropriate. See also applicationDidEnterBackground:. // Save cached data in the main thread - DataController.shared.saveMainContext() + mainContext.saveIfNeeded() // Cancel tasks and close sessions PwgSession.shared.dataSession.invalidateAndCancel() @@ -468,7 +475,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { task.setTaskCompleted(success: true) // Save cached data in the main thread DispatchQueue.main.async { - DataController.shared.saveMainContext() + self.mainContext.saveIfNeeded() } } diff --git a/piwigo/Supporting Files/SceneDelegate.swift b/piwigo/Supporting Files/SceneDelegate.swift index b6636145c..e841b9424 100644 --- a/piwigo/Supporting Files/SceneDelegate.swift +++ b/piwigo/Supporting Files/SceneDelegate.swift @@ -6,10 +6,12 @@ // Copyright © 2020 Piwigo. All rights reserved. // -import UIKit import AVFoundation import BackgroundTasks +import CoreData import LocalAuthentication +import UIKit + import piwigoKit import uploadKit @@ -19,6 +21,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? private var privacyView: UIView? + // MARK: - Core Data Object Contexts + private lazy var mainContext: NSManagedObjectContext = { + return DataController.shared.mainContext + }() + + // MARK: - Connecting and Disconnecting scenes /** Apps configure their UIWindow and attach it to the provided UIWindowScene scene. The system calls willConnectTo shortly after the app delegate's "configurationForConnecting" function. @@ -366,7 +374,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { AppVars.shared.isAppUnlocked = !AppVars.shared.isAppLockActive // Save changes in the app's managed object context when the app transitions to the background. - DataController.shared.saveMainContext() + mainContext.saveIfNeeded() // Schedule background tasks after cancelling pending onces BGTaskScheduler.shared.cancelAllTaskRequests() diff --git a/piwigo/Upload/Pick Local Images/LocalImagesViewController.swift b/piwigo/Upload/Pick Local Images/LocalImagesViewController.swift index 1fe5cfcb2..9aa67b8d7 100644 --- a/piwigo/Upload/Pick Local Images/LocalImagesViewController.swift +++ b/piwigo/Upload/Pick Local Images/LocalImagesViewController.swift @@ -118,9 +118,6 @@ class LocalImagesViewController: UIViewController, UICollectionViewDataSource, U override func viewDidLoad() { super.viewDidLoad() - // Pause UploadManager while sorting images - UploadManager.shared.isPaused = true - // Check collection Id if imageCollectionId.count == 0 { PhotosFetch.shared.showPhotosLibraryAccessRestricted(in: self) diff --git a/piwigoIntents/UploadPhotosHandler.swift b/piwigoIntents/UploadPhotosHandler.swift index 44324a432..60bd0f37d 100644 --- a/piwigoIntents/UploadPhotosHandler.swift +++ b/piwigoIntents/UploadPhotosHandler.swift @@ -14,6 +14,12 @@ import uploadKit @available(iOSApplicationExtension 13.0, *) class UploadPhotosHandler: NSObject, UploadPhotosIntentHandling { + // MARK: - Core Data Object Contexts + private lazy var mainContext: NSManagedObjectContext = { + return DataController.shared.mainContext + }() + + // MARK: - Core Data /** The UploadsProvider that collects upload data, saves it to Core Data, @@ -172,7 +178,7 @@ class UploadPhotosHandler: NSObject, UploadPhotosIntentHandling { print("••> Task completed with success.") // Save cached data in the main thread DispatchQueue.main.async { - DataController.shared.saveMainContext() + self.mainContext.saveIfNeeded() } } diff --git a/piwigoKit/Data Cache/Album Data/AlbumProvider.swift b/piwigoKit/Data Cache/Album Data/AlbumProvider.swift index 9b6a6212e..2582e43fd 100644 --- a/piwigoKit/Data Cache/Album Data/AlbumProvider.swift +++ b/piwigoKit/Data Cache/Album Data/AlbumProvider.swift @@ -16,13 +16,11 @@ public class AlbumProvider: NSObject { // MARK: - Core Data Object Contexts private lazy var mainContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.mainContext - return context + return DataController.shared.mainContext }() private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + return DataController.shared.newTaskContext() }() @@ -108,17 +106,12 @@ public class AlbumProvider: NSObject { } // Save all insertions from the context to the store. - do { - try taskContext.save() - if Thread.isMainThread == false { - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } + taskContext.saveIfNeeded() + if Thread.isMainThread == false { + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } } - catch { - print("Error: \(error)\nCould not save Core Data context.") - } } else { // This album does not exist! // Will select the default album or root album @@ -398,7 +391,7 @@ public class AlbumProvider: NSObject { user.removeUploadRightsToAlbum(withID: ID) } - // Do not delete this album during the last interation of the import + // Do not delete this album during the last iteration of the import albumToDeleteIDs.remove(ID) } catch AlbumError.missingData { @@ -437,43 +430,64 @@ public class AlbumProvider: NSObject { } } - // Delete remaining albums if this is the last iteration - if albumsBatch.count < batchSize, - albumToDeleteIDs.isEmpty == false { - // Check whether the auto-upload category will be deleted - if albumToDeleteIDs.contains(UploadVars.autoUploadCategoryId) { - NotificationCenter.default.post(name: .pwgDisableAutoUpload, object: nil, userInfo: nil) - } + // Delete albums if this is the last iteration + if albumsBatch.count < batchSize { + // Albums not returned by the fetch are deleted first + if albumToDeleteIDs.isEmpty == false { + // Check whether the auto-upload category will be deleted + if albumToDeleteIDs.contains(UploadVars.autoUploadCategoryId) { + NotificationCenter.default.post(name: .pwgDisableAutoUpload, object: nil, userInfo: nil) + } + + // Delete albums not returned by the fetch + let albumsToDelete = cachedAlbums.filter({albumToDeleteIDs.contains($0.pwgID)}) + albumsToDelete.forEach { album in + print("••> delete album with ID:\(album.pwgID) and name:\(album.name)") + bckgContext.delete(album) + } - // Delete albums - let albumsToDelete = cachedAlbums.filter({albumToDeleteIDs.contains($0.pwgID)}) - albumsToDelete.forEach { album in - print("••> delete album with ID:\(album.pwgID) and name:\(album.name)") - bckgContext.delete(album) + // Delete duplicate albums, if any + let otherAlbums = cachedAlbums.filter({albumToDeleteIDs.contains($0.pwgID) == false}) + let duplicates = duplicates(inArray: otherAlbums) + duplicates.forEach { album in + bckgContext.delete(album) + } + } else { + // Delete duplicates if any + let duplicates = duplicates(inArray: cachedAlbums) + duplicates.forEach { album in + bckgContext.delete(album) + } } } // Save all insertions from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } + + // Reset the taskContext to free the cache and lower the memory footprint. + bckgContext.reset() success = true } return (success, albumToDeleteIDs) } + private func duplicates(inArray albums: [Album]) -> [Album] { + var seenID = Set(), duplicates = [Album]() + for album in albums { + let catID = album.pwgID + if seenID.contains(catID) { + duplicates.append(album) + } else { + seenID.insert(catID) + } + } + return duplicates + } + // MARK: - Update Albums /** @@ -534,135 +548,17 @@ public class AlbumProvider: NSObject { } // Save all insertions from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } + + // Reset the taskContext to free the cache and lower the memory footprint. + bckgContext.reset() } } - /** - Delete an album with its sub-albums. - - the attributes 'nbSubAlbums' of parent albums are decremented, - - the number of moved images is subtracted from the attributes 'totalNbImages' of parent albums, - - the attributes 'globalRank' of albums in the parent album are updated accordingly. - N.B.: Sub-albums of the album to delete may not be in cache. - N.B.: Task performed in the background. - */ - public func deleteAlbum(_ catID: Int32, inParent parentID: Int32, - inMode mode: pwgAlbumDeletionMode) { - // Job performed in the background - bckgContext.performAndWait { - - // Get users of this server - let users = userProvider.getUserAccounts(inContext: bckgContext) - - // Loop over all users… - for user in users { - // Retrieve albums in persistent store - let fetchRequest = Album.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Album.pwgID), ascending: true)] - - // Retrieve albums to delete: - /// — from the current server - /// — whose ID is the ID of the album to delete - /// — whose one of the upper album IDs is the ID of the album to delete - var andPredicates = [NSPredicate]() - andPredicates.append(NSPredicate(format: "user.server.path == %@", NetworkVars.serverPath)) - andPredicates.append(NSPredicate(format: "user.username == %@", user.username)) - var orSubpredicates = [NSPredicate]() - orSubpredicates.append(NSPredicate(format: "pwgID == %i", catID)) - orSubpredicates.append(NSPredicate(format: "parentId == %i", catID)) - let regExp = NSRegularExpression.escapedPattern(for: String(catID)) - let pattern = String(format: "(^|.*,)%@(,.*|$)", regExp) - orSubpredicates.append(NSPredicate(format: "upperIds MATCHES %@", pattern)) - andPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: orSubpredicates)) - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: andPredicates) - - // Create a fetched results controller and set its fetch request, context, and delegate. - let controller = NSFetchedResultsController(fetchRequest: fetchRequest, - managedObjectContext: bckgContext, - sectionNameKeyPath: nil, cacheName: nil) - // Perform the fetch. - do { - try controller.performFetch() - } catch { - fatalError("Unresolved error \(error)") - } - let albumsToDelete:[Album] = controller.fetchedObjects ?? [] - - // Delete images if demanded - switch mode { - case .none: // Keep all images - break - case .orphaned: // Delete orphaned image objects (including image files) - albumsToDelete.forEach { album in - if let images = album.images { - images.forEach { image in - if image.albums?.count == 1 { - bckgContext.delete(image) - } - } - } - } - case .all: // Delete all image objects (including image files) - albumsToDelete.forEach { album in - if let images = album.images { - images.forEach { image in - bckgContext.delete(image) - } - } - } - } - - // Update parent album and sub-albums - if let albumToDelete = albumsToDelete.first(where: {$0.pwgID == catID}) { - if parentID == Int32.zero { - // Update ranks of albums and sub-albums in root - updateRankOfAlbums(by: -1, inAlbum: Int32.zero, ofUser: user, - afterRank: albumToDelete.globalRank) - } else { - // Update parent albums and sub-albums - updateParents(removing: albumToDelete) - } - } - - // Delete album and sub-albums - albumsToDelete.forEach { cachedAlbum in - print("••> delete album with ID:\(cachedAlbum.pwgID) and name:\(cachedAlbum.name)") - bckgContext.delete(cachedAlbum) - } - } - - // Save all modifications from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() - } - } - } - public func updateAlbums(addingImages nbImages: Int64, toAlbum album: Album) { // Remove image from album album.nbImages += nbImages diff --git a/piwigoKit/Data Cache/DataController.swift b/piwigoKit/Data Cache/DataController.swift index cff012a23..54bb8c693 100644 --- a/piwigoKit/Data Cache/DataController.swift +++ b/piwigoKit/Data Cache/DataController.swift @@ -43,6 +43,7 @@ public class DataController: NSObject { context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy context.automaticallyMergesChangesFromParent = true context.shouldDeleteInaccessibleFaults = true + context.name = "View context" return context }() @@ -52,18 +53,24 @@ public class DataController: NSObject { context.shouldDeleteInaccessibleFaults = true return context } +} - - // MARK: - Core Data Saving - public func saveMainContext() { - // Anything to save? - guard mainContext.hasChanges else { return } +// MARK: - Core Data Saving +extension NSManagedObjectContext { + /// Only performs a save if there are changes to commit. + /// - Returns: `true` if a save was needed. Otherwise, `false`. + public func saveIfNeeded() { + // Anything to save? + guard hasChanges else { return } + + // Save changes do { - try mainContext.save() - } catch let error as NSError { + try save() + } + catch let error as NSError { // Will try later… - debugPrint("Unresolved error \(error), \(error.userInfo)") + print("Could not save context: \(error), \(error.userInfo)") } } } diff --git a/piwigoKit/Data Cache/Image Data/ImageProvider.swift b/piwigoKit/Data Cache/Image Data/ImageProvider.swift index 400fb3239..c308b0fd4 100644 --- a/piwigoKit/Data Cache/Image Data/ImageProvider.swift +++ b/piwigoKit/Data Cache/Image Data/ImageProvider.swift @@ -16,13 +16,11 @@ public class ImageProvider: NSObject { // MARK: - Core Data Object Contexts private lazy var mainContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.mainContext - return context + return DataController.shared.mainContext }() private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + return DataController.shared.newTaskContext() }() @@ -446,22 +444,13 @@ public class ImageProvider: NSObject { } // Save all insertions from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - if Thread.isMainThread == false { - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } + + // Reset the taskContext to free the cache and lower the memory footprint. + bckgContext.reset() success = true } diff --git a/piwigoKit/Data Cache/Location Data/LocationProvider.swift b/piwigoKit/Data Cache/Location Data/LocationProvider.swift index 867367a7b..7cfa40a1f 100644 --- a/piwigoKit/Data Cache/Location Data/LocationProvider.swift +++ b/piwigoKit/Data Cache/Location Data/LocationProvider.swift @@ -36,13 +36,11 @@ public class LocationProvider: NSObject { // MARK: - Core Data object context private lazy var mainContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.mainContext - return context + return DataController.shared.mainContext }() private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + return DataController.shared.newTaskContext() }() diff --git a/piwigoKit/Data Cache/Server Data/ServerProvider.swift b/piwigoKit/Data Cache/Server Data/ServerProvider.swift index a7a0491c5..85fd056f4 100644 --- a/piwigoKit/Data Cache/Server Data/ServerProvider.swift +++ b/piwigoKit/Data Cache/Server Data/ServerProvider.swift @@ -71,21 +71,6 @@ public class ServerProvider: NSObject { print(error.localizedDescription) taskContext.delete(server) } - - // Save insertion from the context to the store. -// if taskContext.hasChanges { -// do { -// try taskContext.save() -// if Thread.isMainThread == false { -// DispatchQueue.main.async { -// DataController.shared.saveMainContext() -// } -// } -// } -// catch { -// print("Error: \(error)\nCould not save Core Data context.") -// } -// } } } diff --git a/piwigoKit/Data Cache/Tag Data/TagProvider.swift b/piwigoKit/Data Cache/Tag Data/TagProvider.swift index cfc53cb4b..f78c7509e 100644 --- a/piwigoKit/Data Cache/Tag Data/TagProvider.swift +++ b/piwigoKit/Data Cache/Tag Data/TagProvider.swift @@ -17,13 +17,11 @@ public class TagProvider: NSObject { // MARK: - Core Data Object Contexts private lazy var mainContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.mainContext - return context + return DataController.shared.mainContext }() private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + return DataController.shared.newTaskContext() }() @@ -235,23 +233,14 @@ public class TagProvider: NSObject { } // Save all insertions from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - if Thread.isMainThread == false { - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } + // Reset the taskContext to free the cache and lower the memory footprint. + bckgContext.reset() + success = true } return (success, tagToDeleteIDs) diff --git a/piwigoKit/Data Cache/Upload Data/UploadProvider.swift b/piwigoKit/Data Cache/Upload Data/UploadProvider.swift index c0c1cff5c..b981fff2c 100644 --- a/piwigoKit/Data Cache/Upload Data/UploadProvider.swift +++ b/piwigoKit/Data Cache/Upload Data/UploadProvider.swift @@ -17,13 +17,11 @@ public class UploadProvider: NSObject { // MARK: - Core Data Object Contexts private lazy var mainContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.mainContext - return context + return DataController.shared.mainContext }() - private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + public lazy var bckgContext: NSManagedObjectContext = { + return DataController.shared.newTaskContext() }() @@ -168,21 +166,11 @@ public class UploadProvider: NSObject { } // Save all insertions and deletions from the context to the store. - if bckgContext.hasChanges { - do { - try bckgContext.save() - if Thread.isMainThread == false { - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - } - catch { - fatalError("Failure to save context: \(error)") - } - // Reset the taskContext to free the cache and lower the memory footprint. - bckgContext.reset() + bckgContext.saveIfNeeded() + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } + success = true } return success diff --git a/piwigoKit/Data Cache/User Data/UserProvider.swift b/piwigoKit/Data Cache/User Data/UserProvider.swift index e1251a2ed..1f22df141 100644 --- a/piwigoKit/Data Cache/User Data/UserProvider.swift +++ b/piwigoKit/Data Cache/User Data/UserProvider.swift @@ -19,14 +19,12 @@ public class UserProvider: NSObject { // MARK: - Core Data Object Contexts - // private lazy var mainContext: NSManagedObjectContext = { - // let context:NSManagedObjectContext = DataController.shared.mainContext - // return context - // }() + private lazy var mainContext: NSManagedObjectContext = { + return DataController.shared.mainContext + }() private lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context + return DataController.shared.newTaskContext() }() @@ -107,18 +105,10 @@ public class UserProvider: NSObject { } // Save all insertions from the context to the store. - if taskContext.hasChanges { - do { - try taskContext.save() - if Thread.isMainThread == false { - DispatchQueue.main.async { - DataController.shared.saveMainContext() - } - } - } - catch { - print("Error: \(error)\nCould not save Core Data context.") - return + taskContext.saveIfNeeded() + if Thread.isMainThread == false { + DispatchQueue.main.async { + self.mainContext.saveIfNeeded() } } } diff --git a/uploadKit/UploadManager+Finisher.swift b/uploadKit/UploadManager+Finisher.swift index 72d077a3a..1b5fbc6fe 100644 --- a/uploadKit/UploadManager+Finisher.swift +++ b/uploadKit/UploadManager+Finisher.swift @@ -38,14 +38,6 @@ extension UploadManager { // Foreground or background task? if isExecutingBackgroundUploadTask { - // Perform fetch - do { - try uploads.performFetch() - try completed.performFetch() - } - catch { - print("••> Could not fetch pending uploads: \(error)") - } // In background task, launch a transfer if possible if countOfBytesToUpload < maxCountOfBytesToUpload { let prepared = (uploads.fetchedObjects ?? []).filter({$0.state == .prepared}) @@ -64,9 +56,6 @@ extension UploadManager { } else if !isPreparing, isUploading.count <= maxNberOfTransfers { findNextImageToUpload() } - - // Update counter and app badge - self.updateNberOfUploadsToComplete() } @@ -230,7 +219,7 @@ extension UploadManager { // Consider next image backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didFinishTransfer() } } diff --git a/uploadKit/UploadManager+Image.swift b/uploadKit/UploadManager+Image.swift index bbc3234ec..901c550dc 100644 --- a/uploadKit/UploadManager+Image.swift +++ b/uploadKit/UploadManager+Image.swift @@ -92,7 +92,7 @@ extension UploadManager { } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndPreparation() } } diff --git a/uploadKit/UploadManager+Resume.swift b/uploadKit/UploadManager+Resume.swift index fc490af0e..16aa7826b 100644 --- a/uploadKit/UploadManager+Resume.swift +++ b/uploadKit/UploadManager+Resume.swift @@ -22,15 +22,6 @@ extension UploadManager isUploading = Set() print("••> Resume upload operations…") - // Perform fetch - do { - try uploads.performFetch() - try completed.performFetch() - } - catch { - print("••> Could not fetch pending uploads: \(error)") - } - // Get active upload tasks bckgSession.getAllTasks { uploadTasks in // Loop over the tasks @@ -44,7 +35,7 @@ extension UploadManager print("\(self.dbg()) task \(task.taskIdentifier) | no object URI!") continue } - guard let uploadID = self.bckgContext.persistentStoreCoordinator? + guard let uploadID = self.uploadProvider.bckgContext.persistentStoreCoordinator? .managedObjectID(forURIRepresentation: objectURI) else { print("\(self.dbg()) task \(task.taskIdentifier) | no objectID!") continue @@ -173,7 +164,7 @@ extension UploadManager PHAssetChangeRequest.deleteAssets(assetsToDelete as NSFastEnumeration) }, completionHandler: { success, error in if let taskContext = uploads.first?.managedObjectContext, - taskContext == self.bckgContext { + taskContext == self.uploadProvider.bckgContext { DispatchQueue.global(qos: .background).async { self.uploadProvider.delete(uploadRequests: uploads) { _ in } } diff --git a/uploadKit/UploadManager+Tasks.swift b/uploadKit/UploadManager+Tasks.swift index 5a0225b83..bbb51594b 100644 --- a/uploadKit/UploadManager+Tasks.swift +++ b/uploadKit/UploadManager+Tasks.swift @@ -64,7 +64,7 @@ extension UploadManager finishing.forEach({ upload in upload.setState(.finishingError, error: JsonError.networkUnavailable, save: false) }) - try? bckgContext.save() + uploadProvider.bckgContext.saveIfNeeded() findNextImageToUpload() return } @@ -87,7 +87,7 @@ extension UploadManager preparing.forEach { upload in upload.setState(.preparingError, error: UploadError.missingAsset, save: false) } - try? bckgContext.save() + uploadProvider.bckgContext.saveIfNeeded() findNextImageToUpload() return } @@ -312,7 +312,8 @@ extension UploadManager print("\(dbg()) task \(task.taskIdentifier) | no object URI!") continue } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) task \(task.taskIdentifier) | no objectID!") continue } @@ -342,7 +343,8 @@ extension UploadManager print("\(dbg()) task \(task.taskIdentifier) | no object URI!") continue } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) task \(task.taskIdentifier) | no objectID!") continue } diff --git a/uploadKit/UploadManager+Transfer.swift b/uploadKit/UploadManager+Transfer.swift index b14e0aafc..5bcc52db1 100644 --- a/uploadKit/UploadManager+Transfer.swift +++ b/uploadKit/UploadManager+Transfer.swift @@ -57,14 +57,14 @@ extension UploadManager { } failure: { error in upload.setState(.uploadingError, error: error, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } } } failure: { error in upload.setState(.uploadingError, error: error, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } } @@ -81,7 +81,7 @@ extension UploadManager { } // Get image and album objects in cache - let imageSet = self.imageProvider.getImages(inContext: self.bckgContext, withIds: Set([imageID])) + let imageSet = self.imageProvider.getImages(inContext: self.uploadProvider.bckgContext, withIds: Set([imageID])) guard let imageData = imageSet.first, let albums = imageData.albums, let albumData = self.albumProvider.getAlbum(ofUser: upload.user, withId: upload.category) else { @@ -89,7 +89,7 @@ extension UploadManager { userInfo: [NSLocalizedDescriptionKey : UploadError.missingAsset.localizedDescription]) upload.setState(.uploadingFail, error: error, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -113,7 +113,7 @@ extension UploadManager { upload.imageId = imageID upload.setState(.moderated, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -153,21 +153,21 @@ extension UploadManager { upload.imageId = imageID upload.setState(.moderated, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } } } failure: { error in upload.setState(.uploadingError, error: error, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } } } failure: { error in upload.setState(.uploadingError, error: error, save: true) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } } @@ -363,19 +363,20 @@ extension UploadManager { print("\(dbg()) \(md5sum) | no object URI!") return } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) \(md5sum) | no objectID!") return } do { - let upload = try bckgContext.existingObject(with: uploadID) as! Upload + let upload = try uploadProvider.bckgContext.existingObject(with: uploadID) as! Upload // Update upload request status if let error = error as NSError? { self.backgroundQueue.async { upload.setState(.uploadingError, error: error, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } @@ -390,14 +391,14 @@ extension UploadManager { upload.setState(.uploadingError, error: error, save: false) } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } else { upload.setState(.uploadingError, error: JsonError.networkUnavailable, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } @@ -436,12 +437,13 @@ extension UploadManager { print("\(dbg()) \(md5sum) | no object URI!") return } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) \(md5sum) | no objectID!") return } do { - let upload = try bckgContext.existingObject(with: uploadID) as! Upload + let upload = try uploadProvider.bckgContext.existingObject(with: uploadID) as! Upload if upload.isFault { // The upload request is not fired yet. upload.willAccessValue(forKey: nil) @@ -456,7 +458,7 @@ extension UploadManager { #endif upload.setState(.uploadingError, error: JsonError.emptyJSONobject, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -470,7 +472,7 @@ extension UploadManager { #endif upload.setState(.uploadingError, error: JsonError.invalidJSONobject, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -491,7 +493,7 @@ extension UploadManager { upload.setState(.uploadingError, error: error, save: false) } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -527,7 +529,7 @@ extension UploadManager { upload.imageId = uploadJSON.data.image_id! upload.setState(.uploaded, save: false) backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -535,7 +537,7 @@ extension UploadManager { // Data cannot be digested, image still ready for upload upload.setState(.uploadingError, error: UploadError.wrongJSONobject, save: false) backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -734,7 +736,8 @@ extension UploadManager { print("\(dbg()) \(md5sum) | no object URI!") return } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) \(md5sum) | no objectID!") return } @@ -756,7 +759,7 @@ extension UploadManager { if let error = error as NSError? { upload.setState(.uploadingError, error: error, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } @@ -770,14 +773,14 @@ extension UploadManager { upload.setState(.uploadingError, error: error, save: false) } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } else { upload.setState(.uploadingError, error: JsonError.networkUnavailable, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } } @@ -804,7 +807,8 @@ extension UploadManager { print("\(dbg()) \(md5sum) | no object URI!") return } - guard let uploadID = bckgContext.persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { + guard let uploadID = uploadProvider.bckgContext + .persistentStoreCoordinator?.managedObjectID(forURIRepresentation: objectURI) else { print("\(dbg()) \(md5sum) | no objectID!") return } @@ -830,7 +834,7 @@ extension UploadManager { #endif upload.setState(.uploadingError, error: JsonError.emptyJSONobject, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -844,7 +848,7 @@ extension UploadManager { #endif upload.setState(.uploadingError, error: JsonError.invalidJSONobject, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -866,7 +870,7 @@ extension UploadManager { upload.setState(.uploadingError, error: error, save: false) } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -906,7 +910,7 @@ extension UploadManager { upload.comment = NetworkUtilities.utf8mb4String(from: getInfos.comment ?? "") if let tags = getInfos.tags { let tagIDs = tags.compactMap({$0.id}).map({$0.stringValue + ","}).reduce("", +).dropLast() - let newTagIDs = tagProvider.getTags(withIDs: String(tagIDs), taskContext: bckgContext).map({$0.objectID}) + let newTagIDs = tagProvider.getTags(withIDs: String(tagIDs), taskContext: uploadProvider.bckgContext).map({$0.objectID}) var newTags = Set() newTagIDs.forEach({ if let copy = upload.managedObjectContext?.object(with: $0) as? Tag { @@ -917,7 +921,7 @@ extension UploadManager { } // Add uploaded image to cache and update UI if needed - if let user = userProvider.getUserAccount(inContext: bckgContext), + if let user = userProvider.getUserAccount(inContext: uploadProvider.bckgContext), user.hasAdminRights { getInfos.fixingUnknowns() imageProvider.didUploadImage(getInfos, asVideo: upload.isVideo, @@ -940,7 +944,7 @@ extension UploadManager { } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload) } return @@ -949,7 +953,7 @@ extension UploadManager { print("\(dbg()) \(md5sum) | wrong JSON object!") upload.setState(.uploadingError, error: UploadError.wrongJSONobject, save: false) self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndTransfer(for: upload, taskID: task.taskIdentifier) } return @@ -985,9 +989,6 @@ extension UploadManager { } func didEndTransfer(for upload: Upload) { - // Update counter and app badge - self.updateNberOfUploadsToComplete() - // Update list of current uploads if let index = isUploading.firstIndex(where: {$0 == upload.objectID}) { isUploading.remove(at: index) diff --git a/uploadKit/UploadManager+Video.swift b/uploadKit/UploadManager+Video.swift index 7aea426d3..5ccc16ab0 100644 --- a/uploadKit/UploadManager+Video.swift +++ b/uploadKit/UploadManager+Video.swift @@ -177,7 +177,7 @@ extension UploadManager { } self.backgroundQueue.async { - try? self.bckgContext.save() + self.uploadProvider.bckgContext.saveIfNeeded() self.didEndPreparation() } } diff --git a/uploadKit/UploadManager.swift b/uploadKit/UploadManager.swift index 7951cf33d..1dc3fc42d 100644 --- a/uploadKit/UploadManager.swift +++ b/uploadKit/UploadManager.swift @@ -112,6 +112,15 @@ public class UploadManager: NSObject { public override init() { super.init() + // Perform fetch + do { + try uploads.performFetch() + try completed.performFetch() + } + catch { + print("••> Could not fetch pending uploads: \(error)") + } + // Register auto-upload disabler NotificationCenter.default.addObserver(self, selector: #selector(stopAutoUploader(_:)), name: .pwgDisableAutoUpload, object: nil) @@ -123,40 +132,34 @@ public class UploadManager: NSObject { } - // MARK: - Core Data Object Contexts - lazy var bckgContext: NSManagedObjectContext = { - let context:NSManagedObjectContext = DataController.shared.newTaskContext() - return context - }() - - // MARK: - Core Data Providers lazy var userProvider: UserProvider = { - let provider : UserProvider = UserProvider.shared - return provider + return UserProvider.shared }() lazy var albumProvider: AlbumProvider = { - let provider : AlbumProvider = AlbumProvider.shared - return provider + return AlbumProvider.shared }() lazy var imageProvider: ImageProvider = { - let provider : ImageProvider = ImageProvider.shared - return provider + return ImageProvider.shared }() lazy var tagProvider: TagProvider = { - let provider : TagProvider = TagProvider.shared - return provider + return TagProvider.shared }() public lazy var uploadProvider: UploadProvider = { - let provider : UploadProvider = UploadProvider.shared - return provider + return UploadProvider.shared }() + // MARK: - Core Data Object Context + lazy var bckgContext: NSManagedObjectContext = { + return uploadProvider.bckgContext + }() + + // MARK: - Core Data Source lazy var fetchPendingRequest: NSFetchRequest = { let fetchRequest = Upload.fetchRequest() @@ -177,9 +180,10 @@ public class UploadManager: NSObject { public lazy var uploads: NSFetchedResultsController = { let uploads = NSFetchedResultsController(fetchRequest: fetchPendingRequest, - managedObjectContext: self.bckgContext, + managedObjectContext: self.uploadProvider.bckgContext, sectionNameKeyPath: nil, cacheName: nil) + uploads.delegate = self return uploads }() @@ -202,9 +206,37 @@ public class UploadManager: NSObject { public lazy var completed: NSFetchedResultsController = { let uploads = NSFetchedResultsController(fetchRequest: fetchCompletedRequest, - managedObjectContext: self.bckgContext, + managedObjectContext: self.uploadProvider.bckgContext, sectionNameKeyPath: nil, cacheName: nil) return uploads }() } + + +// MARK: - NSFetchedResultsControllerDelegate +extension UploadManager: NSFetchedResultsControllerDelegate { + + public func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + + switch type { + case .insert: + // Check whether this upload request can be launched in the foreground + if isExecutingBackgroundUploadTask == false { + findNextImageToUpload() + } + // Update number of uploads to complete + updateNberOfUploadsToComplete() + + case .delete: + // Update number of uploads to complete + updateNberOfUploadsToComplete() + + case .move, .update: + break + + @unknown default: + fatalError("UploadManager: unknown NSFetchedResultsChangeType") + } + } +}