fix: Improve Core Data thread safety and resolve scrolling flicker
- Add background context support to CoreDataManager - Fix TagEntity threading crashes in LabelsRepository - Prevent WebView height updates during scrolling to reduce flicker - Add App Store download link to README
This commit is contained in:
parent
2791b7f227
commit
ac7f4e66eb
10
README.md
10
README.md
@ -18,9 +18,15 @@ https://codeberg.org/readeck/readeck
|
|||||||
<img src="screenshots/ipad.webp" height="400" alt="iPad View">
|
<img src="screenshots/ipad.webp" height="400" alt="iPad View">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## TestFlight Beta Access
|
## Download
|
||||||
|
|
||||||
You can now join the public TestFlight beta for the Readeck iOS app:
|
### App Store (Stable Releases)
|
||||||
|
The official app is available on the App Store with stable, tested releases:
|
||||||
|
|
||||||
|
[Download Readeck on the App Store](https://apps.apple.com/de/app/readeck/id6748764703)
|
||||||
|
|
||||||
|
### TestFlight Beta Access (Early Releases)
|
||||||
|
For early access to new features and beta versions (use with caution):
|
||||||
|
|
||||||
[Join the Readeck Beta on TestFlight](https://testflight.apple.com/join/cV55mKsR)
|
[Join the Readeck Beta on TestFlight](https://testflight.apple.com/join/cV55mKsR)
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,16 @@ class CoreDataManager {
|
|||||||
return persistentContainer.viewContext
|
return persistentContainer.viewContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mainContext: NSManagedObjectContext {
|
||||||
|
return persistentContainer.viewContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBackgroundContext() -> NSManagedObjectContext {
|
||||||
|
let context = persistentContainer.newBackgroundContext()
|
||||||
|
context.automaticallyMergesChangesFromParent = true
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
func save() {
|
func save() {
|
||||||
if context.hasChanges {
|
if context.hasChanges {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
class LabelsRepository: PLabelsRepository {
|
class LabelsRepository: PLabelsRepository, @unchecked Sendable {
|
||||||
private let api: PAPI
|
private let api: PAPI
|
||||||
|
|
||||||
private let coreDataManager = CoreDataManager.shared
|
private let coreDataManager = CoreDataManager.shared
|
||||||
@ -17,27 +17,28 @@ class LabelsRepository: PLabelsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveLabels(_ dtos: [BookmarkLabelDto]) async throws {
|
func saveLabels(_ dtos: [BookmarkLabelDto]) async throws {
|
||||||
for dto in dtos {
|
let backgroundContext = coreDataManager.newBackgroundContext()
|
||||||
if !tagExists(name: dto.name) {
|
|
||||||
dto.toEntity(context: coreDataManager.context)
|
try await backgroundContext.perform { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
for dto in dtos {
|
||||||
|
if !self.tagExists(name: dto.name, in: backgroundContext) {
|
||||||
|
dto.toEntity(context: backgroundContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
try backgroundContext.save()
|
||||||
}
|
}
|
||||||
try coreDataManager.context.save()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tagExists(name: String) -> Bool {
|
private func tagExists(name: String, in context: NSManagedObjectContext) -> Bool {
|
||||||
let fetchRequest: NSFetchRequest<TagEntity> = TagEntity.fetchRequest()
|
let fetchRequest: NSFetchRequest<TagEntity> = TagEntity.fetchRequest()
|
||||||
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
|
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
|
||||||
|
|
||||||
var exists = false
|
do {
|
||||||
coreDataManager.context.performAndWait {
|
let count = try context.count(for: fetchRequest)
|
||||||
do {
|
return count > 0
|
||||||
let results = try coreDataManager.context.fetch(fetchRequest)
|
} catch {
|
||||||
exists = !results.isEmpty
|
return false
|
||||||
} catch {
|
|
||||||
exists = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return exists
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -285,6 +285,8 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
|
|||||||
var onHeightChange: ((CGFloat) -> Void)?
|
var onHeightChange: ((CGFloat) -> Void)?
|
||||||
var onScroll: ((Double) -> Void)?
|
var onScroll: ((Double) -> Void)?
|
||||||
var hasHeightUpdate: Bool = false
|
var hasHeightUpdate: Bool = false
|
||||||
|
var isScrolling: Bool = false
|
||||||
|
var scrollEndTimer: Timer?
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
if navigationAction.navigationType == .linkActivated {
|
if navigationAction.navigationType == .linkActivated {
|
||||||
@ -300,7 +302,8 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
|
|||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
if message.name == "heightUpdate", let height = message.body as? CGFloat {
|
if message.name == "heightUpdate", let height = message.body as? CGFloat {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.hasHeightUpdate == false {
|
// Block height updates during active scrolling to prevent flicker
|
||||||
|
if !self.isScrolling && !self.hasHeightUpdate {
|
||||||
self.onHeightChange?(height)
|
self.onHeightChange?(height)
|
||||||
self.hasHeightUpdate = true
|
self.hasHeightUpdate = true
|
||||||
}
|
}
|
||||||
@ -308,6 +311,15 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
|
|||||||
}
|
}
|
||||||
if message.name == "scrollProgress", let progress = message.body as? Double {
|
if message.name == "scrollProgress", let progress = message.body as? Double {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
// Track scrolling state
|
||||||
|
self.isScrolling = true
|
||||||
|
|
||||||
|
// Reset scrolling state after scroll ends
|
||||||
|
self.scrollEndTimer?.invalidate()
|
||||||
|
self.scrollEndTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
|
||||||
|
self.isScrolling = false
|
||||||
|
}
|
||||||
|
|
||||||
self.onScroll?(progress)
|
self.onScroll?(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user