summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-01-20 08:57:49 +0100
committerAndrej Mihajlov <and@mullvad.net>2023-01-20 17:12:26 +0100
commit1556599c8f7ffc004b7871cbfa0d30c7d3ff668d (patch)
tree8bf75b51ea9c0177a432f8f26a25bb16b5cbbdde /ios
parent1cbf0c4a2b34b6150696edcd52b704686990bb28 (diff)
downloadmullvadvpn-1556599c8f7ffc004b7871cbfa0d30c7d3ff668d.tar.xz
mullvadvpn-1556599c8f7ffc004b7871cbfa0d30c7d3ff668d.zip
Move adaptive presentation delegate from scene delegate into its own class.
Also fix issues in former implementation where transitionCoordinator.containerView used to return UIWindow when changing to split screen. New implementation makes a manual lookup of UITransitionView to ensure that the settings cog is added into the right container view. In the future this better be replaced with UISheetPresentationController subclass but for now we'll keep adding the settings cog manually within the adaptivity call.
Diffstat (limited to 'ios')
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift127
-rw-r--r--ios/MullvadVPN/SceneDelegate.swift113
3 files changed, 163 insertions, 81 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 1994428b55..9c6b216d72 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -204,6 +204,7 @@
58A8EE5A2976BFBB009C0F8D /* SKError+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */; };
58A8EE5E2976DB00009C0F8D /* StorePaymentManagerError+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */; };
58A99ED3240014A0006599E9 /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */; };
+ 58ACA9ED2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift */; };
58ACF6492655365700ACE4B7 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */; };
58ACF64B26553C3F00ACE4B7 /* SettingsSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */; };
58ACF64D26567A5000ACE4B7 /* CustomSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */; };
@@ -803,6 +804,7 @@
58A8EE5D2976DB00009C0F8D /* StorePaymentManagerError+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorePaymentManagerError+Display.swift"; sourceTree = "<group>"; };
58A94AE326CFD945001CB97C /* TunnelStatusNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatusNotificationProvider.swift; sourceTree = "<group>"; };
58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = "<group>"; };
+ 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalRootAdaptivePresentationDelegate.swift; sourceTree = "<group>"; };
58ACF6482655365700ACE4B7 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = "<group>"; };
58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSwitchCell.swift; sourceTree = "<group>"; };
58ACF64C26567A4F00ACE4B7 /* CustomSwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSwitch.swift; sourceTree = "<group>"; };
@@ -1490,6 +1492,7 @@
58F7CA872692E34000FC59FD /* WireguardKeysContentView.swift */,
58E11187292FA11F009FCA84 /* SettingsMigrationUIHandler.swift */,
58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */,
+ 58ACA9EC2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift */,
);
path = MullvadVPN;
sourceTree = "<group>";
@@ -2413,6 +2416,7 @@
58B9EB152489139B00095626 /* RESTError+Display.swift in Sources */,
587B753F2668E5A700DEF7E9 /* NotificationContainerView.swift in Sources */,
58421034282E4B1500F24E46 /* TunnelSettingsV2+REST.swift in Sources */,
+ 58ACA9ED2979569500B5825C /* ModalRootAdaptivePresentationDelegate.swift in Sources */,
58F2E144276A13F300A79513 /* StartTunnelOperation.swift in Sources */,
5868BD33261DCD2600E6027F /* CustomSplitViewController.swift in Sources */,
58CCA01E2242787B004F3011 /* AccountTextField.swift in Sources */,
diff --git a/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift b/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift
new file mode 100644
index 0000000000..2a6d250196
--- /dev/null
+++ b/ios/MullvadVPN/ModalRootAdaptivePresentationDelegate.swift
@@ -0,0 +1,127 @@
+//
+// ModalRootAdaptivePresentationDelegate.swift
+// MullvadVPN
+//
+// Created by pronebird on 19/01/2023.
+// Copyright © 2023 Mullvad VPN AB. All rights reserved.
+//
+
+import UIKit
+
+/**
+
+ Adaptive presentation delegate for `SceneDelegate.modalRootContainer` used for presenting
+ the login flow on iPad.
+
+ The primary purpose of this class is to swap between fullscreen and formsheet presentation based
+ on horizontal size class and make settings (cog) accessible even when parent root is overlayed with
+ modal root.
+
+ Unlike iPhone where only one `RootContainerViewController` is used and behaves very much like
+ navigation controller, iPad uses two of such controllers defined as parent and (think child) modal
+ within this class.
+
+ ## iPhone view controller hierarchy
+
+ - UIWindow
+ - RootContainerViewController
+ - LoginViewController
+ - etc.
+
+ ## iPad view controller hierarchy
+
+ - UIWindow
+ - RootContainerViewController (parent)
+ - UISplitViewController
+ - TunnelViewController
+ - SelectLocationViewController
+ - RootContainerViewController (child [modal])
+ - LoginViewController
+ - etc.
+
+ */
+final class ModalRootAdaptivePresentationDelegate: NSObject,
+ UIAdaptivePresentationControllerDelegate
+{
+ let parentRootContainer: RootContainerViewController
+ let modalRootContainer: RootContainerViewController
+
+ init(
+ parentRootContainer: RootContainerViewController,
+ modalRootContainer: RootContainerViewController
+ ) {
+ self.parentRootContainer = parentRootContainer
+ self.modalRootContainer = modalRootContainer
+
+ super.init()
+ }
+
+ func finishPresentation() {
+ parentRootContainer.removeSettingsButtonFromPresentationContainer()
+ }
+
+ func adaptivePresentationStyle(
+ for controller: UIPresentationController,
+ traitCollection: UITraitCollection
+ ) -> UIModalPresentationStyle {
+ if controller.presentedViewController is RootContainerViewController {
+ return traitCollection.horizontalSizeClass == .regular ? .formSheet : .fullScreen
+ } else {
+ return .none
+ }
+ }
+
+ func presentationController(
+ _ presentationController: UIPresentationController,
+ willPresentWithAdaptiveStyle style: UIModalPresentationStyle,
+ transitionCoordinator: UIViewControllerTransitionCoordinator?
+ ) {
+ // The style is set to none when adaptive presentation is not changing.
+ let actualStyle: UIModalPresentationStyle = style == .none
+ ? presentationController.presentedViewController.modalPresentationStyle
+ : style
+
+ // Force hide header bar in .formSheet presentation and show it in .fullScreen presentation
+ modalRootContainer.setOverrideHeaderBarHidden(actualStyle == .formSheet, animated: false)
+
+ let transitionActions = {
+ if let containerView = self.modalRootContainer.modalPresentationContainerView {
+ self.parentRootContainer.addSettingsButtonToPresentationContainer(containerView)
+ }
+ }
+
+ if actualStyle == .formSheet {
+ // Add settings button into the modal container to make it accessible by users
+ if let transitionCoordinator = transitionCoordinator {
+ transitionCoordinator.animate { _ in
+ transitionActions()
+ }
+ } else {
+ transitionActions()
+ }
+ } else {
+ // Move settings button back into header bar
+ finishPresentation()
+ }
+ }
+
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
+ finishPresentation()
+ }
+}
+
+private extension UIViewController {
+ /// Returns private `UITransitionView` used by UIKit that acts as a container view for modally
+ /// presented controllers. When implementing a presentation controller subclass, this view
+ /// is the one that is used to add additional decorations.
+ var modalPresentationContainerView: UIView? {
+ var currentView = view
+ let iterator = AnyIterator { () -> UIView? in
+ currentView = currentView?.superview
+ return currentView
+ }
+ return iterator.first { view -> Bool in
+ return view.description.starts(with: "<UITransitionView")
+ }
+ }
+}
diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift
index a08ebcca75..549ec3c28e 100644
--- a/ios/MullvadVPN/SceneDelegate.swift
+++ b/ios/MullvadVPN/SceneDelegate.swift
@@ -14,12 +14,11 @@ import RelayCache
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate,
- UIAdaptivePresentationControllerDelegate, RootContainerViewControllerDelegate,
- LoginViewControllerDelegate, DeviceManagementViewControllerDelegate,
- SettingsNavigationControllerDelegate, OutOfTimeViewControllerDelegate,
- SelectLocationViewControllerDelegate, RevokedDeviceViewControllerDelegate,
- NotificationManagerDelegate, TunnelObserver, RelayCacheTrackerObserver,
- SettingsMigrationUIHandler
+ RootContainerViewControllerDelegate, LoginViewControllerDelegate,
+ DeviceManagementViewControllerDelegate, SettingsNavigationControllerDelegate,
+ OutOfTimeViewControllerDelegate, SelectLocationViewControllerDelegate,
+ RevokedDeviceViewControllerDelegate, NotificationManagerDelegate, TunnelObserver,
+ RelayCacheTrackerObserver, SettingsMigrationUIHandler
{
private let logger = Logger(label: "SceneDelegate")
@@ -32,6 +31,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
// Modal root container is used on iPad to present login, TOS, revoked device, device management
// view controllers above `rootContainer` which only contains split controller.
private lazy var modalRootContainer = RootContainerViewController()
+ private lazy var modalRootAdaptivePresentationDelegate = ModalRootAdaptivePresentationDelegate(
+ parentRootContainer: rootContainer,
+ modalRootContainer: modalRootContainer
+ )
private var splitViewController: CustomSplitViewController?
private var selectLocationViewController: SelectLocationViewController?
@@ -300,23 +303,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
switch self.tunnelManager.deviceState {
case .loggedIn:
- let didDismissModalRoot = {
- self.handleExpiredAccount()
- }
-
self.modalRootContainer.setViewControllers(
viewControllers,
animated: self.isModalRootPresented && animated
)
// Dismiss modal root container if needed before proceeding.
- if self.isModalRootPresented {
- self.modalRootContainer.dismiss(
- animated: animated,
- completion: didDismissModalRoot
- )
- } else {
- didDismissModalRoot()
+ self.dismissModalRootContainerIfNeeded(animated: animated) {
+ self.handleExpiredAccount()
}
return
@@ -350,13 +344,27 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
}
private func presentModalRootContainerIfNeeded(animated: Bool) {
+ guard !isModalRootPresented else { return }
+
modalRootContainer.preferredContentSize = CGSize(width: 480, height: 600)
- modalRootContainer.modalPresentationStyle = .formSheet
- modalRootContainer.presentationController?.delegate = self
+ modalRootContainer.presentationController?.delegate = modalRootAdaptivePresentationDelegate
modalRootContainer.isModalInPresentation = true
- if modalRootContainer.presentingViewController == nil {
- rootContainer.present(modalRootContainer, animated: animated)
+ rootContainer.present(modalRootContainer, animated: animated)
+ }
+
+ private func dismissModalRootContainerIfNeeded(
+ animated: Bool,
+ completion: @escaping () -> Void
+ ) {
+ guard isModalRootPresented else {
+ completion()
+ return
+ }
+
+ modalRootContainer.dismiss(animated: animated) {
+ self.modalRootAdaptivePresentationDelegate.finishPresentation()
+ completion()
}
}
@@ -678,7 +686,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
lastLoginAction = nil
// Move the settings button back into header bar
- rootContainer.removeSettingsButtonFromPresentationContainer()
setEnableSettingsButton(isEnabled: true, from: controller)
let relayConstraints = tunnelManager.settings.relayConstraints
@@ -696,12 +703,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
viewControllers.append(tunnelViewController)
rootContainer.setViewControllers(viewControllers, animated: true)
handleExpiredAccount()
+
case .pad:
showSplitViewMaster(true, animated: true)
- controller.dismiss(animated: true) {
+ dismissModalRootContainerIfNeeded(animated: true) {
self.handleExpiredAccount()
}
+
default:
fatalError()
}
@@ -831,64 +840,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
}
}
- // MARK: - UIAdaptivePresentationControllerDelegate
-
- func adaptivePresentationStyle(
- for controller: UIPresentationController,
- traitCollection: UITraitCollection
- ) -> UIModalPresentationStyle {
- if controller.presentedViewController is RootContainerViewController {
- return traitCollection.horizontalSizeClass == .regular ? .formSheet : .fullScreen
- } else {
- return .none
- }
- }
-
- func presentationController(
- _ presentationController: UIPresentationController,
- willPresentWithAdaptiveStyle style: UIModalPresentationStyle,
- transitionCoordinator: UIViewControllerTransitionCoordinator?
- ) {
- let actualStyle: UIModalPresentationStyle
-
- // When adaptive presentation is not changing, the `style` is set to `.none`
- if case .none = style {
- actualStyle = presentationController.presentedViewController.modalPresentationStyle
- } else {
- actualStyle = style
- }
-
- // Force hide header bar in .formSheet presentation and show it in .fullScreen presentation
- if let wrapper = presentationController
- .presentedViewController as? RootContainerViewController
- {
- wrapper.setOverrideHeaderBarHidden(actualStyle == .formSheet, animated: false)
- }
-
- guard actualStyle == .formSheet else {
- // Move the settings button back into header bar
- rootContainer.removeSettingsButtonFromPresentationContainer()
-
- return
- }
-
- // Add settings button into the modal container to make it accessible by user
- if let transitionCoordinator = transitionCoordinator {
- transitionCoordinator.animate { context in
- self.rootContainer.addSettingsButtonToPresentationContainer(context.containerView)
- }
- } else if let containerView = presentationController.containerView {
- rootContainer.addSettingsButtonToPresentationContainer(containerView)
- } else {
- logger.warning(
- """
- Cannot obtain the containerView for presentation controller when presenting with \
- adaptive style \(actualStyle.rawValue) and missing transition coordinator.
- """
- )
- }
- }
-
// MARK: - TunnelObserver
func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) {