From ddb5a33e965681d23b7eaa9b51f1754bfbdf4217 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sat, 9 Mar 2024 12:57:50 +0100 Subject: [PATCH 1/9] Custom apps bookmark migration --- App/App_iOS.swift | 5 ++- App/App_macOS.swift | 5 ++- SwiftUI/Model/BookmarksMigration.swift | 61 ++++++++++++++++++++++++++ Tests/BookmarkMigrationTests.swift | 16 +++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 SwiftUI/Model/BookmarksMigration.swift create mode 100644 Tests/BookmarkMigrationTests.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index de2ed2a6..c1d15111 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -57,7 +57,10 @@ struct Kiwix: App { DownloadService.shared.restartHeartbeatIfNeeded() case let .custom(zimFileURL): LibraryOperations.open(url: zimFileURL) { - navigation.navigateToMostRecentTab() + Task { + await BookmarksMigration.migrationForCustomApps() + navigation.navigateToMostRecentTab() + } } } } diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 35cc2478..01a183a8 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -166,7 +166,10 @@ struct RootView: View { DownloadService.shared.restartHeartbeatIfNeeded() case let .custom(zimFileURL): LibraryOperations.open(url: zimFileURL) { - navigation.currentItem = .reading + Task { + await BookmarksMigration.migrationForCustomApps() + navigation.currentItem = .reading + } } } } diff --git a/SwiftUI/Model/BookmarksMigration.swift b/SwiftUI/Model/BookmarksMigration.swift new file mode 100644 index 00000000..701cca4c --- /dev/null +++ b/SwiftUI/Model/BookmarksMigration.swift @@ -0,0 +1,61 @@ +// +// BookmarksMigration.swift +// Kiwix + +import Foundation +import CoreData + +private extension ZimFile { + var groupById: String { + [name, flavor, languageCode].compactMap { $0 }.joined(separator: ":") + } +} + +enum BookmarksMigration { + + static func migrationForCustomApps() async { + guard FeatureFlags.hasLibrary == false else { return } + await Database.shared.container.performBackgroundTask { context in + let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)] + guard var zimFiles = try? ZimFile.fetchRequest(predicate: ZimFile.Predicate.isDownloaded, + sortDescriptors: sortDescriptors).execute(), + zimFiles.count > 1, + let latest = zimFiles.popLast() else { + return + } + for zimFile in zimFiles { + if zimFile.bookmarks.isEmpty == false { + migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) + } + } + } + } + + /// Migrates the bookmars from an old to new zim file, + /// also updates the bookmark urls accordingly (based on the new zim id as the host of those URLs) + private static func migrateFrom( + zimFile fromZim: ZimFile, + toZimFile toZim: ZimFile, + using context: NSManagedObjectContext + ) { + guard fromZim.bookmarks.isEmpty != false else { return } + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + let newHost = toZim.fileID.uuidString + fromZim.bookmarks.forEach { (bookmark: Bookmark) in + bookmark.zimFile = toZim + if let newArticleURL = bookmark.articleURL.updateHost(to: newHost) { + bookmark.articleURL = newArticleURL + } + bookmark.thumbImageURL = bookmark.thumbImageURL?.updateHost(to: newHost) + } + if context.hasChanges { try? context.save() } + } +} + +extension URL { + func updateHost(to newHost: String) -> URL? { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return nil } + components.host = newHost + return components.url + } +} diff --git a/Tests/BookmarkMigrationTests.swift b/Tests/BookmarkMigrationTests.swift new file mode 100644 index 00000000..bd4a2bc9 --- /dev/null +++ b/Tests/BookmarkMigrationTests.swift @@ -0,0 +1,16 @@ +// +// BookmarkMigrationTests.swift +// UnitTests + +import XCTest +@testable import Kiwix + +final class BookmarkMigrationTests: XCTestCase { + + func testURLHostChange() throws { + let url = URL(string: "kiwix://64C3EA1A-5161-2B94-1F50-606DA5EC0035/wb/Saftladen")! + let newHost: String = UUID(uuidString: "A992BF76-CA94-6B60-A762-9B5BC89B5BBF")!.uuidString + let expectedURL = URL(string: "kiwix://A992BF76-CA94-6B60-A762-9B5BC89B5BBF/wb/Saftladen")! + XCTAssertEqual(url.updateHost(to: newHost), expectedURL) + } +} From 113bdb55f0f907bf4ddf78bd03ac9bc3fc98963a Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 10 Mar 2024 01:49:21 +0100 Subject: [PATCH 2/9] Fix condition --- SwiftUI/Model/BookmarksMigration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftUI/Model/BookmarksMigration.swift b/SwiftUI/Model/BookmarksMigration.swift index 701cca4c..7a8356ed 100644 --- a/SwiftUI/Model/BookmarksMigration.swift +++ b/SwiftUI/Model/BookmarksMigration.swift @@ -38,7 +38,7 @@ enum BookmarksMigration { toZimFile toZim: ZimFile, using context: NSManagedObjectContext ) { - guard fromZim.bookmarks.isEmpty != false else { return } + guard fromZim.bookmarks.isEmpty == false else { return } context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let newHost = toZim.fileID.uuidString fromZim.bookmarks.forEach { (bookmark: Bookmark) in From 3512c1f855d99ffa4e3562dcaeee884614e61daa Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 10 Mar 2024 01:49:41 +0100 Subject: [PATCH 3/9] Remove double condition --- SwiftUI/Model/BookmarksMigration.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SwiftUI/Model/BookmarksMigration.swift b/SwiftUI/Model/BookmarksMigration.swift index 7a8356ed..34dc96c9 100644 --- a/SwiftUI/Model/BookmarksMigration.swift +++ b/SwiftUI/Model/BookmarksMigration.swift @@ -24,9 +24,7 @@ enum BookmarksMigration { return } for zimFile in zimFiles { - if zimFile.bookmarks.isEmpty == false { - migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) - } + migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) } } } From 0ee15788216cbf35bdd32eec1241596b8afbd258 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 09:45:18 +0100 Subject: [PATCH 4/9] Migrate tabs and interactive data for them as well --- SwiftUI/Model/BookmarksMigration.swift | 56 +++++++++++++++++++++----- ViewModel/BrowserViewModel.swift | 25 +++++++++--- ViewModel/NavigationViewModel.swift | 7 +++- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/SwiftUI/Model/BookmarksMigration.swift b/SwiftUI/Model/BookmarksMigration.swift index 34dc96c9..e954fd52 100644 --- a/SwiftUI/Model/BookmarksMigration.swift +++ b/SwiftUI/Model/BookmarksMigration.swift @@ -5,30 +5,44 @@ import Foundation import CoreData -private extension ZimFile { - var groupById: String { - [name, flavor, languageCode].compactMap { $0 }.joined(separator: ":") - } -} - enum BookmarksMigration { + /// Holds the new zimfile host: + /// Set during migration, + /// and read back when updating URLS mapped from WebView interaction state, + /// witch is saved as Data for each opened Tab + @MainActor private static var newHost: String? + private static let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)] + private static let requestLatestZimFile = ZimFile.fetchRequest( + predicate: ZimFile.Predicate.isDownloaded, + sortDescriptors: Self.sortDescriptors + ) + static func migrationForCustomApps() async { guard FeatureFlags.hasLibrary == false else { return } await Database.shared.container.performBackgroundTask { context in - let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)] - guard var zimFiles = try? ZimFile.fetchRequest(predicate: ZimFile.Predicate.isDownloaded, - sortDescriptors: sortDescriptors).execute(), + guard var zimFiles = try? requestLatestZimFile.execute(), zimFiles.count > 1, let latest = zimFiles.popLast() else { return } + + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump for zimFile in zimFiles { migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) } } } + static func migrateCustomAppURL(url: URL) async -> URL { + let newHost = await latestZimFileHost() + guard let newURL = url.updateHost(to: newHost) else { + assertionFailure("url cannot be updated") + return url + } + return newURL + } + /// Migrates the bookmars from an old to new zim file, /// also updates the bookmark urls accordingly (based on the new zim id as the host of those URLs) private static func migrateFrom( @@ -36,9 +50,12 @@ enum BookmarksMigration { toZimFile toZim: ZimFile, using context: NSManagedObjectContext ) { - guard fromZim.bookmarks.isEmpty == false else { return } - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let newHost = toZim.fileID.uuidString + Task { + await MainActor.run { + Self.newHost = newHost + } + } fromZim.bookmarks.forEach { (bookmark: Bookmark) in bookmark.zimFile = toZim if let newArticleURL = bookmark.articleURL.updateHost(to: newHost) { @@ -46,8 +63,25 @@ enum BookmarksMigration { } bookmark.thumbImageURL = bookmark.thumbImageURL?.updateHost(to: newHost) } + fromZim.tabs.forEach { (tab: Tab) in + tab.zimFile = toZim + } if context.hasChanges { try? context.save() } } + + private static func latestZimFileHost() async -> String { + if let newHost = await Self.newHost { return newHost } + // if it wasn't set before, set and return by the last ZimFile in DB: + guard let zimFile = try? requestLatestZimFile.execute().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 + } + return newHost + } } extension URL { diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index cc72d14b..54531890 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -20,16 +20,16 @@ import CoreKiwix final class BrowserViewModel: NSObject, ObservableObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, NSFetchedResultsControllerDelegate { - private static var cache = OrderedDictionary() + @MainActor private static var cache = OrderedDictionary() - static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { + @MainActor static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { let viewModel = cache[tabID] ?? BrowserViewModel(tabID: tabID) cache.removeValue(forKey: tabID) cache[tabID] = viewModel return viewModel } - static func purgeCache() { + @MainActor static func purgeCache() { guard cache.count > 10 else { return } let range = 0 ..< cache.count - 5 cache.values[range].forEach { viewModel in @@ -133,11 +133,14 @@ final class BrowserViewModel: NSObject, ObservableObject, .receive(on: DispatchQueue.main) .sink { [weak self] title, url in guard let title, let url else { return } - self?.didUpdate(title: title, url: url) + Task { [weak self] in + await self?.didUpdate(title: title, url: url) + } + } } - private func didUpdate(title: String, url: URL) { + @MainActor 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 @@ -195,7 +198,17 @@ final class BrowserViewModel: NSObject, ObservableObject, private func restoreBy(tabID: NSManagedObjectID) { if let tab = try? Database.viewContext.existingObject(with: tabID) as? Tab { webView.interactionState = tab.interactionState - url = webView.url + if AppType.isCustom { + Task { + guard let webURL = await webView.url else { + url = nil + return + } + url = await BookmarksMigration.migrateCustomAppURL(url: webURL) + } + } else { + url = webView.url + } } } diff --git a/ViewModel/NavigationViewModel.swift b/ViewModel/NavigationViewModel.swift index 1ab6a302..04e1ec92 100644 --- a/ViewModel/NavigationViewModel.swift +++ b/ViewModel/NavigationViewModel.swift @@ -30,7 +30,12 @@ class NavigationViewModel: ObservableObject { let tab = (try? context.fetch(fetchRequest).first) ?? self.makeTab(context: context) try? context.obtainPermanentIDs(for: [tab]) try? context.save() - currentItem = NavigationItem.tab(objectID: tab.objectID) + Task { + await MainActor.run { + currentItem = NavigationItem.tab(objectID: tab.objectID) + } + } + } @discardableResult From 29258f30f60f45e4e7f31e762862f9eada6d901d Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 10:46:55 +0100 Subject: [PATCH 5/9] Revert --- ViewModel/BrowserViewModel.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index 54531890..a4e51151 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -20,16 +20,16 @@ import CoreKiwix final class BrowserViewModel: NSObject, ObservableObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate, NSFetchedResultsControllerDelegate { - @MainActor private static var cache = OrderedDictionary() + private static var cache = OrderedDictionary() - @MainActor static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { + static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { let viewModel = cache[tabID] ?? BrowserViewModel(tabID: tabID) cache.removeValue(forKey: tabID) cache[tabID] = viewModel return viewModel } - @MainActor static func purgeCache() { + static func purgeCache() { guard cache.count > 10 else { return } let range = 0 ..< cache.count - 5 cache.values[range].forEach { viewModel in @@ -133,14 +133,11 @@ final class BrowserViewModel: NSObject, ObservableObject, .receive(on: DispatchQueue.main) .sink { [weak self] title, url in guard let title, let url else { return } - Task { [weak self] in - await self?.didUpdate(title: title, url: url) - } - + self?.didUpdate(title: title, url: url) } } - @MainActor private func didUpdate(title: String, url: URL) { + 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 From 71da750d6a028e76f1c76b201cd4e0a68082f804 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 13:46:12 +0100 Subject: [PATCH 6/9] Renames, delete old ZIM file post migration, fix Main thread issues --- App/App_iOS.swift | 2 +- App/App_macOS.swift | 2 +- App/SidebarViewController.swift | 24 ++++--- SwiftUI/Model/LibraryOperations.swift | 6 +- SwiftUI/Model/ZimMigration.swift | 96 +++++++++++++++++++++++++++ ViewModel/BrowserViewModel.swift | 41 +++++++++--- 6 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 SwiftUI/Model/ZimMigration.swift diff --git a/App/App_iOS.swift b/App/App_iOS.swift index c1d15111..45c51bec 100644 --- a/App/App_iOS.swift +++ b/App/App_iOS.swift @@ -58,7 +58,7 @@ struct Kiwix: App { case let .custom(zimFileURL): LibraryOperations.open(url: zimFileURL) { Task { - await BookmarksMigration.migrationForCustomApps() + await ZimMigration.forCustomApps() navigation.navigateToMostRecentTab() } } diff --git a/App/App_macOS.swift b/App/App_macOS.swift index 01a183a8..a9b8c1f9 100644 --- a/App/App_macOS.swift +++ b/App/App_macOS.swift @@ -167,7 +167,7 @@ struct RootView: View { case let .custom(zimFileURL): LibraryOperations.open(url: zimFileURL) { Task { - await BookmarksMigration.migrationForCustomApps() + await ZimMigration.forCustomApps() navigation.currentItem = .reading } } diff --git a/App/SidebarViewController.swift b/App/SidebarViewController.swift index f001efd2..9b22c659 100644 --- a/App/SidebarViewController.swift +++ b/App/SidebarViewController.swift @@ -137,22 +137,26 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl // MARK: - Delegations - func controller(_ controller: NSFetchedResultsController, + nonisolated func controller(_ controller: NSFetchedResultsController, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { let tabs = snapshot.itemIdentifiers .compactMap { $0 as? NSManagedObjectID } .map { NavigationItem.tab(objectID: $0) } var snapshot = NSDiffableDataSourceSectionSnapshot() snapshot.append(tabs) - dataSource.apply(snapshot, to: .tabs, animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0) { - // [iOS 15] when a tab is selected, reload it to refresh title and icon - guard #unavailable(iOS 16), - let indexPath = self.collectionView.indexPathsForSelectedItems?.first, - let item = self.dataSource.itemIdentifier(for: indexPath), - case .tab = item else { return } - var snapshot = self.dataSource.snapshot() - snapshot.reconfigureItems([item]) - self.dataSource.apply(snapshot, animatingDifferences: true) + Task { [snapshot] in + await MainActor.run { [snapshot] in + dataSource.apply(snapshot, to: .tabs, animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0) { + // [iOS 15] when a tab is selected, reload it to refresh title and icon + guard #unavailable(iOS 16), + let indexPath = self.collectionView.indexPathsForSelectedItems?.first, + let item = self.dataSource.itemIdentifier(for: indexPath), + case .tab = item else { return } + var snapshot = self.dataSource.snapshot() + snapshot.reconfigureItems([item]) + self.dataSource.apply(snapshot, animatingDifferences: true) + } + } } } diff --git a/SwiftUI/Model/LibraryOperations.swift b/SwiftUI/Model/LibraryOperations.swift index df9cb976..0c3cc345 100644 --- a/SwiftUI/Model/LibraryOperations.swift +++ b/SwiftUI/Model/LibraryOperations.swift @@ -45,8 +45,10 @@ struct LibraryOperations { zimFile.fileURLBookmark = fileURLBookmark zimFile.isMissing = false if context.hasChanges { try? context.save() } - DispatchQueue.main.async { - onComplete?() + Task { + await MainActor.run { + onComplete?() + } } } diff --git a/SwiftUI/Model/ZimMigration.swift b/SwiftUI/Model/ZimMigration.swift new file mode 100644 index 00000000..a761aeff --- /dev/null +++ b/SwiftUI/Model/ZimMigration.swift @@ -0,0 +1,96 @@ +// +// ZimMigration.swift +// Kiwix + +import Foundation +import CoreData + +enum ZimMigration { + + /// Holds the new zimfile host: + /// Set during migration, + /// and read back when updating URLS mapped from WebView interaction state, + /// witch is saved as Data for each opened Tab + @MainActor private static var newHost: String? + private static let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)] + private static let requestLatestZimFile = ZimFile.fetchRequest( + predicate: ZimFile.Predicate.isDownloaded, + sortDescriptors: Self.sortDescriptors + ) + + static func forCustomApps() async { + guard FeatureFlags.hasLibrary == false else { return } + await Database.shared.container.performBackgroundTask { context in + guard var zimFiles = try? requestLatestZimFile.execute(), + zimFiles.count > 1, + let latest = zimFiles.popLast() else { + return + } + + context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump + for zimFile in zimFiles { + migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) + } + } + debugPrint("migrationForCustomApps Complete") + } + + static func customApp(url: URL) async -> URL { + let newHost = await latestZimFileHost() + guard let newURL = url.updateHost(to: newHost) else { + assertionFailure("url cannot be updated") + return url + } + return newURL + } + + /// Migrates the bookmars from an old to new zim file, + /// also updates the bookmark urls accordingly (based on the new zim id as the host of those URLs) + /// deletes the old zim file in the DB + private static func migrateFrom( + zimFile fromZim: ZimFile, + toZimFile toZim: ZimFile, + using context: NSManagedObjectContext + ) { + let newHost = toZim.fileID.uuidString + Task { + await MainActor.run { + Self.newHost = newHost + } + } + fromZim.bookmarks.forEach { (bookmark: Bookmark) in + bookmark.zimFile = toZim + if let newArticleURL = bookmark.articleURL.updateHost(to: newHost) { + bookmark.articleURL = newArticleURL + } + bookmark.thumbImageURL = bookmark.thumbImageURL?.updateHost(to: newHost) + } + fromZim.tabs.forEach { (tab: Tab) in + tab.zimFile = toZim + } + context.delete(fromZim) + if context.hasChanges { try? context.save() } + } + + private static func latestZimFileHost() async -> String { + if let newHost = await Self.newHost { return newHost } + // if it wasn't set before, set and return by the last ZimFile in DB: + guard let zimFile = try? requestLatestZimFile.execute().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 + } + return newHost + } +} + +extension URL { + func updateHost(to newHost: String) -> URL? { + guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return nil } + components.host = newHost + return components.url + } +} diff --git a/ViewModel/BrowserViewModel.swift b/ViewModel/BrowserViewModel.swift index a4e51151..dbdf7db1 100644 --- a/ViewModel/BrowserViewModel.swift +++ b/ViewModel/BrowserViewModel.swift @@ -22,6 +22,7 @@ final class BrowserViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { private static var cache = OrderedDictionary() + @MainActor static func getCached(tabID: NSManagedObjectID) -> BrowserViewModel { let viewModel = cache[tabID] ?? BrowserViewModel(tabID: tabID) cache.removeValue(forKey: tabID) @@ -47,7 +48,7 @@ final class BrowserViewModel: NSObject, ObservableObject, @Published private(set) var articleBookmarked = false @Published private(set) var outlineItems = [OutlineItem]() @Published private(set) var outlineItemTree = [OutlineItem]() - @Published private(set) var url: URL? { + @MainActor @Published private(set) var url: URL? { didSet { if !FeatureFlags.hasLibrary, url == nil { loadMainArticle() @@ -84,6 +85,7 @@ final class BrowserViewModel: NSObject, ObservableObject, // MARK: - Lifecycle + @MainActor init(tabID: NSManagedObjectID? = nil) { self.tabID = tabID webView = WKWebView(frame: .zero, configuration: WebViewConfiguration()) @@ -121,10 +123,18 @@ final class BrowserViewModel: NSObject, ObservableObject, // setup web view property observers canGoBackObserver = webView.observe(\.canGoBack, options: .initial) { [weak self] webView, _ in - self?.canGoBack = webView.canGoBack + Task { [weak self] in + await MainActor.run { [weak self] in + self?.canGoBack = webView.canGoBack + } + } } canGoForwardObserver = webView.observe(\.canGoForward, options: .initial) { [weak self] webView, _ in - self?.canGoForward = webView.canGoForward + Task { [weak self] in + await MainActor.run { [weak self] in + self?.canGoForward = webView.canGoForward + } + } } titleURLObserver = Publishers.CombineLatest( webView.publisher(for: \.title, options: .initial), @@ -146,7 +156,11 @@ final class BrowserViewModel: NSObject, ObservableObject, // update view model articleTitle = title zimFileName = zimFile?.name ?? "" - self.url = url + Task { + await MainActor.run { + self.url = url + } + } let currentTabID: NSManagedObjectID = tabID ?? createNewTabID() tabID = currentTabID @@ -173,19 +187,21 @@ final class BrowserViewModel: NSObject, ObservableObject, } // MARK: - Content Loading - + @MainActor func load(url: URL) { guard webView.url != url else { return } webView.load(URLRequest(url: url)) self.url = url } + @MainActor func loadRandomArticle(zimFileID: UUID? = nil) { let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") guard let url = ZimFileService.shared.getRandomPageURL(zimFileID: zimFileID) else { return } load(url: url) } + @MainActor func loadMainArticle(zimFileID: UUID? = nil) { let zimFileID = zimFileID ?? UUID(uuidString: webView.url?.host ?? "") guard let url = ZimFileService.shared.getMainPageURL(zimFileID: zimFileID) else { return } @@ -198,13 +214,22 @@ final class BrowserViewModel: NSObject, ObservableObject, if AppType.isCustom { Task { guard let webURL = await webView.url else { - url = nil + await MainActor.run { + url = nil + } return } - url = await BookmarksMigration.migrateCustomAppURL(url: webURL) + let newURL = await ZimMigration.customApp(url: webURL) + await MainActor.run { + url = newURL + } } } else { - url = webView.url + Task { + await MainActor.run { + url = webView.url + } + } } } } From 02b5791fd05ec29b6bb2024f45ae165ae88895ce Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 13:54:22 +0100 Subject: [PATCH 7/9] Fixlint --- App/SidebarViewController.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/App/SidebarViewController.swift b/App/SidebarViewController.swift index 9b22c659..af48b16a 100644 --- a/App/SidebarViewController.swift +++ b/App/SidebarViewController.swift @@ -137,8 +137,10 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl // MARK: - Delegations - nonisolated func controller(_ controller: NSFetchedResultsController, - didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) { + nonisolated func controller( + _ controller: NSFetchedResultsController, + didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference + ) { let tabs = snapshot.itemIdentifiers .compactMap { $0 as? NSManagedObjectID } .map { NavigationItem.tab(objectID: $0) } @@ -146,7 +148,11 @@ class SidebarViewController: UICollectionViewController, NSFetchedResultsControl snapshot.append(tabs) Task { [snapshot] in await MainActor.run { [snapshot] in - dataSource.apply(snapshot, to: .tabs, animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0) { + dataSource.apply( + snapshot, + to: .tabs, + animatingDifferences: dataSource.snapshot(for: .tabs).items.count > 0 + ) { // [iOS 15] when a tab is selected, reload it to refresh title and icon guard #unavailable(iOS 16), let indexPath = self.collectionView.indexPathsForSelectedItems?.first, From 2cca938443e75784a16bf358c63006e0367bd900 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 13:54:30 +0100 Subject: [PATCH 8/9] Remove old file --- SwiftUI/Model/BookmarksMigration.swift | 93 -------------------------- 1 file changed, 93 deletions(-) delete mode 100644 SwiftUI/Model/BookmarksMigration.swift diff --git a/SwiftUI/Model/BookmarksMigration.swift b/SwiftUI/Model/BookmarksMigration.swift deleted file mode 100644 index e954fd52..00000000 --- a/SwiftUI/Model/BookmarksMigration.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// BookmarksMigration.swift -// Kiwix - -import Foundation -import CoreData - -enum BookmarksMigration { - - /// Holds the new zimfile host: - /// Set during migration, - /// and read back when updating URLS mapped from WebView interaction state, - /// witch is saved as Data for each opened Tab - @MainActor private static var newHost: String? - private static let sortDescriptors = [NSSortDescriptor(keyPath: \ZimFile.created, ascending: true)] - private static let requestLatestZimFile = ZimFile.fetchRequest( - predicate: ZimFile.Predicate.isDownloaded, - sortDescriptors: Self.sortDescriptors - ) - - static func migrationForCustomApps() async { - guard FeatureFlags.hasLibrary == false else { return } - await Database.shared.container.performBackgroundTask { context in - guard var zimFiles = try? requestLatestZimFile.execute(), - zimFiles.count > 1, - let latest = zimFiles.popLast() else { - return - } - - context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - for zimFile in zimFiles { - migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) - } - } - } - - static func migrateCustomAppURL(url: URL) async -> URL { - let newHost = await latestZimFileHost() - guard let newURL = url.updateHost(to: newHost) else { - assertionFailure("url cannot be updated") - return url - } - return newURL - } - - /// Migrates the bookmars from an old to new zim file, - /// also updates the bookmark urls accordingly (based on the new zim id as the host of those URLs) - private static func migrateFrom( - zimFile fromZim: ZimFile, - toZimFile toZim: ZimFile, - using context: NSManagedObjectContext - ) { - let newHost = toZim.fileID.uuidString - Task { - await MainActor.run { - Self.newHost = newHost - } - } - fromZim.bookmarks.forEach { (bookmark: Bookmark) in - bookmark.zimFile = toZim - if let newArticleURL = bookmark.articleURL.updateHost(to: newHost) { - bookmark.articleURL = newArticleURL - } - bookmark.thumbImageURL = bookmark.thumbImageURL?.updateHost(to: newHost) - } - fromZim.tabs.forEach { (tab: Tab) in - tab.zimFile = toZim - } - if context.hasChanges { try? context.save() } - } - - private static func latestZimFileHost() async -> String { - if let newHost = await Self.newHost { return newHost } - // if it wasn't set before, set and return by the last ZimFile in DB: - guard let zimFile = try? requestLatestZimFile.execute().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 - } - return newHost - } -} - -extension URL { - func updateHost(to newHost: String) -> URL? { - guard var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return nil } - components.host = newHost - return components.url - } -} From 9eab868d45ac07ad214e6d695a412113719d15e5 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Mon, 11 Mar 2024 14:57:46 +0100 Subject: [PATCH 9/9] Remove not needed debugPrint --- SwiftUI/Model/ZimMigration.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/SwiftUI/Model/ZimMigration.swift b/SwiftUI/Model/ZimMigration.swift index a761aeff..3df6c918 100644 --- a/SwiftUI/Model/ZimMigration.swift +++ b/SwiftUI/Model/ZimMigration.swift @@ -32,7 +32,6 @@ enum ZimMigration { migrateFrom(zimFile: zimFile, toZimFile: latest, using: context) } } - debugPrint("migrationForCustomApps Complete") } static func customApp(url: URL) async -> URL {