Skip to content

Commit 417533f

Browse files
authored
Bugfix FXIOS-12865 [Homepage Redesign] Prevent homepage layout shift when keyboard is shown (manually backport #28230) (#28270)
* backport * fix cherry pick error * fix another cherry pick error
1 parent b2c258d commit 417533f

File tree

5 files changed

+147
-18
lines changed

5 files changed

+147
-18
lines changed

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ class BrowserViewController: UIViewController,
308308
return featureFlags.isFeatureEnabled(.deeplinkOptimizationRefactor, checking: .buildOnly)
309309
}
310310

311+
var isStoriesRedesignEnabled: Bool {
312+
return featureFlags.isFeatureEnabled(.homepageStoriesRedesign, checking: .buildOnly)
313+
}
314+
315+
var isHomepageSearchBarEnabled: Bool {
316+
return featureFlags.isFeatureEnabled(.homepageSearchBar, checking: .buildOnly)
317+
}
318+
311319
// MARK: Computed vars
312320

313321
lazy var isBottomSearchBar: Bool = {
@@ -1382,6 +1390,9 @@ class BrowserViewController: UIViewController,
13821390
// when toolbars are hidden/shown the mask on the content view that is used for
13831391
// toolbar translucency needs to be updated
13841392
updateToolbarDisplay()
1393+
1394+
// Update available height for the homepage
1395+
dispatchAvailableContentHeightChangedAction()
13851396
}
13861397

13871398
func checkForJSAlerts() {
@@ -1681,15 +1692,21 @@ class BrowserViewController: UIViewController,
16811692
return
16821693
}
16831694

1684-
let showNavToolbar = toolbarHelper.shouldShowNavigationToolbar(for: traitCollection)
1685-
let toolBarHeight = showNavToolbar ? UIConstants.BottomToolbarHeight : 0
1686-
let spacerHeight = keyboardHeight - toolBarHeight
1695+
let spacerHeight = getKeyboardSpacerHeight(keyboardHeight: keyboardHeight)
1696+
16871697
overKeyboardContainer.addKeyboardSpacer(spacerHeight: spacerHeight)
16881698

16891699
// make sure the keyboard spacer has the right color/translucency
16901700
overKeyboardContainer.applyTheme(theme: themeManager.getCurrentTheme(for: windowUUID))
16911701
}
16921702

