Fix: Janky Animation

This commit is contained in:
Hariharan Mudaliar 2026-03-01 02:01:02 +05:30
commit 8d7cc2ed58
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
3 changed files with 37 additions and 9 deletions

View file

@ -184,6 +184,22 @@ struct ContentView: View {
private let musicControlPauseGrace: TimeInterval = 5 private let musicControlPauseGrace: TimeInterval = 5
private let musicControlResumeDelay: TimeInterval = 0.24 private let musicControlResumeDelay: TimeInterval = 0.24
// MARK: - Tab switch direction for smooth transitions
private var tabSwitchTransition: AnyTransition {
if coordinator.tabSwitchForward {
return .asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
)
} else {
return .asymmetric(
insertion: .move(edge: .leading).combined(with: .opacity),
removal: .move(edge: .trailing).combined(with: .opacity)
)
}
}
private var standardMediaControlsActive: Bool { private var standardMediaControlsActive: Bool {
showStandardMediaControls && !enableMinimalisticUI showStandardMediaControls && !enableMinimalisticUI
} }
@ -399,7 +415,6 @@ struct ContentView: View {
alignment: .top alignment: .top
) )
.frame(maxHeight: .infinity, alignment: .top) .frame(maxHeight: .infinity, alignment: .top)
.animation(nil, value: coordinator.currentView)
.environmentObject(privacyManager) .environmentObject(privacyManager)
.onChange(of: dynamicNotchSize) { oldSize, newSize in .onChange(of: dynamicNotchSize) { oldSize, newSize in
guard oldSize != newSize else { return } guard oldSize != newSize else { return }
@ -755,13 +770,16 @@ struct ContentView: View {
} }
} }
} }
.id(coordinator.currentView) // Force SwiftUI to treat each view as unique .id(coordinator.currentView)
.transition(tabSwitchTransition)
} }
} }
.clipped()
.zIndex(1) .zIndex(1)
.allowsHitTesting(vm.notchState == .open) .allowsHitTesting(vm.notchState == .open)
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0) .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
.opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1) .opacity(abs(gestureProgress) > 0.3 ? min(abs(gestureProgress * 2), 0.8) : 1)
.animation(.smooth(duration: 0.3), value: coordinator.currentView)
} }
} }

View file

@ -102,12 +102,21 @@ class DynamicIslandViewCoordinator: ObservableObject {
static let shared = DynamicIslandViewCoordinator() static let shared = DynamicIslandViewCoordinator()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private static let tabOrder: [NotchViews] = [.home, .shelf, .timer, .stats, .colorPicker, .notes, .clipboard, .extensionExperience]
/// Direction of the most recent tab switch (true = forward/right, false = backward/left)
@Published var tabSwitchForward: Bool = true
@Published var currentView: NotchViews = .home { @Published var currentView: NotchViews = .home {
didSet { didSet {
if Defaults[.enableMinimalisticUI] && currentView != .home { if Defaults[.enableMinimalisticUI] && currentView != .home {
currentView = .home currentView = .home
return return
} }
// Track direction before SwiftUI re-renders
let oldIdx = Self.tabOrder.firstIndex(of: oldValue) ?? 0
let newIdx = Self.tabOrder.firstIndex(of: currentView) ?? 0
tabSwitchForward = newIdx >= oldIdx
handleStatsTabTransition(from: oldValue, to: currentView) handleStatsTabTransition(from: oldValue, to: currentView)
} }
} }

View file

@ -99,9 +99,6 @@ struct TabSelectionView: View {
) )
} }
} }
DispatchQueue.main.async {
ensureValidSelection(with: tabsArray)
}
return tabsArray return tabsArray
} }
var body: some View { var body: some View {
@ -110,11 +107,13 @@ struct TabSelectionView: View {
let isSelected = isSelected(tab) let isSelected = isSelected(tab)
let activeAccent = tab.accentColor ?? .white let activeAccent = tab.accentColor ?? .white
TabButton(label: tab.label, icon: tab.icon, selected: isSelected) { TabButton(label: tab.label, icon: tab.icon, selected: isSelected) {
withAnimation(.smooth(duration: 0.3)) {
if tab.view == .extensionExperience { if tab.view == .extensionExperience {
coordinator.selectedExtensionExperienceID = tab.experienceID coordinator.selectedExtensionExperienceID = tab.experienceID
} }
coordinator.currentView = tab.view coordinator.currentView = tab.view
} }
}
.frame(height: 26) .frame(height: 26)
.foregroundStyle(isSelected ? activeAccent : .gray) .foregroundStyle(isSelected ? activeAccent : .gray)
.background { .background {
@ -132,8 +131,10 @@ struct TabSelectionView: View {
} }
} }
} }
.animation(.smooth, value: coordinator.currentView)
.clipShape(Capsule()) .clipShape(Capsule())
.onAppear {
ensureValidSelection(with: tabs)
}
} }
private var extensionTabsEnabled: Bool { private var extensionTabsEnabled: Bool {