summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-05-05 16:42:24 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-05-05 16:42:24 +0200
commit9f00d3357399f35a2e98cbbbab5c93cac6647f59 (patch)
treea0c0c8949eaa1652a7a58b01262f510e9001ea3c
parent1140b09f99add401916bd91bfafa5006a13b9c3e (diff)
parentdc63878e37c7d739281b78770d2cb1bfbb53f299 (diff)
downloadmullvadvpn-9f00d3357399f35a2e98cbbbab5c93cac6647f59.tar.xz
mullvadvpn-9f00d3357399f35a2e98cbbbab5c93cac6647f59.zip
Merge branch 'add-notification-controller-into-root'
-rw-r--r--ios/MullvadVPN/Containers/Root/RootContainerViewController.swift56
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift86
-rw-r--r--ios/MullvadVPN/Coordinators/App/TunnelCoordinator.swift13
-rw-r--r--ios/MullvadVPN/Notifications/UI/NotificationController.swift1
-rw-r--r--ios/MullvadVPN/Presentation controllers/FormsheetPresentationController.swift39
-rw-r--r--ios/MullvadVPN/View controllers/Tunnel/TunnelViewController.swift18
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)