diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index faa02954e7ec..10e3e3ab8ae2 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -115,7 +115,6 @@ 5838322B2AC3EF9600EA2071 /* EventChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* EventChannel.swift */; }; 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; - 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */; }; 583FE02429C1ACB3006E85F9 /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; }; 584023222A406BF5007B27AC /* UDPOverTCPObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */; }; 584023292A407F5F007B27AC /* libtunnel_obfuscator_proxy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 584023282A407F5F007B27AC /* libtunnel_obfuscator_proxy.a */; }; @@ -138,8 +137,6 @@ 58607A4D2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */; }; 586168692976F6BD00EF8598 /* DisplayError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 586168682976F6BD00EF8598 /* DisplayError.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; - 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */; }; - 5864859B29A0EAF2006C5743 /* SecondaryContextPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */; }; 5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */; }; 5864AF0829C78849005B0CD9 /* CellFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */; }; 5864AF0929C78850005B0CD9 /* VPNSettingsCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864AF0229C7879B005B0CD9 /* VPNSettingsCellFactory.swift */; }; @@ -605,6 +602,7 @@ 7ADCB2D82B6A6EB300C88F89 /* AnyRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */; }; 7ADCB2DA2B6A730400C88F89 /* IPOverrideRepositoryStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */; }; 7AE044BB2A935726003915D8 /* Routing.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A88DCD02A8FABBE00D2FF0E /* Routing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */; }; 7AED35CC2BD13F60002A67D1 /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 7AED35CD2BD13FC4002A67D1 /* ApplicationTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C76A072A33850E00100D75 /* ApplicationTarget.swift */; }; 7AEF7F1A2AD00F52006FE45D /* AppMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */; }; @@ -1552,7 +1550,6 @@ 583E1E292848DF67004838B3 /* OperationObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationObserverTests.swift; sourceTree = ""; }; 583E60952A9F6D0800DC61EF /* ConfigurationBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationBuilder.swift; sourceTree = ""; }; 583FE00B29C0C7FD006E85F9 /* ModalPresentationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPresentationConfiguration.swift; sourceTree = ""; }; - 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSplitViewController.swift; sourceTree = ""; }; 583FE01129C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationControllerDismissalInterceptor.swift; sourceTree = ""; }; 5840231F2A406BF5007B27AC /* TunnelObfuscation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TunnelObfuscation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 584023212A406BF5007B27AC /* UDPOverTCPObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscator.swift; sourceTree = ""; }; @@ -1585,8 +1582,6 @@ 58607A4C2947287800BC467D /* AccountExpiryInAppNotificationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryInAppNotificationProvider.swift; sourceTree = ""; }; 586168682976F6BD00EF8598 /* DisplayError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayError.swift; sourceTree = ""; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = ""; }; - 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; - 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryContextPresentationController.swift; sourceTree = ""; }; 5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCellFactory.swift; sourceTree = ""; }; 5864AF0129C7879B005B0CD9 /* CellFactoryProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactoryProtocol.swift; sourceTree = ""; }; 5864AF0229C7879B005B0CD9 /* VPNSettingsCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNSettingsCellFactory.swift; sourceTree = ""; }; @@ -1976,6 +1971,7 @@ 7AD0AA202AD6CB0000119E10 /* URLRequestProxyStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestProxyStub.swift; sourceTree = ""; }; 7ADCB2D72B6A6EB300C88F89 /* AnyRelay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRelay.swift; sourceTree = ""; }; 7ADCB2D92B6A730400C88F89 /* IPOverrideRepositoryStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPOverrideRepositoryStub.swift; sourceTree = ""; }; + 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormsheetPresentationController.swift; sourceTree = ""; }; 7AEF7F192AD00F52006FE45D /* AppMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageHandler.swift; sourceTree = ""; }; 7AF10EB12ADE859200C090B9 /* AlertViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertViewController.swift; sourceTree = ""; }; 7AF10EB32ADE85BC00C090B9 /* RelayFilterCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayFilterCoordinator.swift; sourceTree = ""; }; @@ -3131,15 +3127,6 @@ path = RelayCacheTracker; sourceTree = ""; }; - 5864859729A0D012006C5743 /* Presentation controllers */ = { - isa = PBXGroup; - children = ( - 5864859829A0D028006C5743 /* FormsheetPresentationController.swift */, - 5864859A29A0EAF2006C5743 /* SecondaryContextPresentationController.swift */, - ); - path = "Presentation controllers"; - sourceTree = ""; - }; 5864AF0629C78816005B0CD9 /* Protocols */ = { isa = PBXGroup; children = ( @@ -3402,7 +3389,6 @@ children = ( 58D1560C29C0B27600749324 /* Root */, 583FE01329C102EB006E85F9 /* Navigation */, - 583FE00F29C0F532006E85F9 /* CustomSplitViewController.swift */, ); path = Containers; sourceTree = ""; @@ -3574,7 +3560,7 @@ F09D04B82AE94F27003D4F89 /* GeneralAPIs */, 58B26E1F2943516500D5980C /* Notifications */, 586A950B2901250A007BAF2B /* Operations */, - 5864859729A0D012006C5743 /* Presentation controllers */, + 7AE241492C20682B0076CE33 /* Presentation controllers */, 5864AF0629C78816005B0CD9 /* Protocols */, 585DA87526B0249A00B8C587 /* RelayCacheTracker */, 58E25F802837BBBB002CFB2C /* SceneDelegate.swift */, @@ -3934,6 +3920,14 @@ path = SelectLocation; sourceTree = ""; }; + 7AE241492C20682B0076CE33 /* Presentation controllers */ = { + isa = PBXGroup; + children = ( + 7AE241482C20682B0076CE33 /* FormsheetPresentationController.swift */, + ); + path = "Presentation controllers"; + sourceTree = ""; + }; 7AF9BE912A39F47D00DBFEDB /* RelayFilter */ = { isa = PBXGroup; children = ( @@ -5667,7 +5661,6 @@ 5891BF5125E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift in Sources */, 58E511E628DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 58C76A0B2A338E4300100D75 /* BackgroundTask.swift in Sources */, - 5864859B29A0EAF2006C5743 /* SecondaryContextPresentationController.swift in Sources */, 7A9CCCC32A96302800DD6A34 /* ApplicationCoordinator.swift in Sources */, 5864AF0729C78843005B0CD9 /* SettingsCellFactory.swift in Sources */, 587B75412668FD7800DEF7E9 /* AccountExpirySystemNotificationProvider.swift in Sources */, @@ -5796,7 +5789,6 @@ 586A950E290125F3007BAF2B /* ProductsRequestOperation.swift in Sources */, 7AF9BE902A39F26000DBFEDB /* Collection+Sorting.swift in Sources */, 58F19E35228C15BA00C7710B /* SpinnerActivityIndicatorView.swift in Sources */, - 5864859929A0D028006C5743 /* FormsheetPresentationController.swift in Sources */, 58CEB3022AFD365600E6E088 /* SwitchCellContentConfiguration.swift in Sources */, 7A9CCCB52A96302800DD6A34 /* AddCreditSucceededCoordinator.swift in Sources */, 7A0C0F632A979C4A0058EFCE /* Coordinator+Router.swift in Sources */, @@ -5876,6 +5868,7 @@ 5807E2C02432038B00F5FF30 /* String+Split.swift in Sources */, 58B26E242943520C00D5980C /* NotificationProviderProtocol.swift in Sources */, 5877F94E2A0A59AA0052D9E9 /* NotificationResponse.swift in Sources */, + 7AE2414A2C20682B0076CE33 /* FormsheetPresentationController.swift in Sources */, 7A6389E52B7E4247008E77E1 /* EditCustomListCoordinator.swift in Sources */, 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58EF875D2B1638BF00C098B2 /* ProxyConfigurationTesterProtocol.swift in Sources */, @@ -5885,7 +5878,6 @@ 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, 7A28826D2BAAC9DE00FD9F20 /* IPOverrideHeaderView.swift in Sources */, A98502032B627B120061901E /* LocalNetworkProbe.swift in Sources */, - 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */, 7A6F2FA92AFD0842006D0856 /* CustomDNSDataSource.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift index 13c087b9e58d..108448c424d5 100644 --- a/ios/MullvadVPN/Classes/AppRoutes.swift +++ b/ios/MullvadVPN/Classes/AppRoutes.swift @@ -10,8 +10,7 @@ import Routing import UIKit /** - Enum type describing groups of routes. Each group is a modal layer with horizontal navigation - inside with exception where primary navigation is a part of root controller on iPhone. + Enum type describing groups of routes. */ enum AppRouteGroup: AppRouteGroupProtocol { /** @@ -47,7 +46,7 @@ enum AppRouteGroup: AppRouteGroupProtocol { var isModal: Bool { switch self { case .primary: - return UIDevice.current.userInterfaceIdiom == .pad + return false case .selectLocation, .account, .settings, .changelog, .alert: return true diff --git a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift index 7a32ccf02230..00defec889b3 100644 --- a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift +++ b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift @@ -15,9 +15,6 @@ class AutomaticKeyboardResponder { private var lastKeyboardRect: CGRect? - private let logger = Logger(label: "AutomaticKeyboardResponder") - private var presentationFrameObserver: NSKeyValueObservation? - init(targetView: T, handler: @escaping (T, CGFloat) -> Void) { self.targetView = targetView self.handler = { view, adjustment in @@ -32,18 +29,6 @@ class AutomaticKeyboardResponder { name: UIResponder.keyboardWillChangeFrameNotification, object: nil ) - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil - ) - NotificationCenter.default.addObserver( - self, - selector: #selector(keyboardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil - ) } func updateContentInsets() { @@ -53,14 +38,6 @@ class AutomaticKeyboardResponder { // MARK: - Keyboard notifications - @objc private func keyboardWillShow(_ notification: Notification) { - addPresentationControllerObserver() - } - - @objc private func keyboardWillHide(_ notification: Notification) { - presentationFrameObserver = nil - } - @objc private func keyboardWillChangeFrame(_ notification: Notification) { handleKeyboardNotification(notification) } @@ -99,75 +76,6 @@ class AutomaticKeyboardResponder { } } - private func addPresentationControllerObserver() { - guard isFormSheetPresentation else { return } - - // Presentation controller follows the keyboard on iPad. - // Install the observer to listen for the container view frame and adjust the target view - // accordingly. - guard let containerView = presentationContainerView else { - logger.warning("Cannot determine the container view in form sheet presentation.") - return - } - - presentationFrameObserver = containerView.observe( - \.frame, - options: [.new], - changeHandler: { [weak self] _, _ in - guard let self, - let keyboardFrameValue = lastKeyboardRect else { return } - - adjustContentInsets(convertedKeyboardFrameEnd: keyboardFrameValue) - } - ) - } - - /// Returns the first parent controller in the responder chain - private var parentViewController: UIViewController? { - var responder: UIResponder? = targetView - let iterator = AnyIterator { () -> UIResponder? in - responder = responder?.next - return responder - } - return iterator.first { $0 is UIViewController } as? UIViewController - } - - /// Returns the presentation container view that's moved along with the keyboard on iPad - private var presentationContainerView: UIView? { - var currentView = parentViewController?.view - let iterator = AnyIterator { () -> UIView? in - currentView = currentView?.superview - return currentView - } - - // Find the container view that private `_UIFormSheetPresentationController` moves - // along with the keyboard. - return iterator.first { view -> Bool in - view.description.starts(with: " [UIView] in - [view] + view.subviews - } - - return subviews.first { view -> Bool in - view.description.hasPrefix(" UITraitCollection? { - guard let traitCollection = super.overrideTraitCollection(forChild: childViewController) - else { return nil } - - // Pass the split controller's horizontal size class to the primary controller when split - // view is expanded. - if !isCollapsed, childViewController == viewControllers.last { - let sizeOverrideTraitCollection = UITraitCollection( - horizontalSizeClass: self.traitCollection.horizontalSizeClass - ) - - return UITraitCollection(traitsFrom: [traitCollection, sizeOverrideTraitCollection]) - } else { - return traitCollection - } - } -} diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index 80352cdda2c8..0201cfd29f05 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -26,44 +26,17 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private(set) var router: ApplicationRouter! /** - Primary navigation container. + Navigation container. - On iPhone, it is used as a container for horizontal flows (TOS, Login, Revoked, Out-of-time). - - On iPad, it is used as a container to hold split view controller. Secondary navigation - container presented modally is used for horizontal flows. + Used as a container for horizontal flows (TOS, Login, Revoked, Out-of-time). */ - private let primaryNavigationContainer = RootContainerViewController() - - /** - Secondary navigation container. - - On iPad, it is used in place of primary container for horizontal flows and displayed modally - above primary container. Unused on iPhone. - */ - private let secondaryNavigationContainer = RootContainerViewController() + private let navigationContainer = RootContainerViewController() /// Posts `preferredAccountNumber` notification when user inputs the account number instead of voucher code private let preferredAccountNumberSubject = PassthroughSubject() - private lazy var secondaryRootConfiguration = ModalPresentationConfiguration( - preferredContentSize: UIMetrics.preferredFormSheetContentSize, - modalPresentationStyle: .custom, - isModalInPresentation: true, - transitioningDelegate: SecondaryContextTransitioningDelegate(adjustViewWhenKeyboardAppears: false) - ) - private let notificationController = NotificationController() - private let splitViewController: CustomSplitViewController = { - let svc = CustomSplitViewController() - svc.minimumPrimaryColumnWidth = UIMetrics.minimumSplitViewSidebarWidth - svc.preferredPrimaryColumnWidthFraction = UIMetrics.maximumSplitViewSidebarWidthFraction - svc.dividerColor = UIColor.MainSplitView.dividerColor - svc.primaryEdge = .trailing - return svc - }() - private var splitTunnelCoordinator: TunnelCoordinator? private var splitLocationCoordinator: LocationCoordinator? @@ -84,7 +57,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private var outOfTimeTimer: Timer? var rootViewController: UIViewController { - primaryNavigationContainer + navigationContainer } init( @@ -115,8 +88,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo super.init() - primaryNavigationContainer.delegate = self - secondaryNavigationContainer.delegate = self + navigationContainer.delegate = self router = ApplicationRouter(self) @@ -126,11 +98,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } func start() { - if isPad { - setupSplitView() - } - - setNotificationControllerParent(isPrimary: true) + navigationContainer.notificationController = notificationController continueFlow(animated: false) } @@ -189,7 +157,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo if context.isClosing { switch dismissedRoute.route.routeGroup { case .primary: - endHorizontalFlow(animated: context.isAnimated, completion: completion) + completion() context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() } case .selectLocation, .account, .settings, .changelog, .alert: @@ -292,19 +260,6 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func continueFlow(animated: Bool) { var nextRoutes = evaluateNextRoutes() - if isPad { - /* - On iPad the main route is always visible as it's a part of root controller hence we never - ask router to navigate to it. Instead this is when we hide the primary horizontal - navigation. - */ - if nextRoutes.contains(.main) { - router.dismissAll(.primary, animated: animated) - } - - nextRoutes.removeAll { $0 == .main } - } - for nextRoute in nextRoutes { router.present(nextRoute, animated: animated) } @@ -354,165 +309,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } private func didDismissAccount(_ reason: AccountDismissReason) { - if isPad { - router.dismiss(.account, animated: true) - if reason == .userLoggedOut { - router.dismissAll(.primary, animated: true) - continueFlow(animated: true) - } - } else { - if reason == .userLoggedOut { - router.dismissAll(.primary, animated: false) - continueFlow(animated: false) - } - router.dismiss(.account, animated: true) - } - } - - /** - Navigation controller used for horizontal flows. - */ - private var horizontalFlowController: RootContainerViewController { - if isPad { - return secondaryNavigationContainer - } else { - return primaryNavigationContainer - } - } - - /** - Begins horizontal flow presenting a navigation controller suitable for current user interface - idiom. - - On iPad this takes care of presenting a secondary navigation context using modal presentation. - - On iPhone this does nothing. - */ - private func beginHorizontalFlow(animated: Bool, completion: @escaping () -> Void) { - if isPad, secondaryNavigationContainer.presentingViewController == nil { - secondaryRootConfiguration.apply(to: secondaryNavigationContainer) - addSecondaryContextPresentationStyleObserver() - - primaryNavigationContainer.present( - secondaryNavigationContainer, - animated: animated, - completion: completion - ) - } else { - completion() - } - } - - /** - Marks the end of horizontal flow. - - On iPad this method dismisses the modally presented secondary navigation container and - releases all child view controllers from it. - - Does nothing on iPhone. - */ - private func endHorizontalFlow(animated: Bool = true, completion: (() -> Void)? = nil) { - if isPad { - 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 + if reason == .userLoggedOut { + router.dismissAll(.primary, animated: false) + continueFlow(animated: false) } - } - - /** - 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 { - UIDevice.current.userInterfaceIdiom == .pad - } - - private func setupSplitView() { - let tunnelCoordinator = makeTunnelCoordinator() - let locationCoordinator = makeLocationCoordinator(forModalPresentation: false) - - addChild(tunnelCoordinator) - addChild(locationCoordinator) - - splitTunnelCoordinator = tunnelCoordinator - splitLocationCoordinator = locationCoordinator - - splitViewController.delegate = self - splitViewController.viewControllers = [ - locationCoordinator.navigationController, - tunnelCoordinator.rootViewController, - ] - - primaryNavigationContainer.setViewControllers([splitViewController], animated: false) - - primaryNavigationContainer.notificationViewLayoutGuide = tunnelCoordinator.rootViewController.view - .safeAreaLayoutGuide - - tunnelCoordinator.start() - locationCoordinator.start() + router.dismiss(.account, animated: true) } private func presentTOS(animated: Bool, completion: @escaping (Coordinator) -> Void) { - let coordinator = TermsOfServiceCoordinator(navigationController: horizontalFlowController) + let coordinator = TermsOfServiceCoordinator(navigationController: navigationContainer) coordinator.didFinish = { [weak self] _ in self?.appPreferences.isAgreedToTermsOfService = true @@ -522,9 +327,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(coordinator) coordinator.start() - beginHorizontalFlow(animated: animated) { - completion(coordinator) - } + completion(coordinator) } private func presentChangeLog(animated: Bool, completion: @escaping (Coordinator) -> Void) { @@ -543,10 +346,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } private func presentMain(animated: Bool, completion: @escaping (Coordinator) -> Void) { - precondition(!isPad) let tunnelCoordinator = makeTunnelCoordinator() - horizontalFlowController.pushViewController( + navigationContainer.pushViewController( tunnelCoordinator.rootViewController, animated: animated ) @@ -554,14 +356,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(tunnelCoordinator) tunnelCoordinator.start() - beginHorizontalFlow(animated: animated) { - completion(tunnelCoordinator) - } + completion(tunnelCoordinator) } private func presentRevoked(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = RevokedCoordinator( - navigationController: horizontalFlowController, + navigationController: navigationContainer, tunnelManager: tunnelManager ) @@ -572,14 +372,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(coordinator) coordinator.start(animated: animated) - beginHorizontalFlow(animated: animated) { - completion(coordinator) - } + completion(coordinator) } private func presentOutOfTime(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = OutOfTimeCoordinator( - navigationController: horizontalFlowController, + navigationController: navigationContainer, storePaymentManager: storePaymentManager, tunnelManager: tunnelManager ) @@ -596,14 +394,12 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(coordinator) coordinator.start(animated: animated) - beginHorizontalFlow(animated: animated) { - completion(coordinator) - } + completion(coordinator) } private func presentWelcome(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = WelcomeCoordinator( - navigationController: horizontalFlowController, + navigationController: navigationContainer, storePaymentManager: storePaymentManager, tunnelManager: tunnelManager, accountsProxy: accountsProxy @@ -624,9 +420,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(coordinator) coordinator.start(animated: animated) - beginHorizontalFlow(animated: animated) { - completion(coordinator) - } + completion(coordinator) } private func shouldDismissOutOfTime() -> Bool { @@ -644,7 +438,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentLogin(animated: Bool, completion: @escaping (Coordinator) -> Void) { let coordinator = LoginCoordinator( - navigationController: horizontalFlowController, + navigationController: navigationContainer, tunnelManager: tunnelManager, devicesProxy: devicesProxy ) @@ -661,9 +455,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo addChild(coordinator) coordinator.start(animated: animated) - beginHorizontalFlow(animated: animated) { - completion(coordinator) - } + completion(coordinator) } private func presentAlert( @@ -743,11 +535,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo presentChild( coordinator, - animated: animated, - configuration: ModalPresentationConfiguration( - preferredContentSize: UIMetrics.preferredFormSheetContentSize, - modalPresentationStyle: .formSheet - ) + animated: animated ) { [weak self] in completion(coordinator) @@ -795,11 +583,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo presentChild( coordinator, - animated: animated, - configuration: ModalPresentationConfiguration( - preferredContentSize: UIMetrics.preferredFormSheetContentSize, - modalPresentationStyle: .formSheet - ) + animated: animated ) { completion(coordinator) } @@ -824,12 +608,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo self.tunnelObserver = tunnelObserver updateDeviceInfo(deviceState: tunnelManager.deviceState) - - splitViewController.preferredDisplayMode = tunnelManager.deviceState.splitViewMode } private func deviceStateDidChange(_ deviceState: DeviceState, previousDeviceState: DeviceState) { - splitViewController.preferredDisplayMode = deviceState.splitViewMode updateDeviceInfo(deviceState: deviceState) switch deviceState { @@ -867,8 +648,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo isPresentingAccountExpiryBanner: isPresentingAccountExpiryBanner, deviceState: deviceState ) - self.primaryNavigationContainer.update(configuration: rootDeviceInfoViewModel.configuration) - self.secondaryNavigationContainer.update(configuration: rootDeviceInfoViewModel.configuration) + self.navigationContainer.update(configuration: rootDeviceInfoViewModel.configuration) } // MARK: - Out of time @@ -968,11 +748,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo func rootContainerViewSupportedInterfaceOrientations(_ controller: RootContainerViewController) -> UIInterfaceOrientationMask { - if isPad { - return [.landscape, .portrait] - } else { - return [.portrait] - } + return [.portrait] } func rootContainerViewAccessibilityPerformMagicTap(_ controller: RootContainerViewController) @@ -1019,7 +795,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo // MARK: - Presenting var presentationContext: UIViewController { - primaryNavigationContainer.presentedViewController ?? primaryNavigationContainer + navigationContainer.presentedViewController ?? navigationContainer } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift index 1f5fc64527b3..81d9e9fc02ae 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift @@ -47,9 +47,7 @@ struct ShadowsocksSectionHandler { contentConfiguration.inputFilter = .digitsOnly contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.shadowsocks.port) contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled() - if case .phone = cell.traitCollection.userInterfaceIdiom { - contentConfiguration.textFieldProperties.keyboardType = .numberPad - } + contentConfiguration.textFieldProperties.keyboardType = .numberPad cell.contentConfiguration = contentConfiguration } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift index e2dd45f9d6b9..c1bcf9ca52c7 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift @@ -50,9 +50,7 @@ struct SocksSectionHandler { contentConfiguration.inputFilter = .digitsOnly contentConfiguration.editingEvents.onChange = subject.bindTextAction(to: \.socks.port) contentConfiguration.textFieldProperties = .withSmartFeaturesDisabled() - if case .phone = cell.traitCollection.userInterfaceIdiom { - contentConfiguration.textFieldProperties.keyboardType = .numberPad - } + contentConfiguration.textFieldProperties.keyboardType = .numberPad cell.accessibilityIdentifier = .socks5PortCell cell.contentConfiguration = contentConfiguration } diff --git a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift b/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift deleted file mode 100644 index f21810407498..000000000000 --- a/ios/MullvadVPN/Presentation controllers/SecondaryContextPresentationController.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// SecondaryContextPresentationController.swift -// MullvadVPN -// -// Created by pronebird on 18/02/2023. -// Copyright © 2023 Mullvad VPN AB. All rights reserved. -// - -import UIKit - -/** - This is a presentation controller class used for presentation of secondary navigation context - in application coordinator. - */ -class SecondaryContextPresentationController: FormSheetPresentationController { - override func presentationTransitionWillBegin() { - super.presentationTransitionWillBegin() - - updateHeaderBarHidden() - - if let containerView, - let rootContainer = presentingViewController as? RootContainerViewController { - rootContainer.addTrailingButtonsToPresentationContainer(containerView) - } - } - - override func dismissalTransitionDidEnd(_ completed: Bool) { - super.dismissalTransitionDidEnd(completed) - - if let rootContainer = presentingViewController as? RootContainerViewController, completed { - rootContainer.removeTrailingButtonsFromPresentationContainer() - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - updateHeaderBarHidden() - } - - private func updateHeaderBarHidden() { - let presentedController = presentedViewController as? RootContainerViewController - - presentedController?.setOverrideHeaderBarHidden( - isInFullScreenPresentation ? nil : true, - animated: false - ) - } -} - -class SecondaryContextTransitioningDelegate: FormSheetTransitioningDelegate { - convenience init(adjustViewWhenKeyboardAppears: Bool) { - let option = FormSheetPresentationOptions( - useFullScreenPresentationInCompactWidth: true, - adjustViewWhenKeyboardAppears: adjustViewWhenKeyboardAppears - ) - self.init(options: option) - } - - private override init(options: FormSheetPresentationOptions) { - super.init(options: options) - } - - override func presentationController( - forPresented presented: UIViewController, - presenting: UIViewController?, - source: UIViewController - ) -> UIPresentationController? { - let presentationController = SecondaryContextPresentationController( - presentedViewController: presented, - presenting: source, - options: options - ) - - return presentationController - } -} diff --git a/ios/MullvadVPN/UI appearance/UIMetrics.swift b/ios/MullvadVPN/UI appearance/UIMetrics.swift index c3fbe48cffd2..aa6502d1263a 100644 --- a/ios/MullvadVPN/UI appearance/UIMetrics.swift +++ b/ios/MullvadVPN/UI appearance/UIMetrics.swift @@ -104,8 +104,7 @@ enum UIMetrics { } enum DisconnectSplitButton { - static let secondaryButtonPhone = CGSize(width: 42, height: 42) - static let secondaryButtonPad = CGSize(width: 52, height: 52) + static let secondaryButton = CGSize(width: 42, height: 42) } enum FilterView { diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift index 64d4ad01dcb8..bf6c2d140105 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift @@ -178,17 +178,6 @@ class LoginViewController: UIViewController, RootContainment { ) } - override var disablesAutomaticKeyboardDismissal: Bool { - // Allow dismissing the keyboard in .formSheet presentation style - false - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - if traitCollection.userInterfaceIdiom != previousTraitCollection?.userInterfaceIdiom { - updateCreateButtonEnabled() - } - } - // MARK: - Public func start(action: LoginAction) { @@ -320,14 +309,7 @@ class LoginViewController: UIViewController, RootContainment { switch loginState { case .failure, .default: - // Disable "Create account" button on iPad as user types in the account token, - // however leave it enabled on iPhone to avoid confusion to why it's being disabled - // since it's likely overlaid by keyboard. - if case .pad = traitCollection.userInterfaceIdiom { - isEnabled = contentView.accountInputGroup.textField.text?.isEmpty ?? true - } else { - isEnabled = true - } + isEnabled = true case .success, .authenticating: isEnabled = false diff --git a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift index 8baa2eaac71c..0fcdf7efd1b9 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/DisconnectSplitButton.swift @@ -11,14 +11,7 @@ import UIKit class DisconnectSplitButton: UIView { private var secondaryButtonSize: CGSize { - switch traitCollection.userInterfaceIdiom { - case .phone: - return UIMetrics.DisconnectSplitButton.secondaryButtonPhone - case .pad: - return UIMetrics.DisconnectSplitButton.secondaryButtonPad - default: - return .zero - } + UIMetrics.DisconnectSplitButton.secondaryButton } let primaryButton = AppButton(style: .translucentDangerSplitLeft) @@ -71,14 +64,6 @@ class DisconnectSplitButton: UIView { fatalError("init(coder:) has not been implemented") } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.userInterfaceIdiom != previousTraitCollection?.userInterfaceIdiom { - updateTraitConstraints() - } - } - private func updateTraitConstraints() { let newSize = secondaryButtonSize secondaryButtonWidthConstraint.constant = newSize.width diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift index 16fd69b25b3a..584a3c7ff3e1 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelControlView.swift @@ -129,21 +129,6 @@ final class TunnelControlView: UIView { fatalError("init(coder:) has not been implemented") } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.userInterfaceIdiom != previousTraitCollection?.userInterfaceIdiom { - updateTraitConstraints() - } - - if previousTraitCollection?.userInterfaceIdiom != traitCollection.userInterfaceIdiom || - previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass { - if let viewModel { - updateActionButtons(tunnelState: viewModel.tunnelStatus.state) - } - } - } - func update(with model: TunnelControlViewModel) { viewModel = model let tunnelState = model.tunnelStatus.state @@ -355,30 +340,9 @@ final class TunnelControlView: UIView { private func updateTraitConstraints() { var layoutConstraints = [NSLayoutConstraint]() - switch traitCollection.userInterfaceIdiom { - case .pad: - // Max container width is 70% width of iPad in portrait mode - let maxWidth = min( - UIScreen.main.nativeBounds.width * 0.7, - UIMetrics.maximumSplitViewContentContainerWidth - ) - - layoutConstraints.append(contentsOf: [ - containerView.trailingAnchor.constraint( - lessThanOrEqualTo: layoutMarginsGuide.trailingAnchor - ), - containerView.widthAnchor.constraint(equalToConstant: maxWidth) - .withPriority(.defaultHigh), - ]) - - case .phone: - layoutConstraints.append( - containerView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) - ) - - default: - break - } + layoutConstraints.append( + containerView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) + ) removeConstraints(traitConstraints) traitConstraints = layoutConstraints @@ -456,40 +420,19 @@ final class TunnelControlView: UIView { private extension TunnelState { func actionButtons(traitCollection: UITraitCollection) -> [TunnelControlActionButton] { - switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { - case (.phone, _), (.pad, .compact): - switch self { - case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): - [.selectLocation, .connect] - - case .connecting, .pendingReconnect, .disconnecting(.reconnect), - .waitingForConnectivity(.noConnection): - [.selectLocation, .cancel] - - case .negotiatingPostQuantumKey: - [.selectLocation, .cancel] + switch self { + case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): + [.selectLocation, .connect] - case .connected, .reconnecting, .error: - [.selectLocation, .disconnect] - } - - case (.pad, .regular): - switch self { - case .disconnected, .disconnecting(.nothing), .waitingForConnectivity(.noNetwork): - [.connect] + case .connecting, .pendingReconnect, .disconnecting(.reconnect), + .waitingForConnectivity(.noConnection): + [.selectLocation, .cancel] - case .connecting, .pendingReconnect, .disconnecting(.reconnect), - .waitingForConnectivity(.noConnection): - [.cancel] + case .negotiatingPostQuantumKey: + [.selectLocation, .cancel] - case .negotiatingPostQuantumKey: - [.cancel] - case .connected, .reconnecting, .error: - [.disconnect] - } - - default: - [] + case .connected, .reconnecting, .error: + [.selectLocation, .disconnect] } } diff --git a/ios/MullvadVPN/Views/AppButton.swift b/ios/MullvadVPN/Views/AppButton.swift index c2581d5c7180..3209828d127f 100644 --- a/ios/MullvadVPN/Views/AppButton.swift +++ b/ios/MullvadVPN/Views/AppButton.swift @@ -156,16 +156,4 @@ class AppButton: CustomButton { guard let directionalEdgeInsets = innerDirectionalContentEdgeInsets else { return } super.contentEdgeInsets = directionalEdgeInsets.toEdgeInsets(effectiveUserInterfaceLayoutDirection) } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if traitCollection.userInterfaceIdiom != previousTraitCollection?.userInterfaceIdiom { - if overrideContentEdgeInsets { - updateContentEdgeInsetsFromDirectional() - } else { - contentEdgeInsets = defaultContentInsets - } - } - } }