diff --git a/Core/Sources/Core/DataFile.swift b/Core/Sources/Core/DataFile.swift index b9bf1c342..fa074903b 100644 --- a/Core/Sources/Core/DataFile.swift +++ b/Core/Sources/Core/DataFile.swift @@ -21,20 +21,24 @@ public protocol DataFileDelegate: AnyObject { private var isDirty = false { didSet { if isDirty { - restartTimer() + postponingBlock.runInFuture() } else { - invalidateTimer() + postponingBlock.cancelRun() } } } private let fileURL: URL - private let saveInterval: TimeInterval = 1.0 - private var timer: Timer? + + private lazy var postponingBlock: PostponingBlock = { + PostponingBlock(delayInterval: 1.0) { [weak self] in + self?.saveToDiskIfNeeded() + } + }() public init(fileURL: URL) { - + self.fileURL = fileURL } @@ -64,33 +68,8 @@ private extension DataFile { func saveToDiskIfNeeded() { - assert(Thread.isMainThread) - if isDirty { save() } } - - func restartTimer() { - - assert(Thread.isMainThread) - - invalidateTimer() - - timer = Timer.scheduledTimer(withTimeInterval: saveInterval, repeats: false) { timer in - MainActor.assumeIsolated { - self.saveToDiskIfNeeded() - } - } - } - - func invalidateTimer() { - - assert(Thread.isMainThread) - - if let timer, timer.isValid { - timer.invalidate() - } - timer = nil - } } diff --git a/Core/Sources/Core/PostponingBlock.swift b/Core/Sources/Core/PostponingBlock.swift new file mode 100644 index 000000000..01d919c9d --- /dev/null +++ b/Core/Sources/Core/PostponingBlock.swift @@ -0,0 +1,51 @@ +// +// PostponingBlock.swift +// +// +// Created by Brent Simmons on 6/9/24. +// + +import Foundation + +/// Runs a block of code in the future. Each time `runInFuture` is called, the block is postponed again until the future by `delayInterval`. +@MainActor public final class PostponingBlock { + + private let block: () -> Void + private let delayInterval: TimeInterval + private var timer: Timer? + + public init(delayInterval: TimeInterval, block: @escaping () -> Void) { + + self.block = block + self.delayInterval = delayInterval + } + + /// Run the block in `delayInterval` seconds, canceling any run about to happen before then. + public func runInFuture() { + + invalidateTimer() + + timer = Timer.scheduledTimer(withTimeInterval: delayInterval, repeats: false) { timer in + MainActor.assumeIsolated { + self.block() + } + } + } + + /// Cancel any upcoming run. + public func cancelRun() { + + invalidateTimer() + } +} + +private extension PostponingBlock { + + func invalidateTimer() { + + if let timer, timer.isValid { + timer.invalidate() + } + timer = nil + } +} diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index 72a98c519..ab3513057 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -48,6 +48,15 @@ final class SmartFeed: PseudoFeed { } #endif + private lazy var postponingBlock: PostponingBlock = { + PostponingBlock(delayInterval: 1.0) { + Task { + try? await self.fetchUnreadCounts() + } + } + }() + + private var fetchUnreadCountsTask: Task? private let delegate: SmartFeedDelegate private var unreadCounts = [String: Int]() @@ -63,7 +72,7 @@ final class SmartFeed: PseudoFeed { } } - @MainActor @objc func fetchUnreadCounts() { + @MainActor func fetchUnreadCounts() async throws { let activeAccounts = AccountManager.shared.activeAccounts @@ -79,11 +88,9 @@ final class SmartFeed: PseudoFeed { updateUnreadCount() return } - - Task { @MainActor in - for account in activeAccounts { - await fetchUnreadCount(for: account) - } + + for account in activeAccounts { + await fetchUnreadCount(for: account) } } } @@ -104,7 +111,8 @@ extension SmartFeed: ArticleFetcher { private extension SmartFeed { @MainActor func queueFetchUnreadCounts() { - CoalescingQueue.standard.add(self, #selector(fetchUnreadCounts)) + + postponingBlock.runInFuture() } @MainActor func fetchUnreadCount(for account: Account) async { @@ -125,3 +133,4 @@ private extension SmartFeed { unreadCount = unread } } +