@@ -21,7 +21,7 @@ import SwiftUI
2121
2222// MARK: - Constants
2323
24- let kTabBarAnimationDuration : CGFloat = 0.3
24+ let kTabBarAnimationDuration : CGFloat = 0.2
2525let kAsyncDelay : CGFloat = 0.1
2626
2727// MARK: - Selected Tab Indicator
@@ -34,6 +34,14 @@ struct SelectedTabIndicator: View {
3434
3535 @State private var tabBarHeight : CGFloat = 0
3636 @State private var safeAreaBottom : CGFloat = 0
37+ /// Horizontal scale factor used to animate the indicator expanding from its center.
38+ /// Starts at 0 (invisible) and is animated to 1 (full width) whenever a tab becomes selected.
39+ @State private var indicatorScaleX : CGFloat = 0
40+
41+ /// To disable animation if device in low power mode
42+ @EnvironmentObject private var lowPowerModeObserver : OUDSLowPowerModeObserver
43+ /// To disable animation if user asked for it in device settings
44+ @Environment ( \. accessibilityReduceMotion) private var reduceMotion
3745
3846 @Environment ( \. iPhoneInUse) private var iPhoneInUse
3947 @Environment ( \. theme) private var theme
@@ -46,20 +54,45 @@ struct SelectedTabIndicator: View {
4654 let indicatorPosition = ( geometry. size. height - tabBarHeight + safeAreaBottom) + ( theme. bar. sizeHeightActiveIndicatorCustom / 2 )
4755 let xOffset = tabWidth * CGFloat( selected) + ( tabWidth - indicatorWidth) / 2
4856
49- RoundedRectangle ( cornerRadius: theme. bar. borderRadiusActiveIndicatorCustomTop)
50- . fill ( theme. bar. colorActiveIndicatorCustomSelectedEnabled. color ( for: colorScheme) )
51- . frame ( width: indicatorWidth, height: theme. bar. sizeHeightActiveIndicatorCustom)
52- . position (
53- x: xOffset + indicatorWidth / 2 ,
54- y: indicatorPosition)
55- . animation ( . easeInOut( duration: kTabBarAnimationDuration) , value: selected)
56- . onChange ( of: geometry. size) { _ in
57- updateTabBarHeight ( )
58- }
57+ if reduceMotion || lowPowerModeObserver. isLowPowerModeEnabled {
58+ // No animation: display a full-tab-width indicator, instantly repositioned on selection change.
59+ RoundedRectangle ( cornerRadius: theme. bar. borderRadiusActiveIndicatorCustomTop)
60+ . fill ( theme. bar. colorActiveIndicatorCustomSelectedEnabled. color ( for: colorScheme) )
61+ . frame ( width: indicatorWidth, height: theme. bar. sizeHeightActiveIndicatorCustom)
62+ . position (
63+ x: tabWidth * CGFloat( selected) + tabWidth / 2 ,
64+ y: indicatorPosition)
65+ . onChange ( of: geometry. size) { _ in
66+ updateTabBarHeight ( )
67+ }
68+ } else {
69+ RoundedRectangle ( cornerRadius: theme. bar. borderRadiusActiveIndicatorCustomTop)
70+ . fill ( theme. bar. colorActiveIndicatorCustomSelectedEnabled. color ( for: colorScheme) )
71+ . frame ( width: indicatorWidth, height: theme. bar. sizeHeightActiveIndicatorCustom)
72+ . scaleEffect ( x: indicatorScaleX, y: 1 , anchor: . center)
73+ . position (
74+ x: xOffset + indicatorWidth / 2 ,
75+ y: indicatorPosition)
76+ . onChange ( of: selected) { _ in
77+ // Instantly collapse the indicator to zero width (no animation, so no slide
78+ // from the old tab to the new one), then animate the line expanding outward
79+ // from the center of the new tab.
80+ indicatorScaleX = 0
81+ withAnimation ( . easeInOut( duration: kTabBarAnimationDuration) ) {
82+ indicatorScaleX = 1
83+ }
84+ }
85+ . onChange ( of: geometry. size) { _ in
86+ updateTabBarHeight ( )
87+ }
88+ }
5989 }
6090 . onAppear {
6191 DispatchQueue . main. asyncAfter ( deadline: . now( ) + kAsyncDelay) {
6292 updateTabBarHeight ( )
93+ withAnimation ( . easeInOut( duration: kTabBarAnimationDuration) ) {
94+ indicatorScaleX = 1
95+ }
6396 }
6497 }
6598 . onReceive ( NotificationCenter . default. publisher ( for: UIDevice . orientationDidChangeNotification) ) { _ in
0 commit comments