1703+
private func getKeyboardSpacerHeight(keyboardHeight: CGFloat) -> CGFloat {
1704+
let showNavToolbar = toolbarHelper.shouldShowNavigationToolbar(for: traitCollection)
1705+
let toolBarHeight = showNavToolbar ? UIConstants.BottomToolbarHeight : 0
1706+
let spacerHeight = keyboardHeight - toolBarHeight
1707+
return spacerHeight
1708+
}
1709+
16931710
fileprivate func showQueuedAlertIfAvailable() {
16941711
if let queuedAlertInfo = tabManager.selectedTab?.dequeueJavascriptAlertPrompt() {
16951712
let alertController = queuedAlertInfo.alertController()
@@ -3012,6 +3029,45 @@ class BrowserViewController: UIViewController,
30123029
store.dispatchLegacy(action)
30133030
}
30143031

3032+
private func dispatchAvailableContentHeightChangedAction() {
3033+
guard isStoriesRedesignEnabled, let browserViewControllerState,
3034+
browserViewControllerState.browserViewType == .normalHomepage,
3035+
let homepageState = store.state.screenState(HomepageState.self, for: .homepage, window: windowUUID),
3036+
homepageState.availableContentHeight != getAvailableHomepageContentHeight() else { return }
3037+
3038+
store.dispatchLegacy(
3039+
HomepageAction(
3040+
availableContentHeight: getAvailableHomepageContentHeight(),
3041+
windowUUID: windowUUID,
3042+
actionType: HomepageActionType.availableContentHeightDidChange
3043+
)
3044+
)
3045+
}
3046+
3047+
// Computes the height available for the homepage content to occupy when the address is not being edited.
3048+
// This is accomplished by taking BVC's height and subtracting the height of all of it's immediate subviews
3049+
// This is used to keep the homepage layout constant, such that it doesn't shift when the homepage's view size changes
3050+
// eg when the address bar is tapped and the keyboard is presented
3051+
private func getAvailableHomepageContentHeight() -> CGFloat {
3052+
// We only have to worry about the bottom address bar when it is part of the homepage layout (can be presented
3053+
// without the keyboard)
3054+
var addressBarHeight = isHomepageSearchBarEnabled ? 0 : overKeyboardContainer.frame.height
3055+
3056+
// The overKeyboardContainer typically just contains the bottom address bar, but when editing, also contains a
3057+
// keyboard-sized spacer that we must ignore (since we don't want it to affect the homepage layouts height)
3058+
if isBottomSearchBar && !isHomepageSearchBarEnabled {
3059+
let keyboardHeight = keyboardState?.intersectionHeightForView(view) ?? 0
3060+
let keyboardSpacerHeight = keyboardHeight > 0 ? getKeyboardSpacerHeight(keyboardHeight: keyboardHeight) : 0
3061+
addressBarHeight -= keyboardSpacerHeight
3062+
}
3063+
3064+
// Subtracts all of BVC's immediate subviews to get the space left to allocate to the homepage
3065+
return view.frame.height - statusBarOverlay.frame.height
3066+
- bottomContentStackView.frame.height
3067+
- bottomContainer.frame.height
3068+
- addressBarHeight
3069+
}
3070+
30153071
// MARK: Opening New Tabs
30163072

30173073
/// ⚠️ !! WARNING !! ⚠️

firefox-ios/Client/Frontend/Home/Homepage Rebuild/HomepageSectionLayoutProvider.swift

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -504,11 +504,17 @@ final class HomepageSectionLayoutProvider: FeatureFlaggable {
504504
// It's important to update this calculation whenever a change is made to any of the calculated sections that would
505505
// result in it having a different height (eg changes to top/bottom insets).
506506
private func createSpacerSectionLayout(for environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection {
507+
let homepageState = store.state.screenState(HomepageState.self, for: .homepage, window: windowUUID)
507508
let collectionViewHeight = environment.container.contentSize.height
509+
510+
// If something went wrong with our availableContentHeight calculation in BVC, fall back to just using the actual
511+
// collection view height
512+
let availableContentHeight = homepageState?.availableContentHeight ?? collectionViewHeight
513+
508514
// Dimensions of <= 0.0 cause runtime warnings, so use a minimum height of 0.1
509-
let spacerHeight = max(0.1, collectionViewHeight - getShortcutsSectionHeight(environment: environment)
510-
- getStoriesSectionHeight(environment: environment)
511-
- getSearchBarSectionHeight(environment: environment)
515+
let spacerHeight = max(0.1, availableContentHeight - getShortcutsSectionHeight(environment: environment)
516+
- getStoriesSectionHeight(environment: environment)
517+
- getSearchBarSectionHeight(environment: environment)
512518
)
513519

514520
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
@@ -538,9 +544,9 @@ final class HomepageSectionLayoutProvider: FeatureFlaggable {
538544
guard let state = store.state.screenState(HomepageState.self, for: .homepage, window: windowUUID) else { return 0 }
539545
var totalHeight: CGFloat = 0
540546
let topSitesState = state.topSitesState
541-
let rows = topSitesState.numberOfRows
547+
let maxRows = topSitesState.numberOfRows
542548
let cols = topSitesState.numberOfTilesPerRow
543-
let maxCells = rows * cols
549+
let maxCells = maxRows * cols
544550

545551
// Add header height
546552
totalHeight += getHeaderHeight(headerState: topSitesState.sectionHeaderState, environment: environment)
@@ -563,8 +569,13 @@ final class HomepageSectionLayoutProvider: FeatureFlaggable {
563569
totalHeight += rowHeights.reduce(0, +)
564570

565571
// Add inter-row spacing
566-
let rowsShown = ceil(Double(topSitesState.topSitesData.count) / Double(cols))
567-
totalHeight += CGFloat(rowsShown - 1) * UX.standardSpacing
572+
// Get number of actual rows shown since TopSitesSectionState::numberOfRows just gives us the user pref of max
573+
// number of rows we can show.
574+
// totalRows: number of rows we have enough data for
575+
// presentedRows: number of rows visible in the UI
576+
let totalRows = Int(ceil(Double(topSitesState.topSitesData.count) / Double(cols)))
577+
let presentedRows = min(maxRows, totalRows)
578+
totalHeight += CGFloat(presentedRows - 1) * UX.standardSpacing
568579

569580
// Add section insets
570581
totalHeight += UX.TopSitesConstants.getBottomInset()

firefox-ios/Client/Frontend/Home/Homepage Rebuild/Redux/HomepageAction.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ final class HomepageAction: Action {
2020
let numberOfTopSitesPerRow: Int?
2121
let telemetryExtras: HomepageTelemetryExtras?
2222
let isZeroSearch: Bool?
23+
let availableContentHeight: CGFloat?
2324

2425
init(
2526
isSearchBarEnabled: Bool? = nil,
@@ -28,6 +29,7 @@ final class HomepageAction: Action {
2829
showiPadSetup: Bool? = nil,
2930
telemetryExtras: HomepageTelemetryExtras? = nil,
3031
isZeroSearch: Bool? = nil,
32+
availableContentHeight: CGFloat? = nil,
3133
windowUUID: WindowUUID,
3234
actionType: any ActionType
3335
) {
@@ -37,6 +39,7 @@ final class HomepageAction: Action {
3739
self.showiPadSetup = showiPadSetup
3840
self.telemetryExtras = telemetryExtras
3941
self.isZeroSearch = isZeroSearch
42+
self.availableContentHeight = availableContentHeight
4043
super.init(windowUUID: windowUUID, actionType: actionType)
4144
}
4245
}
@@ -50,6 +53,7 @@ enum HomepageActionType: ActionType {
5053
case didSelectItem
5154
case embeddedHomepage
5255
case sectionSeen
56+
case availableContentHeightDidChange
5357
}
5458

5559
enum HomepageMiddlewareActionType: ActionType {

firefox-ios/Client/Frontend/Home/Homepage Rebuild/Redux/HomepageState.swift

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct HomepageState: ScreenState, Equatable {
3030
let isZeroSearch: Bool
3131
let shouldTriggerImpression: Bool
3232
let shouldShowSpacer: Bool
33+
let availableContentHeight: CGFloat
3334

3435
init(appState: AppState, uuid: WindowUUID) {
3536
guard let homepageState = store.state.screenState(
@@ -53,7 +54,8 @@ struct HomepageState: ScreenState, Equatable {
5354
wallpaperState: homepageState.wallpaperState,
5455
isZeroSearch: homepageState.isZeroSearch,
5556
shouldTriggerImpression: homepageState.shouldTriggerImpression,
56-
shouldShowSpacer: homepageState.shouldShowSpacer
57+
shouldShowSpacer: homepageState.shouldShowSpacer,
58+
availableContentHeight: homepageState.availableContentHeight
5759
)
5860
}
5961

@@ -70,7 +72,8 @@ struct HomepageState: ScreenState, Equatable {
7072
wallpaperState: WallpaperState(windowUUID: windowUUID),
7173
isZeroSearch: false,
7274
shouldTriggerImpression: false,
73-
shouldShowSpacer: false
75+
shouldShowSpacer: false,
76+
availableContentHeight: 0
7477
)
7578
}
7679

@@ -86,7 +89,8 @@ struct HomepageState: ScreenState, Equatable {
8689
wallpaperState: WallpaperState,
8790
isZeroSearch: Bool,
8891
shouldTriggerImpression: Bool,
89-
shouldShowSpacer: Bool
92+
shouldShowSpacer: Bool,
93+
availableContentHeight: CGFloat
9094
) {
9195
self.windowUUID = windowUUID
9296
self.headerState = headerState
@@ -100,6 +104,7 @@ struct HomepageState: ScreenState, Equatable {
100104
self.isZeroSearch = isZeroSearch
101105
self.shouldTriggerImpression = shouldTriggerImpression
102106
self.shouldShowSpacer = shouldShowSpacer
107+
self.availableContentHeight = availableContentHeight
103108
}
104109

105110
static let reducer: Reducer<Self> = { state, action in
@@ -117,6 +122,8 @@ struct HomepageState: ScreenState, Equatable {
117122
}
118123

119124
return handleEmbeddedHomepageAction(state: state, action: action, isZeroSearch: isZeroSearch)
125+
case HomepageActionType.availableContentHeightDidChange:
126+
return handleAvailableContentHeightChangeAction(state: state, action: action)
120127
case GeneralBrowserActionType.didSelectedTabChangeToHomepage:
121128
return handleDidTabChangeToHomepageAction(state: state, action: action)
122129
case HomepageMiddlewareActionType.configuredSpacer:
@@ -139,7 +146,8 @@ struct HomepageState: ScreenState, Equatable {
139146
wallpaperState: WallpaperState.reducer(state.wallpaperState, action),
140147
isZeroSearch: state.isZeroSearch,
141148
shouldTriggerImpression: false,
142-
shouldShowSpacer: state.shouldShowSpacer
149+
shouldShowSpacer: state.shouldShowSpacer,
150+
availableContentHeight: state.availableContentHeight
143151
)
144152
}
145153

@@ -158,7 +166,30 @@ struct HomepageState: ScreenState, Equatable {
158166
wallpaperState: WallpaperState.reducer(state.wallpaperState, action),
159167
isZeroSearch: isZeroSearch,
160168
shouldTriggerImpression: false,
161-
shouldShowSpacer: state.shouldShowSpacer
169+
shouldShowSpacer: state.shouldShowSpacer,
170+
availableContentHeight: state.availableContentHeight
171+
)
172+
}
173+
174+
private static func handleAvailableContentHeightChangeAction(state: HomepageState, action: Action) -> HomepageState {
175+
guard let availableContentHeight = (action as? HomepageAction)?.availableContentHeight else {
176+
return defaultState(from: state)
177+
}
178+
179+
return HomepageState(
180+
windowUUID: state.windowUUID,
181+
headerState: HeaderState.reducer(state.headerState, action),
182+
messageState: MessageCardState.reducer(state.messageState, action),
183+
topSitesState: TopSitesSectionState.reducer(state.topSitesState, action),
184+
searchState: SearchBarState.reducer(state.searchState, action),
185+
jumpBackInState: JumpBackInSectionState.reducer(state.jumpBackInState, action),
186+
bookmarkState: BookmarksSectionState.reducer(state.bookmarkState, action),
187+
pocketState: PocketState.reducer(state.pocketState, action),
188+
wallpaperState: WallpaperState.reducer(state.wallpaperState, action),
189+
isZeroSearch: state.isZeroSearch,
190+
shouldTriggerImpression: false,
191+
shouldShowSpacer: state.shouldShowSpacer,
192+
availableContentHeight: availableContentHeight
162193
)
163194
}
164195

@@ -175,7 +206,8 @@ struct HomepageState: ScreenState, Equatable {
175206
wallpaperState: WallpaperState.reducer(state.wallpaperState, action),
176207
isZeroSearch: state.isZeroSearch,
177208
shouldTriggerImpression: true,
178-
shouldShowSpacer: state.shouldShowSpacer
209+
shouldShowSpacer: state.shouldShowSpacer,
210+
availableContentHeight: state.availableContentHeight
179211
)
180212
}
181213

@@ -196,7 +228,8 @@ struct HomepageState: ScreenState, Equatable {
196228
wallpaperState: WallpaperState.reducer(state.wallpaperState, action),
197229
isZeroSearch: state.isZeroSearch,
198230
shouldTriggerImpression: false,
199-
shouldShowSpacer: isSpacerEnabled
231+
shouldShowSpacer: isSpacerEnabled,
232+
availableContentHeight: state.availableContentHeight
200233
)
201234
}
202235

@@ -233,7 +266,8 @@ struct HomepageState: ScreenState, Equatable {
233266
wallpaperState: wallpaperState,
234267
isZeroSearch: state.isZeroSearch,
235268
shouldTriggerImpression: false,
236-
shouldShowSpacer: state.shouldShowSpacer
269+
shouldShowSpacer: state.shouldShowSpacer,
270+
availableContentHeight: state.availableContentHeight
237271
)
238272
}
239273

firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Homepage Rebuild/Redux/HomepageStateTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ final class HomepageStateTests: XCTestCase {
2727
XCTAssertFalse(initialState.headerState.showiPadSetup)
2828
XCTAssertFalse(initialState.isZeroSearch)
2929
XCTAssertFalse(initialState.shouldTriggerImpression)
30+
XCTAssertEqual(initialState.availableContentHeight, 0)
3031
}
3132

3233
func test_initializeAction_returnsExpectedState() {
@@ -47,6 +48,7 @@ final class HomepageStateTests: XCTestCase {
4748
XCTAssertTrue(newState.headerState.showiPadSetup)
4849
XCTAssertFalse(newState.isZeroSearch)
4950
XCTAssertFalse(initialState.shouldTriggerImpression)
51+
XCTAssertEqual(newState.availableContentHeight, initialState.availableContentHeight)
5052
}
5153

5254
func test_embeddedHomepageAction_withTrueZeroSearch_returnsExpectedState() {
@@ -65,6 +67,7 @@ final class HomepageStateTests: XCTestCase {
6567
XCTAssertEqual(newState.windowUUID, .XCTestDefaultUUID)
6668
XCTAssertTrue(newState.isZeroSearch)
6769
XCTAssertFalse(initialState.shouldTriggerImpression)
70+
XCTAssertEqual(newState.availableContentHeight, initialState.availableContentHeight)
6871
}
6972

7073
func test_embeddedHomepageAction_withFalseZeroSearch_returnsExpectedState() {
@@ -83,6 +86,7 @@ final class HomepageStateTests: XCTestCase {
8386
XCTAssertEqual(newState.windowUUID, .XCTestDefaultUUID)
8487
XCTAssertFalse(newState.isZeroSearch)
8588
XCTAssertFalse(initialState.shouldTriggerImpression)
89+
XCTAssertEqual(newState.availableContentHeight, initialState.availableContentHeight)
8690
}
8791

8892
func test_didSelectedTabChangeToHomepageAction_returnsExpectedState() {
@@ -100,6 +104,26 @@ final class HomepageStateTests: XCTestCase {
100104
XCTAssertEqual(newState.windowUUID, .XCTestDefaultUUID)
101105
XCTAssertFalse(newState.isZeroSearch)
102106
XCTAssertTrue(newState.shouldTriggerImpression)
107+
XCTAssertEqual(newState.availableContentHeight, initialState.availableContentHeight)
108+
}
109+
110+
func test_handleAvailableContentHeightChangeAction_returnsExpectedState() {
111+
let initialState = createSubject()
112+
let reducer = homepageReducer()
113+
114+
let newState = reducer(
115+
initialState,
116+
HomepageAction(
117+
availableContentHeight: 500,
118+
windowUUID: .XCTestDefaultUUID,
119+
actionType: HomepageActionType.availableContentHeightDidChange
120+
)
121+
)
122+
123+
XCTAssertEqual(newState.availableContentHeight, 500)
124+
XCTAssertEqual(newState.windowUUID, .XCTestDefaultUUID)
125+
XCTAssertFalse(newState.shouldTriggerImpression)
126+
XCTAssertEqual(newState.isZeroSearch, initialState.isZeroSearch)
103127
}
104128

105129
// MARK: - Private

0 commit comments

Comments
 (0)