Skip to content

Commit

Permalink
Add FXIOS-9126 [Menu] Add popup sheet menu (#21022)
Browse files Browse the repository at this point in the history
  • Loading branch information
adudenamedruby authored Jul 16, 2024
1 parent ca5d8ca commit 9231f26
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 0 deletions.
20 changes: 20 additions & 0 deletions firefox-ios/Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@
81122E212B221AC0003DD9F8 /* SearchScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81122E202B221AC0003DD9F8 /* SearchScreenState.swift */; };
814A62462B587A3E00608195 /* DefaultThemeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814A62452B587A3E00608195 /* DefaultThemeManagerTests.swift */; };
8187561A2BB4618500DCD1F3 /* OnboardingViewControllerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818756192BB4618500DCD1F3 /* OnboardingViewControllerState.swift */; };
81A3F6F02C2DAEE200BDD86B /* MainMenuCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A3F6EF2C2DAEE200BDD86B /* MainMenuCoordinator.swift */; };
81A3F6F22C2DB00900BDD86B /* MainMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A3F6F12C2DB00900BDD86B /* MainMenuViewController.swift */; };
81A3F6F42C2DD11300BDD86B /* MainMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A3F6F32C2DD11300BDD86B /* MainMenuViewModel.swift */; };
81CAE4DB2B1A2C220040C78A /* BrowserViewControllerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81CAE4DA2B1A2C220040C78A /* BrowserViewControllerState.swift */; };
81E1914D2BB8578600543D78 /* OnboardingMultipleChoiceSelectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E1914C2BB8578600543D78 /* OnboardingMultipleChoiceSelectionDelegate.swift */; };
884CA7492344A301002E4711 /* TextContentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884CA7482344A301002E4711 /* TextContentDetector.swift */; };
Expand Down Expand Up @@ -6160,6 +6163,9 @@
81754147A06566E64C025F70 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = "nn.lproj/Default Browser.strings"; sourceTree = "<group>"; };
818756192BB4618500DCD1F3 /* OnboardingViewControllerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerState.swift; sourceTree = "<group>"; };
81A244F4A7C6FC8976DC21F0 /* br */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = br; path = br.lproj/InfoPlist.strings; sourceTree = "<group>"; };
81A3F6EF2C2DAEE200BDD86B /* MainMenuCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuCoordinator.swift; sourceTree = "<group>"; };
81A3F6F12C2DB00900BDD86B /* MainMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuViewController.swift; sourceTree = "<group>"; };
81A3F6F32C2DD11300BDD86B /* MainMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuViewModel.swift; sourceTree = "<group>"; };
81C14765AA7C25DF1817AC04 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Today.strings; sourceTree = "<group>"; };
81CAE4DA2B1A2C220040C78A /* BrowserViewControllerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewControllerState.swift; sourceTree = "<group>"; };
81DF4C79AE53A2FCA08EEE9C /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = "da.lproj/Default Browser.strings"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -9844,6 +9850,16 @@
path = State;
sourceTree = "<group>";
};
81A3F6EE2C2DAED500BDD86B /* Menu */ = {
isa = PBXGroup;
children = (
81A3F6EF2C2DAEE200BDD86B /* MainMenuCoordinator.swift */,
81A3F6F12C2DB00900BDD86B /* MainMenuViewController.swift */,
81A3F6F32C2DD11300BDD86B /* MainMenuViewModel.swift */,
);
path = Menu;
sourceTree = "<group>";
};
8A0017BF28A3FED300FEFC8B /* MessageCard */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -11615,6 +11631,7 @@
D3A994941A368691008AD1AC /* Browser */ = {
isa = PBXGroup;
children = (
81A3F6EE2C2DAED500BDD86B /* Menu */,
1D7B78952ADF324E0011E9F2 /* Event Queue */,
8A1E3BE428CBBF1E003388C4 /* SearchEngines */,
8A590C5F28C122FF0032F1AA /* OpenInHelper */,
Expand Down Expand Up @@ -14998,6 +15015,7 @@
3BF56D271CDBBE1F00AC4D75 /* SimpleToast.swift in Sources */,
8C4B0F5D2C076B12008B3E74 /* UpdatableAddressFields+Decodable.swift in Sources */,
C8B0F5F6283B7CCE007AE65D /* PocketStory.swift in Sources */,
81A3F6F42C2DD11300BDD86B /* MainMenuViewModel.swift in Sources */,
C81AC6B626160091007800C5 /* LegacyTabTrayViewModel.swift in Sources */,
8ADAE4222A33A113007BF926 /* SendAnonymousUsageDataSetting.swift in Sources */,
E170CA542B72C07A0082EFC5 /* FakespotActionFooterView.swift in Sources */,
Expand Down Expand Up @@ -15063,6 +15081,7 @@
8AB8572E27D94A1A0075C173 /* UXSizeClass.swift in Sources */,
965C3C8F29313A1B006499ED /* AppSessionManager.swift in Sources */,
45D5EDA729269F7500311934 /* DataObserver.swift in Sources */,
81A3F6F02C2DAEE200BDD86B /* MainMenuCoordinator.swift in Sources */,
961577922A38FDB300391E8D /* SponsoredTileDataUtility.swift in Sources */,
D863C8F21F68BFC20058D95F /* GradientProgressBar.swift in Sources */,
C8445A14264428DC00B83F53 /* LibraryPanelViewState.swift in Sources */,
Expand Down Expand Up @@ -15205,6 +15224,7 @@
5A70EF1F295E3DFC00790249 /* UnitTestAppDelegate.swift in Sources */,
2128E27E2934F78600FB91BE /* CustomAppActivity.swift in Sources */,
966B0DC82926F60500A85A7E /* UIResponder+Extensions.swift in Sources */,
81A3F6F22C2DB00900BDD86B /* MainMenuViewController.swift in Sources */,
E16E1C9828C25F1D00EE2EF5 /* SiteTableViewHeader.swift in Sources */,
E177989E2BD7D75A00F6F0EB /* ToolbarMiddleware.swift in Sources */,
C8CD80D72A1E2C6E0097C3AE /* NimbusMessagingHelperUtilityProtocol.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions firefox-ios/Client/Coordinators/Browser/BrowserCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,15 @@ class BrowserCoordinator: BaseCoordinator,
return coordinator
}

private func makeMenuCoordinator() -> MainMenuCoordinator? {
guard !childCoordinators.contains(where: { $0 is MainMenuCoordinator }) else { return nil }

let coordinator = MainMenuCoordinator(router: router, tabManager: tabManager)
coordinator.parentCoordinator = self
add(child: coordinator)
return coordinator
}

func showShareExtension(
url: URL,
sourceView: UIView,
Expand Down Expand Up @@ -716,6 +725,12 @@ class BrowserCoordinator: BaseCoordinator,
router.present(viewController)
}

// MARK: - Main Menu
func showMainMenu() {
guard let coordinator = makeMenuCoordinator() else { return }
coordinator.startModal()
}

// MARK: - ParentCoordinatorDelegate
func didFinish(from childCoordinator: Coordinator) {
remove(child: childCoordinator)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ protocol BrowserNavigationHandler: AnyObject, QRCodeNavigationHandler {
func showBackForwardList()

func showMicrosurvey(model: MicrosurveyModel)

/// Shows the app menu
func showMainMenu()
}

extension BrowserNavigationHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,14 @@ class BrowserViewController: UIViewController,
}

func didTapOnMenu(button: UIButton?) {
if featureFlags.isFeatureEnabled(.menuRefactor, checking: .buildOnly) {
navigationHandler?.showMainMenu()
} else {
showPhotonMainMenu(from: button)
}
}

private func showPhotonMainMenu(from button: UIButton?) {
guard let button else { return }

// Ensure that any keyboards or spinners are dismissed before presenting the menu
Expand Down
47 changes: 47 additions & 0 deletions firefox-ios/Client/Frontend/Browser/Menu/MainMenuCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import Common
import Foundation
import Shared

protocol MainMenuCoordinatorDelegate: AnyObject {
// Define any coordinator delegate methods
}

class MainMenuCoordinator: BaseCoordinator, FeatureFlaggable {
weak var parentCoordinator: ParentCoordinatorDelegate?
private var profile: Profile
private let tabManager: TabManager

init(router: Router,
profile: Profile = AppContainer.shared.resolve(),
tabManager: TabManager) {
self.tabManager = tabManager
self.profile = profile
super.init(router: router)
}

func startModal() {
let viewController = createMainMenuViewController()

if let sheet = viewController.sheetPresentationController {
sheet.detents = [.medium(), .large()]
}
router.present(viewController, animated: true)
}

func dismissModal(animated: Bool) {
router.dismiss(animated: animated, completion: nil)
parentCoordinator?.didFinish(from: self)
}

private func createMainMenuViewController() -> MainMenuViewController {
let mainMenuViewController = MainMenuViewController(
windowUUID: tabManager.windowUUID,
viewModel: MainMenuViewModel(windowUUID: tabManager.windowUUID)
)
return mainMenuViewController
}
}
163 changes: 163 additions & 0 deletions firefox-ios/Client/Frontend/Browser/Menu/MainMenuViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import Common
import ComponentLibrary
import UIKit
import Shared
import Redux

class MainMenuViewController: UIViewController,
Themeable,
Notifiable,
UIAdaptivePresentationControllerDelegate,
UISheetPresentationControllerDelegate,
UIScrollViewDelegate,
StoreSubscriber {
typealias SubscriberStateType = BrowserViewControllerState

private struct UX {
static let closeButtonWidthHeight: CGFloat = 30
static let scrollContentStackSpacing: CGFloat = 16
}

var notificationCenter: NotificationProtocol
var themeManager: ThemeManager
var themeObserver: NSObjectProtocol?

private var viewModel: MainMenuViewModel
private let windowUUID: WindowUUID

var currentWindowUUID: UUID? { return windowUUID }

private lazy var scrollView: UIScrollView = .build()

private lazy var contentStackView: UIStackView = .build { stackView in
stackView.axis = .vertical
stackView.spacing = UX.scrollContentStackSpacing
}

private lazy var closeButton: CloseButton = .build { view in
let viewModel = CloseButtonViewModel(
a11yLabel: .Shopping.CloseButtonAccessibilityLabel,
a11yIdentifier: AccessibilityIdentifiers.Shopping.sheetCloseButton
)
view.configure(viewModel: viewModel)
view.addTarget(self, action: #selector(self.closeTapped), for: .touchUpInside)
}

// MARK: - Initializers
init(
windowUUID: WindowUUID,
viewModel: MainMenuViewModel,
notificationCenter: NotificationProtocol = NotificationCenter.default,
themeManager: ThemeManager = AppContainer.shared.resolve()
) {
self.viewModel = viewModel
self.windowUUID = windowUUID
self.notificationCenter = notificationCenter
self.themeManager = themeManager
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View setup & lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presentationController?.delegate = self
sheetPresentationController?.delegate = self
scrollView.delegate = self

setupNotifications(forObserver: self,
observing: [.DynamicFontChanged])

setupView()
listenForThemeChange(view)
subscribeToRedux()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
applyTheme()
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateModalA11y()
}

// MARK: - View setup
private func setupView() { }

private func updateContent() {
contentStackView.removeAllArrangedViews()
applyTheme()
}

// MARK: - Redux
func subscribeToRedux() {
let uuid = windowUUID
store.subscribe(self, transform: {
$0.select({ appState in
return BrowserViewControllerState(appState: appState, uuid: uuid)
})
})
}

func unsubscribeFromRedux() {
store.unsubscribe(self)
}

func newState(state: BrowserViewControllerState) {
}

// MARK: - UX related
func applyTheme() {
let theme = themeManager.getCurrentTheme(for: windowUUID)
view.backgroundColor = theme.colors.layer1
}

// MARK: - Notifications
func handleNotifications(_ notification: Notification) { }

@objc
private func closeTapped() { }

deinit {
unsubscribeFromRedux()
}

// In iOS 15 modals with a large detent read content underneath the modal
// in voice over. To prevent this we manually turn this off.
private func updateModalA11y() {
var currentDetent: UISheetPresentationController.Detent.Identifier? = viewModel.getCurrentDetent(
for: sheetPresentationController
)

if currentDetent == nil,
let sheetPresentationController,
let firstDetent = sheetPresentationController.detents.first {
if firstDetent == .medium() {
currentDetent = .medium
} else if firstDetent == .large() {
currentDetent = .large
}
}

view.accessibilityViewIsModal = currentDetent == .large ? true : false
}

// MARK: - UIAdaptivePresentationControllerDelegate
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { }

// MARK: - UISheetPresentationControllerDelegate
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(
_ sheetPresentationController: UISheetPresentationController
) {
updateModalA11y()
}
}
23 changes: 23 additions & 0 deletions firefox-ios/Client/Frontend/Browser/Menu/MainMenuViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import UIKit
import Common
import Shared

class MainMenuViewModel {
let windowUUID: WindowUUID

init(profile: Profile = AppContainer.shared.resolve(),
windowUUID: WindowUUID) {
self.windowUUID = windowUUID
}

func getCurrentDetent(
for presentedController: UIPresentationController?
) -> UISheetPresentationController.Detent.Identifier? {
guard let sheetController = presentedController as? UISheetPresentationController else { return nil }
return sheetController.selectedDetentIdentifier
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,31 @@ final class BrowserCoordinatorTests: XCTestCase {
XCTAssertTrue(mockRouter.presentedViewController is BottomSheetViewController)
}

// MARK: - Menu
func testShowMainMenu_addsMainMenuCoordinator() {
let subject = createSubject()

subject.showMainMenu()

XCTAssertEqual(subject.childCoordinators.count, 1)
XCTAssertTrue(subject.childCoordinators.first is MainMenuCoordinator)
XCTAssertEqual(mockRouter.presentCalled, 1)
XCTAssertTrue(mockRouter.presentedViewController is MainMenuViewController)
}

func testMainMenuCoordinatorDelegate_didDidDismiss_removesChild() {
let subject = createSubject()
subject.browserHasLoaded()

subject.showMainMenu()
let menuCoordinator = subject.childCoordinators[0] as! MainMenuCoordinator
menuCoordinator.dismissModal(animated: false)

XCTAssertEqual(mockRouter.dismissCalled, 1)
XCTAssertTrue(subject.childCoordinators.isEmpty)
}

// MARK: - Microsurvey
func testShowMicrosurvey_addsMicrosurveyCoordinator() {
let subject = createSubject()

Expand Down
Loading

0 comments on commit 9231f26

Please sign in to comment.