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">
</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)

View File

@ -50,6 +50,16 @@ class CoreDataManager {
return persistentContainer.viewContext
}
var mainContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
return context
}
func save() {
if context.hasChanges {
do {

View File

@ -1,7 +1,7 @@
import Foundation
import CoreData
class LabelsRepository: PLabelsRepository {
class LabelsRepository: PLabelsRepository, @unchecked Sendable {
private let api: PAPI
private let coreDataManager = CoreDataManager.shared
@ -17,27 +17,28 @@ class LabelsRepository: PLabelsRepository {
}
func saveLabels(_ dtos: [BookmarkLabelDto]) async throws {
for dto in dtos {
if !tagExists(name: dto.name) {
dto.toEntity(context: coreDataManager.context)
let backgroundContext = coreDataManager.newBackgroundContext()
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()
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
var exists = false
coreDataManager.context.performAndWait {
do {
let results = try coreDataManager.context.fetch(fetchRequest)
exists = !results.isEmpty
} catch {
exists = false
}
do {
let count = try context.count(for: fetchRequest)
return count > 0
} catch {
return false
}
return exists
}
}

View File

@ -285,6 +285,8 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
var onHeightChange: ((CGFloat) -> Void)?
var onScroll: ((Double) -> Void)?
var hasHeightUpdate: Bool = false
var isScrolling: Bool = false
var scrollEndTimer: Timer?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
@ -300,7 +302,8 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "heightUpdate", let height = message.body as? CGFloat {
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.hasHeightUpdate = true
}
@ -308,6 +311,15 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
}
if message.name == "scrollProgress", let progress = message.body as? Double {
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)
}
}