diff --git a/BrowserKit/Sources/OnboardingKit/Views/Compact/OnboardingViewCompact.swift b/BrowserKit/Sources/OnboardingKit/Views/Compact/OnboardingViewCompact.swift index c46f6fb8e1d1e..915de11ba050b 100644 --- a/BrowserKit/Sources/OnboardingKit/Views/Compact/OnboardingViewCompact.swift +++ b/BrowserKit/Sources/OnboardingKit/Views/Compact/OnboardingViewCompact.swift @@ -62,6 +62,7 @@ struct OnboardingViewCompact: Themea } .ignoresSafeArea(.all, edges: .bottom) } + .animation(.easeOut, value: geo.size) } .accessibilityElement(children: .contain) .listenToThemeChanges(theme: $theme, manager: themeManager, windowUUID: windowUUID) diff --git a/BrowserKit/Sources/OnboardingKit/Views/Helper/UX.swift b/BrowserKit/Sources/OnboardingKit/Views/Helper/UX.swift index e54ab21afc775..87779930a9372 100644 --- a/BrowserKit/Sources/OnboardingKit/Views/Helper/UX.swift +++ b/BrowserKit/Sources/OnboardingKit/Views/Helper/UX.swift @@ -96,10 +96,11 @@ enum UX { struct LaunchScreen { struct Logo { - static let size: CGFloat = 125 static let rotationDuration: TimeInterval = 2.0 - static let rotationAngle: Double = 360 + static let rotationAngle: Double = .pi * 2.0 static let image = "firefoxLoader" + static let animationKey = "rotationAnimation" + static let animationKeyPath = "transform.rotation.z" } } diff --git a/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/LaunchScreenView.swift b/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/LaunchScreenView.swift new file mode 100644 index 0000000000000..cf7222c5ed288 --- /dev/null +++ b/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/LaunchScreenView.swift @@ -0,0 +1,58 @@ +// 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 SwiftUI +import Common + +public struct LaunchScreenBackgroundView: View { + private let windowUUID: WindowUUID + private let themeManager: ThemeManager + + public init(windowUUID: WindowUUID, themeManager: ThemeManager) { + self.windowUUID = windowUUID + self.themeManager = themeManager + } + + public var body: some View { + AnimatedGradientView( + windowUUID: windowUUID, + themeManager: themeManager + ) + .ignoresSafeArea() + } +} + +public class LaunchScreenLoaderView: UIView { + private let imageView: UIImageView = .build { + $0.image = UIImage(named: UX.LaunchScreen.Logo.image, in: Bundle.module, with: nil) + $0.contentMode = .scaleAspectFit + } + + override public init(frame: CGRect) { + super.init(frame: frame) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(imageView) + imageView.pinToSuperview() + } + + public func startAnimating() { + imageView.layer.removeAnimation(forKey: UX.LaunchScreen.Logo.animationKey) + + let rotationAnimation = CABasicAnimation(keyPath: UX.LaunchScreen.Logo.animationKeyPath) + rotationAnimation.fromValue = 0.0 + rotationAnimation.toValue = UX.LaunchScreen.Logo.rotationAngle + rotationAnimation.duration = UX.LaunchScreen.Logo.rotationDuration + rotationAnimation.repeatCount = HUGE + rotationAnimation.isCumulative = true + + imageView.layer.add(rotationAnimation, forKey: UX.LaunchScreen.Logo.animationKey) + } +} diff --git a/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/ModernLaunchScreenView.swift b/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/ModernLaunchScreenView.swift deleted file mode 100644 index 96ee79d92c9a4..0000000000000 --- a/BrowserKit/Sources/OnboardingKit/Views/LaunchScreen/ModernLaunchScreenView.swift +++ /dev/null @@ -1,71 +0,0 @@ -// 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 SwiftUI -import Common - -public struct ModernLaunchScreenView: View { - @State private var rotationAngle: Double = 0 - @State private var isAnimating = false - - private let windowUUID: WindowUUID - private let themeManager: ThemeManager - - public init(windowUUID: WindowUUID, themeManager: ThemeManager) { - self.windowUUID = windowUUID - self.themeManager = themeManager - } - - public var body: some View { - ZStack { - AnimatedGradientView( - windowUUID: windowUUID, - themeManager: themeManager - ) - .ignoresSafeArea() - - Image(UX.LaunchScreen.Logo.image, bundle: Bundle.module) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UX.LaunchScreen.Logo.size, height: UX.LaunchScreen.Logo.size) - .rotationEffect(.degrees(rotationAngle)) - .animation( - .linear(duration: UX.LaunchScreen.Logo.rotationDuration).repeatForever(autoreverses: false), - value: rotationAngle - ) - .accessibilityHidden(true) - } - .onAppear { - startAnimation() - } - .onDisappear { - stopAnimation() - } - } - - public func startAnimation() { - guard !isAnimating else { return } - isAnimating = true - rotationAngle = UX.LaunchScreen.Logo.rotationAngle - } - - public func stopAnimation() { - isAnimating = false - rotationAngle = 0 - } -} - -// MARK: - Preview -#if DEBUG -struct ModernLaunchScreenView_Previews: PreviewProvider { - static var previews: some View { - ModernLaunchScreenView( - windowUUID: .DefaultUITestingUUID, - themeManager: DefaultThemeManager(sharedContainerIdentifier: "") - ) - .previewDevice("iPhone 15 Pro") - .previewDisplayName("Modern Launch Screen") - } -} -#endif diff --git a/BrowserKit/Sources/OnboardingKit/Views/TermsOfServiceView/TermsOfServiceCompactView.swift b/BrowserKit/Sources/OnboardingKit/Views/TermsOfServiceView/TermsOfServiceCompactView.swift index 2eddd3b521750..6af26a755b70b 100644 --- a/BrowserKit/Sources/OnboardingKit/Views/TermsOfServiceView/TermsOfServiceCompactView.swift +++ b/BrowserKit/Sources/OnboardingKit/Views/TermsOfServiceView/TermsOfServiceCompactView.swift @@ -41,6 +41,7 @@ public struct TermsOfServiceCompactView = { - let controller = UIHostingController(rootView: modernLaunchView) + private let loaderView: LaunchScreenLoaderView = .build { + $0.isAccessibilityElement = false + } + private lazy var backgroundViewController: UIHostingController = { + let controller = UIHostingController( + rootView: LaunchScreenBackgroundView(windowUUID: windowUUID, themeManager: themeManager) + ) controller.view.backgroundColor = .clear return controller }() @@ -81,6 +83,7 @@ class ModernLaunchScreenViewController: UIViewController, LaunchFinishedLoadingD override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + loaderView.startAnimating() // TODO: FXIOS-13434 Refactor the `LaunchScreenViewModel` to enhance the logic // making it easier to comprehend and facilitating unit testing. // Only load next launch type if loading is complete, otherwise defer it @@ -93,11 +96,6 @@ class ModernLaunchScreenViewController: UIViewController, LaunchFinishedLoadingD } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - startLoaderAnimation() - } - // MARK: - Loading func startLoading() { isLoading = true @@ -106,36 +104,30 @@ class ModernLaunchScreenViewController: UIViewController, LaunchFinishedLoadingD // MARK: - Setup private func setupLayout() { - addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.didMove(toParent: self) + addChild(backgroundViewController) + view.addSubviews(backgroundViewController.view, loaderView) + backgroundViewController.didMove(toParent: self) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false + backgroundViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), - hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + backgroundViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + backgroundViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + backgroundViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + loaderView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loaderView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + loaderView.heightAnchor.constraint(lessThanOrEqualToConstant: UX.logoSize), + loaderView.widthAnchor.constraint(lessThanOrEqualToConstant: UX.logoSize) ]) } - // MARK: - Animation Control - func startLoaderAnimation() { - modernLaunchView.startAnimation() - } - - func stopLoaderAnimation() { - modernLaunchView.stopAnimation() - } - // MARK: - LaunchFinishedLoadingDelegate func launchWith(launchType: LaunchType) { - stopLoaderAnimation() coordinator?.launchWith(launchType: launchType) } func launchBrowser() { - stopLoaderAnimation() coordinator?.launchBrowser() } @@ -149,10 +141,14 @@ class ModernLaunchScreenViewController: UIViewController, LaunchFinishedLoadingD } } + func loadNextLaunchType() { + viewModel.loadNextLaunchType() + } + // MARK: - Themeable Protocol func applyTheme() { let theme = themeManager.getCurrentTheme(for: windowUUID) - view.backgroundColor = theme.colors.layer1 + view.backgroundColor = theme.colors.gradientOnboardingStop3 } } diff --git a/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionAnimator.swift b/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionAnimator.swift deleted file mode 100644 index f46bd1f2e2902..0000000000000 --- a/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionAnimator.swift +++ /dev/null @@ -1,98 +0,0 @@ -// 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 - -/// Custom transition animator for modern launch screen to onboarding/ToS transitions -class ModernLaunchTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { - let isDismissing: Bool - - init(isDismissing: Bool) { - self.isDismissing = isDismissing - } - - // MARK: - UX Constants - private enum UX { - static let totalDuration: TimeInterval = 0.4 - static let clearAlpha: CGFloat = 0.0 - static let midAlpha: CGFloat = 0.6 - static let opaqueAlpha: CGFloat = 1.0 - } - - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { - return UX.totalDuration - } - - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - guard let fromViewController = transitionContext.viewController(forKey: .from), - let toViewController = transitionContext.viewController(forKey: .to) else { - transitionContext.completeTransition(false) - return - } - - if isDismissing { - animateDismiss( - using: transitionContext, - fromController: fromViewController, - toController: toViewController - ) - } else { - animatePresent( - using: transitionContext, - fromController: fromViewController, - toController: toViewController - ) - } - } - - func animatePresent( - using transitionContext: UIViewControllerContextTransitioning, - fromController: UIViewController, - toController: UIViewController - ) { - let fromSnapshot = fromController.view.snapshot - let image = UIImageView(image: fromSnapshot) - let containerView = transitionContext.containerView - let finalFrame = transitionContext.finalFrame(for: toController) - let launchController = fromController as? ModernLaunchScreenViewController - - toController.view.frame = finalFrame - toController.view.alpha = UX.midAlpha - - containerView.addSubview(toController.view) - containerView.addSubview(image) - image.pinToSuperview() - - UIView.animate(withDuration: UX.totalDuration) { - toController.view.alpha = UX.opaqueAlpha - launchController?.stopLoaderAnimation() - image.alpha = UX.clearAlpha - } completion: { _ in - image.removeFromSuperview() - transitionContext.completeTransition(true) - } - } - - func animateDismiss( - using transitionContext: UIViewControllerContextTransitioning, - fromController: UIViewController, - toController: UIViewController - ) { - let containerView = transitionContext.containerView - let finalFrame = transitionContext.finalFrame(for: toController) - toController.view.frame = finalFrame - let launchController = toController as? ModernLaunchScreenViewController - - containerView.addSubview(toController.view) - containerView.addSubview(fromController.view) - - UIView.animate(withDuration: UX.totalDuration) { - launchController?.startLoaderAnimation() - fromController.view.alpha = UX.clearAlpha - } completion: { _ in - transitionContext.completeTransition(true) - fromController.view.removeFromSuperview() - } - } -} diff --git a/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionDelegate.swift b/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionDelegate.swift deleted file mode 100644 index 1b0caa4519124..0000000000000 --- a/firefox-ios/Client/Coordinators/LaunchView/ModernLaunchTransitionDelegate.swift +++ /dev/null @@ -1,21 +0,0 @@ -// 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 - -class ModernLaunchTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { - func animationController( - forDismissed dismissed: UIViewController - ) -> (any UIViewControllerAnimatedTransitioning)? { - return ModernLaunchTransitionAnimator(isDismissing: true) - } - - func animationController( - forPresented presented: UIViewController, - presenting: UIViewController, - source: UIViewController - ) -> (any UIViewControllerAnimatedTransitioning)? { - return ModernLaunchTransitionAnimator(isDismissing: false) - } -} diff --git a/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift b/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift index 3cbd95cbaf8cb..4027b6b0fa31d 100644 --- a/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift +++ b/firefox-ios/Client/Coordinators/Scene/SceneCoordinator.swift @@ -20,6 +20,7 @@ class SceneCoordinator: BaseCoordinator, private let windowManager: WindowManager private let reservedWindowUUID: ReservedWindowUUID private let introManager: IntroScreenManagerProtocol + private weak var launchScreenViewController: UIViewController? init(scene: UIScene, sceneSetupHelper: SceneSetupHelper = SceneSetupHelper(), @@ -62,7 +63,7 @@ class SceneCoordinator: BaseCoordinator, // Use legacy launch screen for returning users or when modern onboarding is disabled launchScreenVC = LaunchScreenViewController(windowUUID: windowUUID, coordinator: self) } - + launchScreenViewController = launchScreenVC router.push(launchScreenVC, animated: false) } @@ -165,6 +166,12 @@ class SceneCoordinator: BaseCoordinator, func didFinishTermsOfService(from coordinator: LaunchCoordinator) { router.dismiss(animated: true) remove(child: coordinator) + // TODO: FXIOS-13434 Refactor the `LaunchScreenViewModel` to enhance the presentation logic + // This workaround is needed since .overFullScreen presentation doesn't call UIViewController lifecycle methods. + guard let launchScreenViewController = launchScreenViewController as? ModernLaunchScreenViewController else { + return + } + launchScreenViewController.loadNextLaunchType() } func didFinishLaunch(from coordinator: LaunchCoordinator) { diff --git a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/SceneCoordinatorTests.swift b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/SceneCoordinatorTests.swift index e87be8bab66f8..9c560eee4fd6a 100644 --- a/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/SceneCoordinatorTests.swift +++ b/firefox-ios/firefox-ios-tests/Tests/ClientTests/Coordinators/SceneCoordinatorTests.swift @@ -88,6 +88,28 @@ final class SceneCoordinatorTests: XCTestCase { XCTAssertNotNil(subject.childCoordinators.first as? BrowserCoordinator) } + func testDidFinishTermsOfService_dimissesCurrentPresentedController() { + let subject = createSubject() + let launchCoordinator = LaunchCoordinator(router: mockRouter, windowUUID: .XCTestDefaultUUID) + + subject.didFinishTermsOfService(from: launchCoordinator) + + XCTAssertEqual(mockRouter.dismissCalled, 1) + } + + func testDidFinishTermsOfService_removesLaunchCoordinator() { + let subject = createSubject() + let launchCoordinator = LaunchCoordinator(router: mockRouter, windowUUID: .XCTestDefaultUUID) + subject.add(child: launchCoordinator) + + subject.didFinishTermsOfService(from: launchCoordinator) + + let numberOfLaunchCoordinators = subject.childCoordinators.count { + $0 is LaunchCoordinator + } + XCTAssertEqual(numberOfLaunchCoordinators, 0) + } + // MARK: - Handle route func testHandleRoute_launchNotFinished_routeSaved() {