diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-05-05 16:42:24 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-05-05 16:42:24 +0200 |
| commit | 9f00d3357399f35a2e98cbbbab5c93cac6647f59 (patch) | |
| tree | a0c0c8949eaa1652a7a58b01262f510e9001ea3c | |
| parent | 1140b09f99add401916bd91bfafa5006a13b9c3e (diff) | |
| parent | dc63878e37c7d739281b78770d2cb1bfbb53f299 (diff) | |
| download | mullvadvpn-9f00d3357399f35a2e98cbbbab5c93cac6647f59.tar.xz mullvadvpn-9f00d3357399f35a2e98cbbbab5c93cac6647f59.zip | |
Merge branch 'add-notification-controller-into-root'
6 files changed, 181 insertions, 32 deletions
diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift index b1f742b982..1577c93020 100644 --- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift +++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift @@ -707,6 +707,62 @@ class RootContainerViewController: UIViewController { // Tell accessibility that the significant part of screen was changed. UIAccessibility.post(notification: .screenChanged, argument: nil) } + + // MARK: - Notification controller support + + /** + An instance of notification controller presented within container. + */ + var notificationController: NotificationController? { + didSet { + guard oldValue != notificationController else { return } + + oldValue.flatMap { removeNotificationController($0) } + notificationController.flatMap { addNotificationController($0) } + } + } + + /** + Layout guide for notification view. + + When set, notification view follows the layout guide that defines its dimensions and position, otherwise it's + laid out within container's safe area. + */ + var notificationViewLayoutGuide: UILayoutGuide? { + didSet { + notificationController.flatMap { updateNotificationViewConstraints($0) } + } + } + + private var notificationViewConstraints: [NSLayoutConstraint] = [] + + private func updateNotificationViewConstraints(_ notificationController: NotificationController) { + let newConstraints = notificationController.view + .pinEdgesTo(notificationViewLayoutGuide ?? view.safeAreaLayoutGuide) + + NSLayoutConstraint.deactivate(notificationViewConstraints) + NSLayoutConstraint.activate(newConstraints) + + notificationViewConstraints = newConstraints + } + + private func addNotificationController(_ notificationController: NotificationController) { + guard let notificationView = notificationController.view else { return } + + notificationView.configureForAutoLayout() + + addChild(notificationController) + view.addSubview(notificationView) + notificationController.didMove(toParent: self) + + updateNotificationViewConstraints(notificationController) + } + + private func removeNotificationController(_ notificationController: NotificationController) { + notificationController.willMove(toParent: nil) + notificationController.view.removeFromSuperview() + notificationController.removeFromParent() + } } /// A UIViewController extension that gives view controllers an access to root container diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index 4b3816b630..53032583d1 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -20,7 +20,7 @@ private let preferredFormSheetContentSize = CGSize(width: 480, height: 640) Application coordinator managing split view and two navigation contexts. */ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate, - UISplitViewControllerDelegate, ApplicationRouterDelegate + UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate { /** Application router. @@ -52,6 +52,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo transitioningDelegate: SecondaryContextTransitioningDelegate() ) + private let notificationController = NotificationController() + private let splitViewController: CustomSplitViewController = { let svc = CustomSplitViewController() svc.minimumPrimaryColumnWidth = UIMetrics.minimumSplitViewSidebarWidth @@ -104,6 +106,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo router = ApplicationRouter(self) addTunnelObserver() + + NotificationManager.shared.delegate = self } func start() { @@ -111,6 +115,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo setupSplitView() } + setNotificationControllerParent(isPrimary: true) + continueFlow(animated: false) } @@ -335,6 +341,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func beginHorizontalFlow(animated: Bool, completion: @escaping () -> Void) { if isPad, secondaryNavigationContainer.presentingViewController == nil { secondaryRootConfiguration.apply(to: secondaryNavigationContainer) + addSecondaryContextPresentationStyleObserver() primaryNavigationContainer.present( secondaryNavigationContainer, @@ -356,12 +363,75 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo */ private func endHorizontalFlow(animated: Bool = true, completion: (() -> Void)? = nil) { if isPad { - secondaryNavigationContainer.dismiss(animated: animated, completion: completion) + removeSecondaryContextPresentationStyleObserver() + + secondaryNavigationContainer.dismiss(animated: animated) { + // Put notification controller back into primary container. + self.setNotificationControllerParent(isPrimary: true) + + completion?() + } } else { completion?() } } + /** + Assigns notification controller to either primary or secondary container making sure that only one of them holds + the reference. + */ + private func setNotificationControllerParent(isPrimary: Bool) { + if isPrimary { + secondaryNavigationContainer.notificationController = nil + primaryNavigationContainer.notificationController = notificationController + } else { + primaryNavigationContainer.notificationController = nil + secondaryNavigationContainer.notificationController = notificationController + } + } + + /** + Start observing secondary context presentation style which is in compact environment turns into fullscreen + and otherwise looks like formsheet. + + In response to compact environment and fullscreen presentation, the observer re-assigns notification controller + from primary to secondary context to mimic the look and feel of iPhone app. The opposite is also true, that it + will make sure that notification controller is presented within primary context when secondary context is in + formsheet presentation style. + */ + private func addSecondaryContextPresentationStyleObserver() { + removeSecondaryContextPresentationStyleObserver() + + NotificationCenter.default.addObserver( + self, + selector: #selector(formSheetControllerWillChangeFullscreenPresentation(_:)), + name: FormsheetPresentationController.willChangeFullScreenPresentation, + object: secondaryNavigationContainer + ) + } + + /** + Stop observing secondary context presentation style. + */ + private func removeSecondaryContextPresentationStyleObserver() { + NotificationCenter.default.removeObserver( + self, + name: FormsheetPresentationController.willChangeFullScreenPresentation, + object: secondaryNavigationContainer + ) + } + + /** + This method is called in response to changes in fullscreen presentation style of form sheet presentation + controller. + */ + @objc private func formSheetControllerWillChangeFullscreenPresentation(_ note: Notification) { + guard let isFullscreenNumber = note + .userInfo?[SecondaryContextPresentationController.isFullScreenUserInfoKey] as? NSNumber else { return } + + setNotificationControllerParent(isPrimary: !isFullscreenNumber.boolValue) + } + private var isPad: Bool { return UIDevice.current.userInterfaceIdiom == .pad } @@ -384,6 +454,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo primaryNavigationContainer.setViewControllers([splitViewController], animated: false) + primaryNavigationContainer.notificationViewLayoutGuide = tunnelCoordinator.rootViewController.view + .safeAreaLayoutGuide + tunnelCoordinator.start() selectLocationCoordinator.start() } @@ -790,6 +863,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo return true } + // MARK: - NotificationManagerDelegate + + func notificationManagerDidUpdateInAppNotifications( + _ manager: NotificationManager, + notifications: [InAppNotificationDescriptor] + ) { + notificationController.setNotifications(notifications, animated: true) + } + // MARK: - Presenting var presentationContext: UIViewController { diff --git a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift index c63b0f80ef..bd35d79558 100644 --- a/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift @@ -8,7 +8,7 @@ import UIKit -class TunnelCoordinator: Coordinator, NotificationManagerDelegate { +class TunnelCoordinator: Coordinator { private let tunnelManager: TunnelManager private let controller: TunnelViewController @@ -44,8 +44,6 @@ class TunnelCoordinator: Coordinator, NotificationManagerDelegate { tunnelManager.addObserver(tunnelObserver) updateVisibility(animated: false) - - NotificationManager.shared.delegate = self } private func updateVisibility(animated: Bool) { @@ -53,13 +51,4 @@ class TunnelCoordinator: Coordinator, NotificationManagerDelegate { controller.setMainContentHidden(!deviceState.isLoggedIn, animated: animated) } - - // MARK: - NotificationManagerDelegate - - func notificationManagerDidUpdateInAppNotifications( - _ manager: NotificationManager, - notifications: [InAppNotificationDescriptor] - ) { - controller.notificationController.setNotifications(notifications, animated: true) - } } diff --git a/ios/MullvadVPN/Notifications/UI/NotificationController.swift b/ios/MullvadVPN/Notifications/UI/NotificationController.swift index be87f74a07..14c133433b 100644 --- a/ios/MullvadVPN/Notifications/UI/NotificationController.swift +++ b/ios/MullvadVPN/Notifications/UI/NotificationController.swift @@ -25,6 +25,7 @@ final class NotificationController: UIViewController { override func loadView() { view = NotificationContainerView(frame: UIScreen.main.bounds) + view.clipsToBounds = true } override func viewDidLoad() { diff --git a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift index 047fd84bca..97ab75fefc 100644 --- a/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift +++ b/ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift @@ -16,6 +16,23 @@ private let animationDuration: TimeInterval = 0.5 Custom implementation of a formsheet presentation controller. */ class FormsheetPresentationController: UIPresentationController { + /** + Name of notification posted when fullscreen presentation changes, including during initial presentation. + */ + static let willChangeFullScreenPresentation = Notification + .Name(rawValue: "FormsheetPresentationControllerWillChangeFullScreenPresentation") + + /** + User info key passed along with `willChangeFullScreenPresentation` notification that contains boolean value that + indicates if presentation controller is using fullscreen presentation. + */ + static let isFullScreenUserInfoKey = "isFullScreen" + + /** + Last known presentation style used to prevent emitting duplicate `willChangeFullScreenPresentation` notifications. + */ + private var lastKnownIsInFullScreen: Bool? + private let dimmingView: UIView = { let dimmingView = UIView() dimmingView.backgroundColor = .black @@ -85,6 +102,8 @@ class FormsheetPresentationController: UIPresentationController { } else { revealDimmingView() } + + postFullscreenPresentationWillChangeIfNeeded() } override func presentationTransitionDidEnd(_ completed: Bool) { @@ -112,6 +131,26 @@ class FormsheetPresentationController: UIPresentationController { dimmingView.removeFromSuperview() } } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + postFullscreenPresentationWillChangeIfNeeded() + } + + private func postFullscreenPresentationWillChangeIfNeeded() { + let currentIsInFullScreen = isInFullScreenPresentation + + guard lastKnownIsInFullScreen != currentIsInFullScreen else { return } + + lastKnownIsInFullScreen = currentIsInFullScreen + + NotificationCenter.default.post( + name: Self.willChangeFullScreenPresentation, + object: presentedViewController, + userInfo: [Self.isFullScreenUserInfoKey: NSNumber(booleanLiteral: currentIsInFullScreen)] + ) + } } class FormsheetTransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate { diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift index 4ec24fd400..8f6cc10a18 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift @@ -20,7 +20,6 @@ class TunnelViewController: UIViewController, RootContainment { var shouldShowSelectLocationPicker: (() -> Void)? - let notificationController = NotificationController() private let mapViewController = MapViewController() override var preferredStatusBarStyle: UIStatusBarStyle { @@ -82,7 +81,6 @@ class TunnelViewController: UIViewController, RootContainment { addMapController() addContentView() - addNotificationController() tunnelState = interactor.tunnelStatus.state updateContentView(animated: false) @@ -180,22 +178,6 @@ class TunnelViewController: UIViewController, RootContainment { ]) } - private func addNotificationController() { - let notificationView = notificationController.view! - notificationView.translatesAutoresizingMaskIntoConstraints = false - - addChild(notificationController) - view.addSubview(notificationView) - notificationController.didMove(toParent: self) - - NSLayoutConstraint.activate([ - notificationView.topAnchor.constraint(equalTo: view.topAnchor), - notificationView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - notificationView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - notificationView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } - private func addContentView() { contentView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(contentView) |
