From 8ac1e19a115c50fde46b4a4566e3e9d119b50cfc Mon Sep 17 00:00:00 2001 From: Carson Ramsden Date: Mon, 16 Dec 2024 10:04:57 -0700 Subject: [PATCH] Refactor FXIOS-10541 [Homepage Rebuild] Add scrolling logic to the new homepage (#23758) Add scrolling logic to new homepage --- .../Browser/BrowserCoordinator.swift | 1 + .../HomepageViewController.swift | 52 ++++++++++++++++ .../HomepageViewControllerTests.swift | 62 ++++++++++++++++++- 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift index ffe037d01ed5..6de5ba505f45 100644 --- a/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift @@ -160,6 +160,7 @@ class BrowserCoordinator: BaseCoordinator, return } self.homepageViewController = homepageController + homepageController.scrollToTop() } func showPrivateHomepage(overlayManager: OverlayModeManager) { diff --git a/firefox-ios/Client/Frontend/Home/Homepage Rebuild/HomepageViewController.swift b/firefox-ios/Client/Frontend/Home/Homepage Rebuild/HomepageViewController.swift index 359cdfeee7c4..48974129e917 100644 --- a/firefox-ios/Client/Frontend/Home/Homepage Rebuild/HomepageViewController.swift +++ b/firefox-ios/Client/Frontend/Home/Homepage Rebuild/HomepageViewController.swift @@ -5,9 +5,11 @@ import Foundation import Common import Redux +import Shared final class HomepageViewController: UIViewController, UICollectionViewDelegate, + FeatureFlaggable, ContentContainable, Themeable, Notifiable, @@ -46,6 +48,7 @@ final class HomepageViewController: UIViewController, private var overlayManager: OverlayModeManager private var logger: Logger private var homepageState: HomepageState + private var lastContentOffsetY: CGFloat = 0 private var currentTheme: Theme { themeManager.getCurrentTheme(for: windowUUID) @@ -123,7 +126,25 @@ final class HomepageViewController: UIViewController, wallpaperView.updateImageForOrientationChange() } + // called when the homepage is displayed to make sure it's scrolled to top + func scrollToTop(animated: Bool = false) { + collectionView?.setContentOffset(.zero, animated: animated) + if let collectionView = collectionView { + handleScroll(collectionView, isUserInteraction: false) + } + } + func scrollViewDidScroll(_ scrollView: UIScrollView) { + handleScroll(scrollView, isUserInteraction: true) + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + lastContentOffsetY = scrollView.contentOffset.y + handleToolbarStateOnScroll() + } + + private func handleScroll(_ scrollView: UIScrollView, isUserInteraction: Bool) { + // We only handle status bar overlay alpha if there's a wallpaper applied on the homepage if homepageState.wallpaperState.wallpaperConfiguration.hasImage { let theme = themeManager.getCurrentTheme(for: windowUUID) statusBarScrollDelegate?.scrollViewDidScroll( @@ -132,6 +153,37 @@ final class HomepageViewController: UIViewController, theme: theme ) } + // this action controls the address toolbar's border position, and to prevent spamming redux with actions for every + // change in content offset, we keep track of lastContentOffsetY to know if the border needs to be updated + if (lastContentOffsetY > 0 && scrollView.contentOffset.y <= 0) || + (lastContentOffsetY <= 0 && scrollView.contentOffset.y > 0) { + lastContentOffsetY = scrollView.contentOffset.y + store.dispatch( + GeneralBrowserMiddlewareAction( + scrollOffset: scrollView.contentOffset, + windowUUID: windowUUID, + actionType: GeneralBrowserMiddlewareActionType.websiteDidScroll)) + } + } + + private func handleToolbarStateOnScroll() { + // TODO: FXIOS-10877 This logic will be handled by toolbar state, the homepage will just dispatch the action + let toolbarState = store.state.screenState(ToolbarState.self, for: .toolbar, window: windowUUID) + + // Only dispatch action when user is in edit mode to avoid having the toolbar re-displayed + if featureFlags.isFeatureEnabled(.toolbarRefactor, checking: .buildOnly), + let toolbarState, + toolbarState.addressToolbar.isEditing { + // When the user scrolls the homepage (not overlaid on a webpage when searching) we cancel edit mode + // On a website we just dismiss the keyboard + if toolbarState.addressToolbar.url == nil { + let action = ToolbarAction(windowUUID: windowUUID, actionType: ToolbarActionType.cancelEdit) + store.dispatch(action) + } else { + let action = ToolbarAction(windowUUID: windowUUID, actionType: ToolbarActionType.hideKeyboard) + store.dispatch(action) + } + } } // MARK: - Redux diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Homepage Rebuild/HomepageViewControllerTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Homepage Rebuild/HomepageViewControllerTests.swift index 0ba5a7cfd651..1145a455409a 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Homepage Rebuild/HomepageViewControllerTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Frontend/Homepage Rebuild/HomepageViewControllerTests.swift @@ -7,20 +7,23 @@ import Common @testable import Client -final class HomepageViewControllerTests: XCTestCase { +final class HomepageViewControllerTests: XCTestCase, StoreTestUtility { let windowUUID: WindowUUID = .XCTestDefaultUUID var mockNotificationCenter: MockNotificationCenter? var mockThemeManager: MockThemeManager? + var mockStore: MockStoreForMiddleware! override func setUp() { super.setUp() DependencyHelperMock().bootstrapDependencies() + setupStore() } override func tearDown() { mockNotificationCenter = nil mockThemeManager = nil DependencyHelperMock().reset() + resetStore() super.tearDown() } @@ -97,6 +100,50 @@ final class HomepageViewControllerTests: XCTestCase { XCTAssertEqual(mockStatusBarScrollDelegate.savedScrollView, scrollView) } + func test_scrollToTop_updatesStatusBarScrollDelegate_andSetsCollectionViewOffset() { + let mockStatusBarScrollDelegate = MockStatusBarScrollDelegate() + let homepageVC = createSubject(statusBarScrollDelegate: mockStatusBarScrollDelegate) + let wallpaperConfiguration = WallpaperConfiguration(hasImage: true) + let newState = HomepageState.reducer( + HomepageState(windowUUID: .XCTestDefaultUUID), + WallpaperAction( + wallpaperConfiguration: wallpaperConfiguration, + windowUUID: .XCTestDefaultUUID, + actionType: WallpaperMiddlewareActionType.wallpaperDidInitialize + ) + ) + + guard let collectionView = homepageVC.view.subviews.first(where: { + $0 is UICollectionView + }) as? UICollectionView else { + XCTFail() + return + } + + homepageVC.newState(state: newState) + homepageVC.scrollToTop() + + XCTAssertEqual(collectionView.contentOffset, .zero) + XCTAssertEqual(mockStatusBarScrollDelegate.savedScrollView, collectionView) + } + + func test_scrollViewDidScroll_TriggersGeneralBrowserMiddlewareAction() throws { + let mockStatusBarScrollDelegate = MockStatusBarScrollDelegate() + let homepageVC = createSubject(statusBarScrollDelegate: mockStatusBarScrollDelegate) + let scrollView = UIScrollView() + scrollView.contentOffset.y = 10 + + homepageVC.scrollViewDidScroll(scrollView) + + let actionCalled = try XCTUnwrap( + mockStore.dispatchedActions.first(where: { + $0 is GeneralBrowserMiddlewareAction + }) as? GeneralBrowserMiddlewareAction + ) + let actionType = try XCTUnwrap(actionCalled.actionType as? GeneralBrowserMiddlewareActionType) + XCTAssertEqual(actionType, GeneralBrowserMiddlewareActionType.websiteDidScroll) + } + private func createSubject(statusBarScrollDelegate: StatusBarScrollDelegate? = nil) -> HomepageViewController { let notificationCenter = MockNotificationCenter() let themeManager = MockThemeManager() @@ -113,4 +160,17 @@ final class HomepageViewControllerTests: XCTestCase { trackForMemoryLeaks(homepageViewController) return homepageViewController } + + func setupAppState() -> Client.AppState { + return AppState() + } + + func setupStore() { + mockStore = MockStoreForMiddleware(state: setupAppState()) + StoreTestUtilityHelper.setupStore(with: mockStore) + } + + func resetStore() { + StoreTestUtilityHelper.resetStore() + } }