Fix: Memory Leak

This commit is contained in:
Hariharan Mudaliar 2025-11-10 23:20:38 +05:30
commit 12db48bc25

View file

@ -25,10 +25,17 @@ final class ReminderLiveActivityManager: ObservableObject {
private var nextReminder: ReminderEntry? private var nextReminder: ReminderEntry?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var tickerCancellable: AnyCancellable? private var tickerTask: Task<Void, Never>? { didSet { oldValue?.cancel() } }
private var evaluationTask: Task<Void, Never>? private var evaluationTask: Task<Void, Never>?
private var fallbackRefreshTask: Task<Void, Never>? private var fallbackRefreshTask: Task<Void, Never>?
private var refreshTask: Task<Void, Never>? private var refreshTask: Task<Void, Never>?
private var pendingRefreshTask: Task<Void, Never>?
private var pendingRefreshForce = false
private var pendingRefreshToken = UUID()
private var lastRefreshDate: Date?
private let minimumRefreshInterval: TimeInterval = 10
private let refreshDebounceInterval: TimeInterval = 0.3
private var refreshTaskToken = UUID()
private let calendarService: CalendarServiceProviding private let calendarService: CalendarServiceProviding
private let calendarManager = CalendarManager.shared private let calendarManager = CalendarManager.shared
@ -38,7 +45,7 @@ final class ReminderLiveActivityManager: ObservableObject {
private init(calendarService: CalendarServiceProviding = CalendarService()) { private init(calendarService: CalendarServiceProviding = CalendarService()) {
self.calendarService = calendarService self.calendarService = calendarService
setupObservers() setupObservers()
Task { await self.refreshUpcomingReminder() } scheduleRefresh(force: true)
} }
private func setupObservers() { private func setupObservers() {
@ -80,7 +87,7 @@ final class ReminderLiveActivityManager: ObservableObject {
calendarManager.$events calendarManager.$events
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }
self.scheduleRefresh(force: true) self.scheduleRefresh(force: false)
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -93,12 +100,14 @@ final class ReminderLiveActivityManager: ObservableObject {
} }
private func cancelAllTimers() { private func cancelAllTimers() {
tickerCancellable?.cancel() tickerTask = nil
tickerCancellable = nil
evaluationTask?.cancel() evaluationTask?.cancel()
evaluationTask = nil evaluationTask = nil
fallbackRefreshTask?.cancel() fallbackRefreshTask?.cancel()
fallbackRefreshTask = nil fallbackRefreshTask = nil
pendingRefreshTask?.cancel()
pendingRefreshTask = nil
pendingRefreshForce = false
refreshTask?.cancel() refreshTask?.cancel()
refreshTask = nil refreshTask = nil
} }
@ -154,32 +163,92 @@ final class ReminderLiveActivityManager: ObservableObject {
fallbackRefreshTask = Task { [weak self] in fallbackRefreshTask = Task { [weak self] in
guard let self else { return } guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
await self.refreshUpcomingReminder() await self.scheduleRefresh(force: true)
} }
} }
private func scheduleRefresh(force: Bool) { private func scheduleRefresh(force: Bool) {
pendingRefreshForce = pendingRefreshForce || force
pendingRefreshToken = UUID()
let token = pendingRefreshToken
refreshTask?.cancel() refreshTask?.cancel()
refreshTask = Task { [weak self] in pendingRefreshTask?.cancel()
pendingRefreshTask = Task { [weak self] in
guard let self else { return } guard let self else { return }
await self.refreshUpcomingReminder(force: force) do {
try await Task.sleep(nanoseconds: UInt64(self.refreshDebounceInterval * 1_000_000_000))
} catch {
return
}
await self.executeScheduledRefresh(token: token)
} }
} }
private func startTickerIfNeeded() { private func executeScheduledRefresh(token: UUID) async {
guard tickerCancellable == nil else { return } guard pendingRefreshToken == token else { return }
tickerCancellable = Timer.publish(every: 1, tolerance: 0.2, on: .main, in: .common) pendingRefreshTask = nil
.autoconnect()
.sink { [weak self] date in if Task.isCancelled {
guard let self else { return } return
self.currentDate = date }
Task { await self.evaluateCurrentState(at: date) }
let now = Date()
let force = pendingRefreshForce
if !force, let last = lastRefreshDate {
let elapsed = now.timeIntervalSince(last)
if elapsed < minimumRefreshInterval {
let remaining = max(minimumRefreshInterval - elapsed, refreshDebounceInterval)
pendingRefreshToken = UUID()
let nextToken = pendingRefreshToken
pendingRefreshTask = Task { [weak self] in
guard let self else { return }
do {
try await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
} catch {
return
}
await self.executeScheduledRefresh(token: nextToken)
}
return
} }
}
pendingRefreshForce = false
lastRefreshDate = now
refreshTask?.cancel()
let taskToken = UUID()
refreshTaskToken = taskToken
let task = Task { [weak self] in
guard let self else { return }
await self.refreshUpcomingReminder(force: force)
}
refreshTask = task
defer {
if refreshTaskToken == taskToken {
refreshTask = nil
}
}
_ = try? await task.value
}
private func startTickerIfNeeded() {
guard tickerTask == nil else { return }
tickerTask = Task { [weak self] in
while let self, !Task.isCancelled {
await self.handleTick()
do {
try await Task.sleep(for: .seconds(1))
} catch {
break
}
}
}
} }
private func stopTicker() { private func stopTicker() {
tickerCancellable?.cancel() tickerTask = nil
tickerCancellable = nil
} }
private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) { private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) {
@ -277,6 +346,15 @@ final class ReminderLiveActivityManager: ObservableObject {
} }
} }
@MainActor
private func handleTick() async {
let now = Date()
if abs(currentDate.timeIntervalSince(now)) >= 0.5 {
currentDate = now
}
await evaluateCurrentState(at: now)
}
private func glyphName(for event: EventModel) -> String { private func glyphName(for event: EventModel) -> String {
switch event.type { switch event.type {
case .birthday: case .birthday: