Skip to content

Commit 6665fa7

Browse files
pylappCopilot
andauthored
refactor: new animation of selected tab indicator animation for tab bar component (#1351) (#1355)
Change animation for selected tab indicator for iOS <= 18 Closes #1351 Assisted-by: Claude Sonnet 4.6 (GitHub Copilot) Tested-by: Jérôme Régnier <jerome.regnier@orange.com> Reviewed-by: Ludovic Pinel <ludovic.pinel@orange.com> Co-authored-by: Pierre-Yves Lapersonne <pierreyves.lapersonne@orange.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Signed-off-by: Pierre-Yves Lapersonne <pierreyves.lapersonne@orange.com>
1 parent 201c6be commit 6665fa7

File tree

2 files changed

+45
-11
lines changed

2 files changed

+45
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
### Changed
1919

20+
- Selected tab animation for `tab bar component` (iOS 18 and older) (Orange-OpenSource/ouds-ios#1351)
2021
- `alert message` component with new label colors (Orange-OpenSource/ouds-ios#1342)
2122
- Rename in `bullet list` component API "unordered icon" to "unordered asset" (Orange-OpenSource/ouds-ios#1326)
2223
- AGENTS.md file to focus only on users (Orange-OpenSource/ouds-ios#1341)

OUDS/Core/Components/Sources/Navigations/TabBar/Internal/SelectedTabIndicator.swift

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import SwiftUI
2121

2222
// MARK: - Constants
2323

24-
let kTabBarAnimationDuration: CGFloat = 0.3
24+
let kTabBarAnimationDuration: CGFloat = 0.2
2525
let 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

Comments
 (0)