From 6e365c1b1171218daf5c690762a6746281bccc23 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 21 Dec 2025 13:45:38 +0530 Subject: [PATCH] Fix Timer Bug for UI Layout --- DynamicIsland/ContentView.swift | 2 +- .../DynamicIslandViewCoordinator.swift | 20 +++ DynamicIsland/Localizable.xcstrings | 4 - .../components/Notch/NotchTimerView.swift | 137 ++++++++++-------- .../models/DynamicIslandViewModel.swift | 2 +- 5 files changed, 99 insertions(+), 66 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 60fd3ea..ace6cff 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -767,7 +767,7 @@ struct ContentView: View { triggerHapticIfAllowed() } - let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive + let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive && !enableMinimalisticUI guard vm.notchState == .closed, !coordinator.sneakPeek.show, diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 4772ed8..462ef7b 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -51,6 +51,10 @@ class DynamicIslandViewCoordinator: ObservableObject { @Published var currentView: NotchViews = .home { didSet { + if Defaults[.enableMinimalisticUI] && currentView != .home { + currentView = .home + return + } handleStatsTabTransition(from: oldValue, to: currentView) } } @@ -116,6 +120,13 @@ class DynamicIslandViewCoordinator: ObservableObject { self?.handleTimerFeatureToggle(change.newValue) } .store(in: &cancellables) + + Defaults.publisher(.enableMinimalisticUI) + .receive(on: DispatchQueue.main) + .sink { [weak self] change in + self?.handleMinimalisticModeChange(change.newValue) + } + .store(in: &cancellables) } private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) { @@ -152,6 +163,15 @@ class DynamicIslandViewCoordinator: ObservableObject { currentView = .home } } + + private func handleMinimalisticModeChange(_ isEnabled: Bool) { + guard isEnabled else { return } + if currentView != .home { + withAnimation(.smooth) { + currentView = .home + } + } + } func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { let resolvedDuration: TimeInterval diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index e8181da..59a1a82 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -11340,10 +11340,6 @@ "comment" : "A description of how to manage the microphone privacy indicator preferences.", "isCommentAutoGenerated" : true }, - "Managed in Clock app" : { - "comment" : "A notice displayed below the custom timer section, explaining that the timer is managed by the Clock app.", - "isCommentAutoGenerated" : true - }, "Mark as complete" : { "comment" : "An accessibility label for a button that marks an item as complete.", "isCommentAutoGenerated" : true diff --git a/DynamicIsland/components/Notch/NotchTimerView.swift b/DynamicIsland/components/Notch/NotchTimerView.swift index 23f2fab..b13c448 100644 --- a/DynamicIsland/components/Notch/NotchTimerView.swift +++ b/DynamicIsland/components/Notch/NotchTimerView.swift @@ -16,6 +16,7 @@ struct NotchTimerView: View { @ObservedObject var timerManager = TimerManager.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @Default(.enableTimerFeature) var enableTimerFeature + @Default(.enableMinimalisticUI) private var enableMinimalisticUI @Default(.timerPresets) private var timerPresets @Default(.timerIconColorMode) private var colorMode @Default(.timerSolidColor) private var solidColor @@ -75,11 +76,13 @@ struct NotchTimerView: View { private var leftColumn: some View { VStack(alignment: .leading, spacing: 16) { if timerManager.isTimerActive { + Spacer(minLength: 0) activeTimerCard + Spacer(minLength: 0) } else { customTimerComposer + Spacer(minLength: 0) } - Spacer(minLength: 0) } .frame(maxWidth: .infinity, alignment: .leading) .frame(maxHeight: maxTabContentHeight, alignment: .top) @@ -104,7 +107,9 @@ struct NotchTimerView: View { ForEach(timerPresets) { preset in TimerPresetCard(preset: preset, isActive: timerManager.activePresetId == preset.id) { timerManager.startTimer(duration: preset.duration, name: preset.name, preset: preset) - coordinator.currentView = .timer + if !enableMinimalisticUI { + coordinator.currentView = .timer + } } .listRowInsets(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) .listRowBackground(Color.clear) @@ -146,13 +151,9 @@ struct NotchTimerView: View { countdownSection } - if showsProgress { - progressSection - } + progressSection } - .padding(16) - .background(Color.white.opacity(0.06)) - .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) + .padding(.horizontal, 4) } @ViewBuilder @@ -186,7 +187,7 @@ struct NotchTimerView: View { } } } else { - externalTimerNotice + inactiveTimerPlaceholder } } @@ -218,47 +219,52 @@ struct NotchTimerView: View { .frame(height: 32) } + @ViewBuilder private var countdownSection: some View { - VStack(alignment: .trailing, spacing: 4) { - Text(timerManager.formattedRemainingTime()) - .font(.system(size: 36, weight: .black, design: .monospaced)) - .foregroundStyle(timerManager.isOvertime ? Color.red : .white) - .contentTransition(.numericText()) - .animation(.smooth(duration: 0.25), value: timerManager.remainingTime) - .lineLimit(1) - .minimumScaleFactor(0.8) + if showsProgress && progressStyle == .ring { + TimerProgressRing( + progress: timerManager.progress, + tint: timerAccentColor, + timeText: timerManager.formattedRemainingTime(), + isOvertime: timerManager.isOvertime, + remainingTime: timerManager.remainingTime + ) + } else { + VStack(alignment: .trailing, spacing: 4) { + Text(timerManager.formattedRemainingTime()) + .font(.system(size: 36, weight: .black, design: .monospaced)) + .foregroundStyle(timerManager.isOvertime ? Color.red : .white) + .contentTransition(.numericText()) + .animation(.smooth(duration: 0.25), value: timerManager.remainingTime) + .lineLimit(1) + .minimumScaleFactor(0.8) - if timerManager.isOvertime { - Text("Overtime") - .font(.caption) - .foregroundStyle(.red) + if timerManager.isOvertime { + Text("Overtime") + .font(.caption) + .foregroundStyle(.red) + } } + .frame(width: 190, alignment: .trailing) } - .frame(width: 190, alignment: .trailing) } @ViewBuilder private var progressSection: some View { - if progressStyle == .bar { - GeometryReader { geometry in - Capsule() - .fill(Color.white.opacity(0.12)) - .frame(height: 4) - .overlay(alignment: .leading) { - Capsule() - .fill(timerAccentColor) - .frame(width: geometry.size.width * max(0, CGFloat(min(timerManager.progress, 1.0)))) - .animation(.smooth(duration: 0.25), value: timerManager.progress) - } - } - .frame(height: 4) - .frame(maxWidth: .infinity) - } else { - HStack(spacing: 12) { - TimerProgressRing(progress: timerManager.progress, tint: timerAccentColor) - .frame(height: 56) - Spacer() - } + if showsProgress && progressStyle == .bar { + Capsule() + .fill(Color.white.opacity(0.12)) + .frame(height: 4) + .overlay(alignment: .leading) { + Capsule() + .fill(timerAccentColor) + .frame(height: 4) + .scaleEffect(x: normalizedProgress, y: 1, anchor: .leading) + .animation(.smooth(duration: 0.25), value: timerManager.progress) + } + .frame(maxWidth: .infinity) + .padding(.top, 6) + .padding(.bottom, 2) } } @@ -314,18 +320,9 @@ struct NotchTimerView: View { .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) } - private var externalTimerNotice: some View { - VStack(alignment: .leading, spacing: 6) { - Label("Managed in Clock app", systemImage: "arrow.triangle.2.circlepath") - .font(.caption) - .foregroundStyle(.secondary) - - Button("Hide", role: .destructive) { - timerManager.endExternalTimer(triggerSmoothClose: false) - } - .buttonStyle(.borderless) - .font(.caption) - } + private var inactiveTimerPlaceholder: some View { + Color.clear + .frame(height: 46) } private var disabledState: some View { @@ -383,6 +380,10 @@ struct NotchTimerView: View { } } + private var normalizedProgress: CGFloat { + CGFloat(max(0, min(timerManager.progress, 1))) + } + private var timerDisplayName: String { timerManager.timerName.isEmpty ? "Timer" : timerManager.timerName } @@ -450,7 +451,9 @@ struct NotchTimerView: View { Button { withAnimation(.smooth) { timerManager.startTimer(duration: customDurationInSeconds, name: "Custom Timer") - coordinator.currentView = .timer + if !enableMinimalisticUI { + coordinator.currentView = .timer + } } } label: { Label("Start", systemImage: "play.fill") @@ -552,18 +555,32 @@ private struct TimerControlButton: View { private struct TimerProgressRing: View { let progress: Double let tint: Color + let timeText: String + let isOvertime: Bool + let remainingTime: TimeInterval + + private var clampedProgress: Double { min(max(progress, 0), 1) } var body: some View { ZStack { Circle() - .stroke(Color.white.opacity(0.12), lineWidth: 6) + .stroke(Color.white.opacity(0.12), lineWidth: 8) + Circle() - .trim(from: 0, to: min(progress, 1.0)) - .stroke(tint, style: StrokeStyle(lineWidth: 6, lineCap: .round)) + .trim(from: 0, to: clampedProgress) + .stroke(tint, style: StrokeStyle(lineWidth: 8, lineCap: .round)) .rotationEffect(.degrees(-90)) - .animation(.smooth(duration: 0.3), value: progress) + .animation(.smooth(duration: 0.3), value: clampedProgress) + + Text(timeText) + .font(.system(size: 28, weight: .black, design: .monospaced)) + .foregroundStyle(isOvertime ? Color.red : .white) + .minimumScaleFactor(0.6) + .lineLimit(1) + .contentTransition(.numericText()) + .animation(.smooth(duration: 0.25), value: remainingTime) } - .frame(width: 60, height: 60) + .frame(width: 110, height: 110) } } diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index c55a852..c5de241 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -284,7 +284,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject { // Set the current view to shelf if it contains files and the user enables openShelfByDefault // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home - if !ShelfStateViewModel.shared.isEmpty && Defaults[.openShelfByDefault] { + if !ShelfStateViewModel.shared.isEmpty && Defaults[.openShelfByDefault] && !Defaults[.enableMinimalisticUI] { coordinator.currentView = .shelf } else if !coordinator.openLastTabByDefault { coordinator.currentView = .home