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:
Ilyas Hallak 2025-09-26 21:56:49 +02:00
parent 2791b7f227
commit ac7f4e66eb
4 changed files with 47 additions and 18 deletions

View File

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

View File

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

View File

@ -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 {
let backgroundContext = coreDataManager.newBackgroundContext()
try await backgroundContext.perform { [weak self] in
guard let self = self else { return }
for dto in dtos { for dto in dtos {
if !tagExists(name: dto.name) { if !self.tagExists(name: dto.name, in: backgroundContext) {
dto.toEntity(context: coreDataManager.context) dto.toEntity(context: backgroundContext)
} }
} }
try coreDataManager.context.save() try backgroundContext.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
coreDataManager.context.performAndWait {
do { do {
let results = try coreDataManager.context.fetch(fetchRequest) let count = try context.count(for: fetchRequest)
exists = !results.isEmpty return count > 0
} catch { } catch {
exists = false return false
} }
} }
return exists
}
} }

View File

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