Skip to content

Commit f99853e

Browse files
authored
Refactor #30235 ⁃ [TabScrollController refactor] Use transform instead of constraints to animate toolbars (#30255)
* Use transform in animation instead of constraints * Improve code readability plus documentation
1 parent 1f182b2 commit f99853e

File tree

2 files changed

+62
-54
lines changed

2 files changed

+62
-54
lines changed

firefox-ios/Client/Frontend/Browser/BrowserViewController/Extensions/BrowserViewController+TabScrollHandlerDelegate.swift

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ extension BrowserViewController: TabScrollHandler.Delegate {
99
return calculateOverKeyboardScrollHeight(safeAreaInsets: UIWindow.keyWindow?.safeAreaInsets)
1010
}
1111

12+
// Checks if minimal address bar is enabled and tab is on reader mode bar or findInPage
13+
private var shouldSendAlphaChangeAction: Bool {
14+
guard let tab = tabManager.selectedTab,
15+
let tabURL = tab.url else { return false }
16+
17+
return isMinimalAddressBarEnabled && !tab.isFindInPageMode && !tabURL.isReaderModeURL
18+
}
19+
1220
private var headerOffset: CGFloat {
1321
let baseOffset = -getHeaderSize().height
1422
let isiPad = UIDevice.current.userInterfaceIdiom == .pad
@@ -20,28 +28,32 @@ extension BrowserViewController: TabScrollHandler.Delegate {
2028
return baseOffset + UX.minimalHeaderOffset
2129
}
2230

23-
/// Interactive toolbar transition.
24-
/// Top bar moves in [headerOffset, 0] using `originTop - clampProgress`
25-
/// Bottom bar moves in [0, height]using `originBottom + clampProgress`
26-
/// Values are clamped to their ranges, then layout is requested.
31+
/// Animates the toolbar transition between expanded and collapsed states based on scroll progress.
32+
/// This method applies a smooth translation transform to either the top or bottom toolbar
33+
/// (depending on whether `isBottomSearchBar` is true), animating its movement as the user scrolls.
34+
/// - Parameters:
35+
/// - progress: The current scroll progress used to determine the translation amount.
36+
/// Positive values indicate upward scrolling (collapsing), and negative values indicate downward scrolling (expanding).
37+
/// - state: The target display state of the toolbar (`.collapsed` or `.expanded`).
2738
func updateToolbarTransition(progress: CGFloat, towards state: TabScrollHandler.ToolbarDisplayState) {
28-
// Clamp movement to the intended direction (toward `state`)
29-
let clampProgress = (state == .collapsed) ? max(0, progress) : min(0, progress)
30-
31-
// Top toolbar: range [headerOffset ... 0]
32-
if !isBottomSearchBar {
33-
let originTop: CGFloat = (state == .expanded) ? 0 : headerOffset
34-
let topOffset = clamp(offset: originTop - clampProgress, min: headerOffset, max: 0)
35-
headerTopConstraint?.update(offset: topOffset)
36-
header.superview?.layoutIfNeeded()
39+
let isCollapsing = (state == .collapsed)
40+
let clampProgress = isCollapsing ? max(0, progress) : min(0, progress)
41+
42+
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) { [self] in
43+
if isBottomSearchBar {
44+
let translationY = isCollapsing ? clampProgress : 0
45+
let transform = CGAffineTransform(translationX: 0, y: translationY)
46+
bottomContainer.transform = transform
47+
overKeyboardContainer.transform = transform
48+
bottomBlurView.transform = transform
49+
} else {
50+
let topTransform = isCollapsing
51+
? CGAffineTransform(translationX: 0, y: -clampProgress)
52+
: .identity
53+
header.transform = topTransform
54+
topBlurView.transform = topTransform
55+
}
3756
}
38-
39-
// Bottom toolbar: range [0 ... height]
40-
let bottomContainerHeight = getBottomContainerSize().height
41-
let originBottom: CGFloat = (state == .expanded) ? bottomContainerHeight : 0
42-
let bottomOffset = clamp(offset: originBottom + clampProgress, min: 0, max: bottomContainerHeight)
43-
bottomContainerConstraint?.update(offset: bottomOffset)
44-
bottomContainer.superview?.layoutIfNeeded()
4557
}
4658

4759
func showToolbar() {
@@ -67,7 +79,7 @@ extension BrowserViewController: TabScrollHandler.Delegate {
6779

6880
private func updateTopToolbar(topOffset: CGFloat, alpha: CGFloat) {
6981
guard UIAccessibility.isReduceMotionEnabled else {
70-
animateTopToolbar(topOffset: topOffset, alpha: alpha)
82+
animateTopToolbar(alpha: alpha)
7183
return
7284
}
7385

@@ -91,9 +103,7 @@ extension BrowserViewController: TabScrollHandler.Delegate {
91103
overKeyboardContainerOffset: CGFloat,
92104
alpha: CGFloat) {
93105
guard UIAccessibility.isReduceMotionEnabled else {
94-
animateBottomToolbar(bottomOffset: bottomContainerOffset,
95-
overKeyboardOffset: overKeyboardContainerOffset,
96-
alpha: alpha)
106+
animateBottomToolbar(alpha: alpha)
97107
return
98108
}
99109

@@ -114,16 +124,19 @@ extension BrowserViewController: TabScrollHandler.Delegate {
114124
}
115125
}
116126

117-
private func animateTopToolbar(topOffset: CGFloat, alpha: CGFloat) {
118-
headerTopConstraint?.update(offset: topOffset)
119-
127+
private func animateTopToolbar(alpha: CGFloat) {
128+
let isShowing = alpha == 1
120129
UIView.animate(withDuration: UX.topToolbarDuration,
121130
delay: 0,
122-
options: [.curveEaseOut, .beginFromCurrentState]) { [weak self] in
123-
guard let self else { return }
124-
125-
header.superview?.layoutIfNeeded()
126-
header.updateAlphaForSubviews(alpha)
131+
options: [.curveEaseOut]) { [self] in
132+
if !isShowing {
133+
header.transform = .identity.translatedBy(x: 0, y: -topBlurView.frame.height)
134+
topBlurView.transform = .identity.translatedBy(x: 0,
135+
y: -topBlurView.frame.height)
136+
} else {
137+
header.transform = .identity
138+
topBlurView.transform = .identity
139+
}
127140
}
128141

129142
if shouldSendAlphaChangeAction {
@@ -135,20 +148,23 @@ extension BrowserViewController: TabScrollHandler.Delegate {
135148
)
136149
)
137150
}
138-
}
139-
140-
private func animateBottomToolbar(bottomOffset: CGFloat,
141-
overKeyboardOffset: CGFloat,
142-
alpha: CGFloat) {
143-
bottomContainerConstraint?.update(offset: bottomOffset)
144-
overKeyboardContainerConstraint?.update(offset: overKeyboardOffset)
151+
}
145152

153+
private func animateBottomToolbar(alpha: CGFloat) {
154+
let isShowing = alpha == 1
155+
let customOffset: CGFloat = getBottomContainerSize().height + overKeyboardContainerHeight
146156
UIView.animate(withDuration: UX.bottomToolbarDuration,
147157
delay: 0,
148-
options: [.curveEaseOut, .beginFromCurrentState]) { [weak self] in
149-
guard let self else { return }
150-
151-
bottomContainer.superview?.layoutIfNeeded()
158+
options: [.curveEaseOut]) { [self] in
159+
if !isShowing {
160+
bottomContainer.transform = .identity.translatedBy(x: 0, y: customOffset)
161+
overKeyboardContainer.transform = .identity.translatedBy(x: 0, y: customOffset)
162+
bottomBlurView.transform = .identity.translatedBy(x: 0, y: customOffset)
163+
} else {
164+
bottomContainer.transform = .identity
165+
overKeyboardContainer.transform = .identity
166+
bottomBlurView.transform = .identity
167+
}
152168
}
153169

154170
if shouldSendAlphaChangeAction {
@@ -162,14 +178,6 @@ extension BrowserViewController: TabScrollHandler.Delegate {
162178
}
163179
}
164180

165-
// Checks if minimal address bar is enabled and tab is on reader mode bar or findInPage
166-
private var shouldSendAlphaChangeAction: Bool {
167-
guard let tab = tabManager.selectedTab,
168-
let tabURL = tab.url else { return false }
169-
170-
return isMinimalAddressBarEnabled && !tab.isFindInPageMode && !tabURL.isReaderModeURL
171-
}
172-
173181
/// Helper method for testing overKeyboardScrollHeight behavior.
174182
/// - Parameters:
175183
/// - safeAreaInsets: The safe area insets to use (nil treated as .zero).

firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class BrowserViewController: UIViewController,
4747
static let minimalHeaderOffset: CGFloat = 14
4848

4949
static let topToolbarDuration: TimeInterval = 0.3
50-
static let bottomToolbarDuration: TimeInterval = 0.5
50+
static let bottomToolbarDuration: TimeInterval = 0.4
5151
}
5252

5353
/// Describes the state of the current search session. This state is used
@@ -223,11 +223,11 @@ class BrowserViewController: UIViewController,
223223
}()
224224

225225
// MARK: Blur views for translucent toolbars
226-
private lazy var topBlurView: UIVisualEffectView = .build { view in
226+
lazy var topBlurView: UIVisualEffectView = .build { view in
227227
view.effect = self.effect
228228
}
229229

230-
private lazy var bottomBlurView: UIVisualEffectView = .build { view in
230+
lazy var bottomBlurView: UIVisualEffectView = .build { view in
231231
view.effect = self.effect
232232
}
233233

0 commit comments

Comments
 (0)