Single background context for DB

This commit is contained in:
Balazs Perlaki-Horvath 2024-07-24 00:30:50 +02:00
parent ef8d6b55c2
commit 611b7c39ea
16 changed files with 132 additions and 104 deletions

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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) {

View File

@ -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))

View File

@ -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

View File

@ -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<T>(_ 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)
}
}

View File

@ -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) }

View File

@ -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
}
}

View File

@ -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)

View File

@ -49,18 +49,12 @@ final class LibraryViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
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)

View File

@ -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) }

View File

@ -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
)

View File

@ -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)
}
}
}

View File

@ -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()
)

View File

@ -115,7 +115,7 @@ struct ZimFilesNew_Previews: PreviewProvider {
NavigationStack {
ZimFilesNew(dismiss: nil)
.environmentObject(LibraryViewModel())
.environment(\.managedObjectContext, Database.viewContext)
.environment(\.managedObjectContext, Database.shared.viewContext)
}
}
}

View File

@ -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