From 611b7c39ea9d84dd5440bc40b7da98f217b0b412 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Wed, 24 Jul 2024 00:30:50 +0200 Subject: [PATCH] Single background context for DB --- App/App_iOS.swift | 4 +- App/App_macOS.swift | 12 ++--- App/SidebarViewController.swift | 4 +- App/SplitViewController.swift | 5 +- Model/DownloadService.swift | 48 +++++++++-------- SwiftUI/Model/Database.swift | 74 +++++++++++++++++---------- SwiftUI/Model/LibraryOperations.swift | 6 +-- SwiftUI/Model/ZimMigration.swift | 13 +++-- ViewModel/BrowserViewModel.swift | 22 ++++---- ViewModel/LibraryViewModel.swift | 21 ++++---- ViewModel/NavigationViewModel.swift | 12 ++--- ViewModel/SearchViewModel.swift | 2 +- Views/Library/Library.swift | 3 +- Views/Library/ZimFileDetail.swift | 5 +- Views/Library/ZimFilesNew.swift | 2 +- Views/Settings/LanguageSelector.swift | 3 +- 16 files changed, 132 insertions(+), 104 deletions(-) diff --git a/App/App_iOS.swift b/App/App_iOS.swift index aac65fcb..9d54ed95 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -36,7 +36,7 @@ struct Kiwix: App { WindowGroup { RootView() .ignoresSafeArea() - .environment(\.managedObjectContext, Database.viewContext) + .environment(\.managedObjectContext, Database.shared.viewContext) .environmentObject(library) .environmentObject(navigation) .modifier(AlertHandler()) @@ -45,7 +45,7 @@ struct Kiwix: App { .modifier(SaveContentHandler()) .onChange(of: scenePhase) { newValue in guard newValue == .inactive else { return } - try? Database.viewContext.save() + try? Database.shared.viewContext.save() } .onOpenURL { url in if url.isFileURL { diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 2566171b..b557be41 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -39,7 +39,7 @@ struct Kiwix: App { var body: some Scene { WindowGroup { RootView() - .environment(\.managedObjectContext, Database.shared.container.viewContext) + .environment(\.managedObjectContext, Database.shared.viewContext) .environmentObject(libraryRefreshViewModel) }.commands { SidebarCommands() @@ -149,7 +149,9 @@ struct RootView: View { case .categories: ZimFilesCategories(dismiss: nil).modifier(LibraryZimFileDetailSidePanel()) case .downloads: - ZimFilesDownloads(dismiss: nil).modifier(LibraryZimFileDetailSidePanel()) + ZimFilesDownloads(dismiss: nil) + .environment(\.managedObjectContext, Database.shared.viewContext) + .modifier(LibraryZimFileDetailSidePanel()) case .new: ZimFilesNew(dismiss: nil).modifier(LibraryZimFileDetailSidePanel()) default: @@ -214,10 +216,8 @@ struct RootView: View { DownloadService.shared.restartHeartbeatIfNeeded() case let .custom(zimFileURL): LibraryOperations.open(url: zimFileURL) { - Task { - await ZimMigration.forCustomApps() - navigation.currentItem = .reading - } + ZimMigration.forCustomApps() + navigation.currentItem = .reading } } } diff --git a/App/SidebarViewController.swift b/App/SidebarViewController.swift index f52e7e53..20d461d1 100644 --- a/App/SidebarViewController.swift +++ b/App/SidebarViewController.swift @@ -40,7 +40,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl }() private let fetchedResultController = NSFetchedResultsController( fetchRequest: Tab.fetchRequest(sortDescriptors: [NSSortDescriptor(key: "created", ascending: false)]), - managedObjectContext: Database.viewContext, + managedObjectContext: Database.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil ) @@ -187,7 +187,7 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl // MARK: - Collection View Configuration private func configureCell(cell: UICollectionViewListCell, indexPath: IndexPath, item: NavigationItem) { - if case let .tab(objectID) = item, let tab = try? Database.viewContext.existingObject(with: objectID) as? Tab { + if case let .tab(objectID) = item, let tab = try? Database.shared.viewContext.existingObject(with: objectID) as? Tab { var config = cell.defaultContentConfiguration() config.text = tab.title ?? "common.tab.menu.new_tab".localized if let zimFile = tab.zimFile, let category = Category(rawValue: zimFile.category) { diff --git a/App/SplitViewController.swift b/App/SplitViewController.swift index 68f921d3..b6ae947e 100644 --- a/App/SplitViewController.swift +++ b/App/SplitViewController.swift @@ -111,7 +111,10 @@ final class SplitViewController: UISplitViewController { let controller = UIHostingController(rootView: ZimFilesCategories(dismiss: nil)) setViewController(UINavigationController(rootViewController: controller), for: .secondary) case .downloads: - let controller = UIHostingController(rootView: ZimFilesDownloads(dismiss: nil)) + let controller = UIHostingController( + rootView: ZimFilesDownloads(dismiss: nil) + .environment(\.managedObjectContext, Database.shared.viewContext) + ) setViewController(UINavigationController(rootViewController: controller), for: .secondary) case .new: let controller = UIHostingController(rootView: ZimFilesNew(dismiss: nil)) diff --git a/Model/DownloadService.swift b/Model/DownloadService.swift index 01154970..0477cade 100644 --- a/Model/DownloadService.swift +++ b/Model/DownloadService.swift @@ -74,16 +74,18 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat let zimFileID = UUID(uuidString: taskDescription) else { return } self.progress.updateFor(uuid: zimFileID, totalBytes: task.countOfBytesReceived) } - self.startHeartbeat() + Task { @MainActor in + self.startHeartbeat() + } } } /// Start heartbeat, which will update database every 0.25 second - private func startHeartbeat() { - DispatchQueue.main.async { + @MainActor private func startHeartbeat() { +// DispatchQueue.main.async { guard self.heartbeat == nil else { return } - self.heartbeat = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in - Database.shared.container.performBackgroundTask { [weak self] context in + self.heartbeat = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in + Database.shared.performBackgroundTask { [weak self] context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump if let progressValues = self?.progress.values() { for (zimFileID, downloadedBytes) in progressValues { @@ -97,17 +99,15 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat } } os_log("Heartbeat started.", log: Log.DownloadService, type: .info) - } +// } } /// Stop heartbeat, which stops periodical database update - private func stopHeartbeat() { - DispatchQueue.main.async { - guard self.heartbeat != nil else { return } - self.heartbeat?.invalidate() - self.heartbeat = nil - os_log("Heartbeat stopped.", log: Log.DownloadService, type: .info) - } + @MainActor private func stopHeartbeat() { + guard self.heartbeat != nil else { return } + self.heartbeat?.invalidate() + self.heartbeat = nil + os_log("Heartbeat stopped.", log: Log.DownloadService, type: .info) } // MARK: - Download Actions @@ -116,9 +116,9 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat /// - Parameters: /// - zimFile: the zim file to download /// - allowsCellularAccess: if using cellular data is allowed - func start(zimFileID: UUID, allowsCellularAccess: Bool) { + @MainActor func start(zimFileID: UUID, allowsCellularAccess: Bool) { requestNotificationAuthorization() - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let fetchRequest = ZimFile.fetchRequest(fileID: zimFileID) guard let zimFile = try? context.fetch(fetchRequest).first, @@ -160,7 +160,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat session.getTasksWithCompletionHandler { _, _, downloadTasks in guard let task = downloadTasks.filter({ $0.taskDescription == zimFileID.uuidString }).first else { return } task.cancel { resumeData in - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let request = DownloadTask.fetchRequest(fileID: zimFileID) guard let downloadTask = try? context.fetch(request).first else { return } @@ -175,7 +175,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat /// - Parameter zimFileID: identifier of the zim file func resume(zimFileID: UUID) { requestNotificationAuthorization() - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let request = DownloadTask.fetchRequest(fileID: zimFileID) @@ -190,14 +190,16 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat downloadTask.resumeData = nil try? context.save() - self.startHeartbeat() + Task { @MainActor in + self.startHeartbeat() + } } } // MARK: - Database private func deleteDownloadTask(zimFileID: UUID) { - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump do { let request = DownloadTask.fetchRequest(fileID: zimFileID) @@ -225,7 +227,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in guard settings.authorizationStatus != .denied else { return } - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in // configure notification content let content = UNMutableNotificationContent() content.title = "download_service.complete.title".localized @@ -248,7 +250,9 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat let zimFileID = UUID(uuidString: taskDescription) else { return } progress.resetFor(uuid: zimFileID) if progress.isEmpty() { - stopHeartbeat() + Task { @MainActor in + stopHeartbeat() + } } // download finished successfully if there's no error @@ -268,7 +272,7 @@ final class DownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegat Note: The result data equality check is used as a trick to distinguish user pausing the download task vs failure. When pausing, the same resume data would have already been saved when the delegate is called. */ - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let request = DownloadTask.fetchRequest(fileID: zimFileID) let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? Data diff --git a/SwiftUI/Model/Database.swift b/SwiftUI/Model/Database.swift index f7c76a54..549400bc 100644 --- a/SwiftUI/Model/Database.swift +++ b/SwiftUI/Model/Database.swift @@ -19,21 +19,36 @@ import os final class Database { static let shared = Database() private var notificationToken: NSObjectProtocol? - private var token: NSPersistentHistoryToken? - private var tokenURL = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("token.data") + private let sync = InSync(label: "database.token") + private var _token: NSPersistentHistoryToken? + private let container: NSPersistentContainer + private let backgroundContext: NSManagedObjectContext + private let backgroundQueue = DispatchQueue(label: "database.background.queue", + qos: .utility, + attributes: [.concurrent]) private init() { + container = Self.createContainer() + backgroundContext = container.newBackgroundContext() + backgroundContext.persistentStoreCoordinator = container.persistentStoreCoordinator + backgroundContext.automaticallyMergesChangesFromParent = false + backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + backgroundContext.undoManager = nil + backgroundContext.shouldDeleteInaccessibleFaults = true + // due to objc++ interop, only the older notification value is working for downloads: // https://developer.apple.com/documentation/coredata/nspersistentstoreremotechangenotification?language=objc let storeChange: NSNotification.Name = .init(rawValue: "NSPersistentStoreRemoteChangeNotification") + notificationToken = NotificationCenter.default.addObserver( forName: storeChange, object: nil, queue: nil) { _ in try? self.mergeChanges() } - token = { + let intialToken: NSPersistentHistoryToken? = { guard let data = UserDefaults.standard.data(forKey: "PersistentHistoryToken") else { return nil } return try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: data) }() + updateToken(intialToken) } deinit { @@ -42,23 +57,33 @@ final class Database { } } - static var viewContext: NSManagedObjectContext { - Database.shared.container.viewContext + var viewContext: NSManagedObjectContext { + container.viewContext } - static func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { - Database.shared.container.performBackgroundTask(block) + func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) { + backgroundQueue.sync { [self] in + backgroundContext.perform { [self] in + block(backgroundContext) + } + } } - static func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) throws -> T) async rethrows -> T { - try await Database.shared.container.performBackgroundTask(block) + private func token() -> NSPersistentHistoryToken? { + sync.read { + self._token + } + } + + private func updateToken(_ value: NSPersistentHistoryToken?) { + sync.execute { + self._token = value + } } /// A persistent container to set up the Core Data stack. - lazy var container: NSPersistentContainer = { - /// - Tag: persistentContainer + private static func createContainer() -> NSPersistentContainer { let container = NSPersistentContainer(name: "DataModel") - guard let description = container.persistentStoreDescriptions.first else { fatalError("Failed to retrieve a persistent store description.") } @@ -84,18 +109,17 @@ final class Database { container.viewContext.undoManager = nil container.viewContext.shouldDeleteInaccessibleFaults = true return container - }() + } /// Save image data to zim files. func saveImageData(url: URL, completion: @escaping (Data) -> Void) { - URLSession.shared.dataTask(with: url) { data, response, _ in + URLSession.shared.dataTask(with: url) { [self] data, response, _ in guard let response = response as? HTTPURLResponse, response.statusCode == 200, let mimeType = response.mimeType, mimeType.contains("image"), let data = data else { return } - let context = self.container.newBackgroundContext() - context.perform { + performBackgroundTask { [data] context in let predicate = NSPredicate(format: "faviconURL == %@", url as CVarArg) let request = ZimFile.fetchRequest(predicate: predicate) guard let zimFile = try? context.fetch(request).first else { return } @@ -108,26 +132,24 @@ final class Database { /// Merge changes performed on batch requests to view context. private func mergeChanges() throws { - let context = container.newBackgroundContext() - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - context.undoManager = nil - context.perform { + performBackgroundTask{ [weak self] context in + guard let self else { return } // fetch and merge changes - let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.token) + let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: self.token()) guard let result = try? context.execute(fetchRequest) as? NSPersistentHistoryResult else { - os_log("no persistent history found after token: \(self.token)") - self.token = nil + os_log("no persistent history found after token: \(self.token())") + self.updateToken(nil) return } guard let transactions = result.result as? [NSPersistentHistoryTransaction] else { - os_log("no transactions in persistent history found after token: \(self.token)") - self.token = nil + os_log("no transactions in persistent history found after token: \(self.token())") + self.updateToken(nil) return } self.container.viewContext.performAndWait { transactions.forEach { transaction in self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification()) - self.token = transaction.token + self.updateToken(transaction.token) } } diff --git a/SwiftUI/Model/LibraryOperations.swift b/SwiftUI/Model/LibraryOperations.swift index c00c9eff..17c8167a 100644 --- a/SwiftUI/Model/LibraryOperations.swift +++ b/SwiftUI/Model/LibraryOperations.swift @@ -43,7 +43,7 @@ struct LibraryOperations { } // upsert zim file in the database - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let predicate = NSPredicate(format: "fileID == %@", metadata.fileID as CVarArg) let fetchRequest = ZimFile.fetchRequest(predicate: predicate) @@ -65,7 +65,7 @@ struct LibraryOperations { /// Reopen zim files from url bookmark data. static func reopen(onComplete: (() -> Void)?) { var successCount = 0 - let context = Database.shared.container.viewContext + let context = Database.shared.viewContext let request = ZimFile.fetchRequest(predicate: ZimFile.Predicate.isDownloaded) guard let zimFiles = try? context.fetch(request) else { @@ -149,7 +149,7 @@ struct LibraryOperations { /// - Parameter zimFile: the zim file to unlink static func unlink(zimFileID: UUID) { ZimFileService.shared.close(fileID: zimFileID) - Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump guard let zimFile = try? ZimFile.fetchRequest(fileID: zimFileID).execute().first else { return } zimFile.bookmarks.forEach { context.delete($0) } diff --git a/SwiftUI/Model/ZimMigration.swift b/SwiftUI/Model/ZimMigration.swift index c41aaf50..aff828e8 100644 --- a/SwiftUI/Model/ZimMigration.swift +++ b/SwiftUI/Model/ZimMigration.swift @@ -33,9 +33,9 @@ enum ZimMigration { sortDescriptors: Self.sortDescriptors ) - static func forCustomApps() async { + static func forCustomApps() { guard FeatureFlags.hasLibrary == false else { return } - await Database.shared.container.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in guard var zimFiles = try? requestLatestZimFile.execute(), zimFiles.count > 1, let latest = zimFiles.popLast() else { @@ -77,17 +77,16 @@ enum ZimMigration { if context.hasChanges { try? context.save() } } + @MainActor private static func latestZimFileHost() async -> String { - if let newHost = await Self.newHost { return newHost } + if let newHost = Self.newHost { return newHost } // if it wasn't set before, set and return by the last ZimFile in DB: - guard let zimFile = try? Database.viewContext.fetch(requestLatestZimFile).first else { + guard let zimFile = try? Database.shared.viewContext.fetch(requestLatestZimFile).first else { fatalError("we should have at least 1 zim file for a custom app") } let newHost = zimFile.fileID.uuidString // save the new host for later - await MainActor.run { - Self.newHost = newHost - } + Self.newHost = newHost return newHost } } diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 7e1d1f4b..b4ba6727 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -104,7 +104,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // Bookmark fetching: bookmarkFetchedResultsController = NSFetchedResultsController( fetchRequest: Bookmark.fetchRequest(), // initially empty - managedObjectContext: Database.viewContext, + managedObjectContext: Database.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil ) @@ -183,7 +183,7 @@ final class BrowserViewModel: NSObject, ObservableObject, private func didUpdate(title: String, url: URL) { let zimFile: ZimFile? = { guard let zimFileID = UUID(uuidString: url.host ?? "") else { return nil } - return try? Database.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first + return try? Database.shared.viewContext.fetch(ZimFile.fetchRequest(fileID: zimFileID)).first }() metaData = ZimFileService.shared.getContentMetaData(url: url) @@ -204,14 +204,14 @@ final class BrowserViewModel: NSObject, ObservableObject, tabID = currentTabID // update tab data - if let tab = try? Database.viewContext.existingObject(with: currentTabID) as? Tab { + if let tab = try? Database.shared.viewContext.existingObject(with: currentTabID) as? Tab { tab.title = articleTitle tab.zimFile = zimFile } } func updateLastOpened() { - guard let tabID, let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { return } + guard let tabID, let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return } tab.lastOpened = Date() } @@ -221,11 +221,11 @@ final class BrowserViewModel: NSObject, ObservableObject, func persistState() { guard let tabID, - let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab else { + let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab else { return } tab.interactionState = webView.interactionState as? Data - try? Database.viewContext.save() + try? Database.shared.viewContext.save() } // MARK: - Content Loading @@ -251,7 +251,7 @@ final class BrowserViewModel: NSObject, ObservableObject, } private func restoreBy(tabID: NSManagedObjectID) { - if let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab { + if let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab { webView.interactionState = tab.interactionState Task { await MainActor.run { @@ -501,7 +501,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // bookmark let bookmarkAction: UIAction = { - let context = Database.viewContext + let context = Database.shared.viewContext let predicate = NSPredicate(format: "articleURL == %@", url as CVarArg) let request = Bookmark.fetchRequest(predicate: predicate) @@ -596,7 +596,7 @@ final class BrowserViewModel: NSObject, ObservableObject, private func createNewTabID() -> NSManagedObjectID { if let tabID { return tabID } - let context = Database.viewContext + let context = Database.shared.viewContext let tab = Tab(context: context) tab.created = Date() tab.lastOpened = Date() @@ -615,7 +615,7 @@ final class BrowserViewModel: NSObject, ObservableObject, func createBookmark(url: URL? = nil) { guard let url = url ?? webView.url else { return } let title = webView.title - Database.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in let bookmark = Bookmark(context: context) bookmark.articleURL = url bookmark.created = Date() @@ -631,7 +631,7 @@ final class BrowserViewModel: NSObject, ObservableObject, func deleteBookmark(url: URL? = nil) { guard let url = url ?? webView.url else { return } - Database.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in let request = Bookmark.fetchRequest(predicate: NSPredicate(format: "articleURL == %@", url as CVarArg)) guard let bookmark = try? context.fetch(request).first else { return } context.delete(bookmark) diff --git a/ViewModel/LibraryViewModel.swift b/ViewModel/LibraryViewModel.swift index 3d74a2bd..afb1102b 100644 --- a/ViewModel/LibraryViewModel.swift +++ b/ViewModel/LibraryViewModel.swift @@ -49,18 +49,12 @@ final class LibraryViewModel: ObservableObject { private var cancellables = Set() private let urlSession: URLSession - private let context: NSManagedObjectContext private var insertionCount = 0 private var deletionCount = 0 @MainActor init(urlSession: URLSession? = nil) { self.urlSession = urlSession ?? URLSession.shared - - context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - context.persistentStoreCoordinator = Database.shared.container.persistentStoreCoordinator - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - context.undoManager = nil process = LibraryProcess.shared state = process.state process.$state.sink { [weak self] newState in @@ -204,11 +198,15 @@ final class LibraryViewModel: ObservableObject { } private func process(parser: Parser) async throws { - try await withCheckedThrowingContinuation { continuation in - context.perform { + try await withCheckedThrowingContinuation { [weak self] continuation -> Void in + Database.shared.performBackgroundTask { [weak self] context in + guard let self else { + continuation.resume() + return + } do { // insert new zim files - let existing = try self.context.fetch(ZimFile.fetchRequest()).map { $0.fileID } + let existing = try context.fetch(ZimFile.fetchRequest()).map { $0.fileID } var zimFileIDs = parser.zimFileIDs.subtracting(existing) let insertRequest = NSBatchInsertRequest( entity: ZimFile.entity(), @@ -224,7 +222,7 @@ final class LibraryViewModel: ObservableObject { } ) insertRequest.resultType = .count - if let result = try self.context.execute(insertRequest) as? NSBatchInsertResult { + if let result = try context.execute(insertRequest) as? NSBatchInsertResult { self.insertionCount = result.result as? Int ?? 0 } @@ -236,10 +234,9 @@ final class LibraryViewModel: ObservableObject { ]) let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) deleteRequest.resultType = .resultTypeCount - if let result = try self.context.execute(deleteRequest) as? NSBatchDeleteResult { + if let result = try context.execute(deleteRequest) as? NSBatchDeleteResult { self.deletionCount = result.result as? Int ?? 0 } - continuation.resume() } catch { os_log("Error saving OPDS Data: %s", log: Log.OPDS, type: .error, error.localizedDescription) diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index 5486b27d..da2fb6af 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -36,7 +36,7 @@ final class NavigationViewModel: ObservableObject { } func navigateToMostRecentTab() { - let context = Database.viewContext + let context = Database.shared.viewContext let fetchRequest = Tab.fetchRequest(sortDescriptors: [NSSortDescriptor(key: "lastOpened", ascending: false)]) fetchRequest.fetchLimit = 1 let tab = (try? context.fetch(fetchRequest).first) ?? self.makeTab(context: context) @@ -50,7 +50,7 @@ final class NavigationViewModel: ObservableObject { @discardableResult func createTab() -> NSManagedObjectID { - let context = Database.viewContext + let context = Database.shared.viewContext let tab = self.makeTab(context: context) #if !os(macOS) currentItem = NavigationItem.tab(objectID: tab.objectID) @@ -61,10 +61,10 @@ final class NavigationViewModel: ObservableObject { @MainActor func tabIDFor(url: URL?) -> NSManagedObjectID { guard let url, - let coordinator = Database.viewContext.persistentStoreCoordinator, + let coordinator = Database.shared.viewContext.persistentStoreCoordinator, let tabID = coordinator.managedObjectID(forURIRepresentation: url), // make sure it's not went missing - let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab, + let tab = try? Database.shared.viewContext.existingObject(with: tabID) as? Tab, tab.zimFile != nil else { return createTab() @@ -75,7 +75,7 @@ final class NavigationViewModel: ObservableObject { /// Delete a single tab, and select another tab /// - Parameter tabID: ID of the tab to delete func deleteTab(tabID: NSManagedObjectID) { - Database.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in let sortByCreation = [NSSortDescriptor(key: "created", ascending: false)] guard let tabs: [Tab] = try? context.fetch(Tab.fetchRequest(predicate: nil, sortDescriptors: sortByCreation)), @@ -107,7 +107,7 @@ final class NavigationViewModel: ObservableObject { /// Delete all tabs, and open a new tab func deleteAllTabs() { - Database.performBackgroundTask { context in + Database.shared.performBackgroundTask { context in // delete all existing tabs let tabs = try? context.fetch(Tab.fetchRequest()) tabs?.forEach { context.delete($0) } diff --git a/ViewModel/SearchViewModel.swift b/ViewModel/SearchViewModel.swift index 446ed70c..a7539d49 100644 --- a/ViewModel/SearchViewModel.swift +++ b/ViewModel/SearchViewModel.swift @@ -34,7 +34,7 @@ class SearchViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDel let predicate = NSPredicate(format: "includedInSearch == true AND fileURLBookmark != nil") fetchedResultsController = NSFetchedResultsController( fetchRequest: ZimFile.fetchRequest(predicate: predicate), - managedObjectContext: Database.shared.container.viewContext, + managedObjectContext: Database.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil ) diff --git a/Views/Library/Library.swift b/Views/Library/Library.swift index a41bd12e..4624a5ff 100644 --- a/Views/Library/Library.swift +++ b/Views/Library/Library.swift @@ -57,6 +57,7 @@ struct Library: View { .navigationTitle(NavigationItem.categories.name) case .downloads: ZimFilesDownloads(dismiss: dismiss) + .environment(\.managedObjectContext, Database.shared.viewContext) case .new: ZimFilesNew(dismiss: dismiss) } @@ -79,7 +80,7 @@ struct Library_Previews: PreviewProvider { NavigationStack { Library(dismiss: nil) .environmentObject(LibraryViewModel()) - .environment(\.managedObjectContext, Database.viewContext) + .environment(\.managedObjectContext, Database.shared.viewContext) } } } diff --git a/Views/Library/ZimFileDetail.swift b/Views/Library/ZimFileDetail.swift index a57f35e8..c144e1a6 100644 --- a/Views/Library/ZimFileDetail.swift +++ b/Views/Library/ZimFileDetail.swift @@ -179,7 +179,10 @@ struct ZimFileDetail: View { } }()), primaryButton: .default(Text("zim_file.action.download.button.anyway".localized)) { - DownloadService.shared.start(zimFileID: zimFile.id, allowsCellularAccess: false) + DownloadService.shared.start( + zimFileID: zimFile.id, + allowsCellularAccess: false + ) }, secondaryButton: .cancel() ) diff --git a/Views/Library/ZimFilesNew.swift b/Views/Library/ZimFilesNew.swift index d899b166..e6522548 100644 --- a/Views/Library/ZimFilesNew.swift +++ b/Views/Library/ZimFilesNew.swift @@ -115,7 +115,7 @@ struct ZimFilesNew_Previews: PreviewProvider { NavigationStack { ZimFilesNew(dismiss: nil) .environmentObject(LibraryViewModel()) - .environment(\.managedObjectContext, Database.viewContext) + .environment(\.managedObjectContext, Database.shared.viewContext) } } } diff --git a/Views/Settings/LanguageSelector.swift b/Views/Settings/LanguageSelector.swift index 6a970e1c..fc5a2a9e 100644 --- a/Views/Settings/LanguageSelector.swift +++ b/Views/Settings/LanguageSelector.swift @@ -165,8 +165,7 @@ class Languages { fetchRequest.resultType = .dictionaryResultType let languages: [Language] = await withCheckedContinuation { continuation in - let context = Database.shared.container.newBackgroundContext() - context.perform { + Database.shared.performBackgroundTask { context in guard let results = try? context.fetch(fetchRequest) else { continuation.resume(returning: []) return