diff --git a/README.md b/README.md
index 9e59c2d..60832f6 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,15 @@ https://codeberg.org/readeck/readeck
-## 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)
diff --git a/readeck/Data/CoreData/CoreDataManager.swift b/readeck/Data/CoreData/CoreDataManager.swift
index 0eeb145..b5b6593 100644
--- a/readeck/Data/CoreData/CoreDataManager.swift
+++ b/readeck/Data/CoreData/CoreDataManager.swift
@@ -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 {
diff --git a/readeck/Data/Repository/LabelsRepository.swift b/readeck/Data/Repository/LabelsRepository.swift
index fda950e..bb73e26 100644
--- a/readeck/Data/Repository/LabelsRepository.swift
+++ b/readeck/Data/Repository/LabelsRepository.swift
@@ -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.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
}
}
diff --git a/readeck/UI/Components/WebView.swift b/readeck/UI/Components/WebView.swift
index 2c48860..382f1a3 100644
--- a/readeck/UI/Components/WebView.swift
+++ b/readeck/UI/Components/WebView.swift
@@ -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)
}
}