diff --git a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift index d21c36a..ab2145d 100644 --- a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift @@ -201,22 +201,22 @@ struct LockScreenTimerWidget: View { } private func stopTimer() { - let stopAction: @MainActor () -> Void = { - if timerManager.allowsManualInteraction { - timerManager.stopTimer() - } else { - timerManager.endExternalTimer(triggerSmoothClose: false) - } - } + let allowsManualInteraction = timerManager.allowsManualInteraction - Task { @MainActor in - // Trigger the scale-out animation first so the dismissal mirrors the unlock flow. - LockScreenTimerWidgetPanelManager.shared.hide() + LockScreenTimerWidgetPanelManager.shared.hide() + Task.detached(priority: .userInitiated) { [allowsManualInteraction] in try? await Task.sleep(nanoseconds: LockScreenTimerWidgetPanelManager.hideAnimationDurationNanoseconds) guard !Task.isCancelled else { return } - stopAction() + + await MainActor.run { + if allowsManualInteraction { + TimerManager.shared.stopTimer() + } else { + TimerManager.shared.endExternalTimer(triggerSmoothClose: false) + } + } } } diff --git a/DynamicIsland/managers/LockScreenTimerWidgetManager.swift b/DynamicIsland/managers/LockScreenTimerWidgetManager.swift index 50fee0d..bfc2b65 100644 --- a/DynamicIsland/managers/LockScreenTimerWidgetManager.swift +++ b/DynamicIsland/managers/LockScreenTimerWidgetManager.swift @@ -137,6 +137,7 @@ final class LockScreenTimerWidgetPanelManager { } } + func refreshPosition(animated: Bool) { guard let window, window.isVisible, let screen = NSScreen.main else { return } let frame = targetFrame(on: screen) diff --git a/DynamicIsland/managers/TimerControlWindowManager.swift b/DynamicIsland/managers/TimerControlWindowManager.swift index 860feac..0803eb7 100644 --- a/DynamicIsland/managers/TimerControlWindowManager.swift +++ b/DynamicIsland/managers/TimerControlWindowManager.swift @@ -4,7 +4,7 @@ import SwiftUI import SkyLightWindow import QuartzCore -struct TimerControlWindowMetrics { +struct TimerControlWindowMetrics: Equatable { let notchHeight: CGFloat let notchWidth: CGFloat let rightWingWidth: CGFloat @@ -90,15 +90,23 @@ final class TimerControlWindowManager { guard window != nil else { return present(using: viewModel, metrics: metrics) } + + if let lastMetrics, lastMetrics == metrics { + return true + } + return present(using: viewModel, metrics: metrics) } - func hide(animated: Bool = true) { + func hide(animated: Bool = true, tearDown: Bool = true) { guard let window else { return } guard animated, window.alphaValue > 0.01 else { window.orderOut(nil) window.alphaValue = 0 + if tearDown { + tearDownWindowResources(using: window) + } return } @@ -110,13 +118,26 @@ final class TimerControlWindowManager { context.timingFunction = CAMediaTimingFunction(name: .easeIn) window.animator().setFrame(retreatFrame, display: true) window.animator().alphaValue = 0 - } completionHandler: { + } completionHandler: { [weak self] in window.setFrame(originalFrame, display: false) window.orderOut(nil) window.alphaValue = 0 + if tearDown { + self?.tearDownWindowResources(using: window) + } } } + private func tearDownWindowResources(using window: NSWindow? = nil) { + let targetWindow = window ?? self.window + targetWindow?.contentView = nil + targetWindow?.orderOut(nil) + hostingView = nil + lastMetrics = nil + self.window = nil + hasDelegated = false + } + private func ensureHostingView(with overlay: TimerControlOverlay) -> NSHostingView { if let hostingView { hostingView.rootView = overlay