diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-08-17 14:08:18 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-08-17 14:24:50 +0200 |
| commit | 3548a2298721c63e4cf463a6da154d28971b26ee (patch) | |
| tree | d8af625539ef4c2863993c6587d31c7d3ebf3e3d /ios | |
| parent | ff9eb384a5d32516b7f17d625ceab29fd34c3271 (diff) | |
| download | mullvadvpn-3548a2298721c63e4cf463a6da154d28971b26ee.tar.xz mullvadvpn-3548a2298721c63e4cf463a6da154d28971b26ee.zip | |
Extract router and break it down on smaller files
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 28 | ||||
| -rw-r--r-- | ios/MullvadVPN/Routing/AppRouteProtocol.swift | 60 | ||||
| -rw-r--r-- | ios/MullvadVPN/Routing/AppRoutes.swift | 121 | ||||
| -rw-r--r-- | ios/MullvadVPN/Routing/ApplicationRouter.swift (renamed from ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift) | 358 | ||||
| -rw-r--r-- | ios/MullvadVPN/Routing/ApplicationRouterDelegate.swift | 62 | ||||
| -rw-r--r-- | ios/MullvadVPN/Routing/ApplicationRouterTypes.swift | 151 |
6 files changed, 419 insertions, 361 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 33ac96d9fd..c8b8755373 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -53,6 +53,10 @@ 068CE5782927BE4800A068BB /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068CE5732927B7A400A068BB /* Migration.swift */; }; 0697D6E728F01513007A9E99 /* TransportMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0697D6E628F01513007A9E99 /* TransportMonitor.swift */; }; 06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; + 5802EBC52A8E44AC00E5CE4C /* AppRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */; }; + 5802EBC72A8E457A00E5CE4C /* AppRouteProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */; }; + 5802EBC92A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */; }; + 5802EBCB2A8E45DC00E5CE4C /* ApplicationRouterTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5802EBCA2A8E45DC00E5CE4C /* ApplicationRouterTypes.swift */; }; 5803B4B02940A47300C23744 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */; }; 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5803B4B12940A48700C23744 /* TunnelStore.swift */; }; 5806767C27048E9B00C858CB /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CE5E7B224146470008646E /* PacketTunnelProvider.swift */; }; @@ -933,6 +937,10 @@ 06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; }; 06FAE67C28F83CA50033DD93 /* URLSessionTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTransport.swift; sourceTree = "<group>"; }; 06FAE67D28F83CA50033DD93 /* RESTTransport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTransport.swift; sourceTree = "<group>"; }; + 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; }; + 5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteProtocol.swift; sourceTree = "<group>"; }; + 5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterDelegate.swift; sourceTree = "<group>"; }; + 5802EBCA2A8E45DC00E5CE4C /* ApplicationRouterTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationRouterTypes.swift; sourceTree = "<group>"; }; 5803B4AF2940A47300C23744 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; }; 5803B4B12940A48700C23744 /* TunnelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStore.swift; sourceTree = "<group>"; }; 58059DDD28468158002B1049 /* OutputOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputOperation.swift; sourceTree = "<group>"; }; @@ -1545,6 +1553,18 @@ path = MullvadREST; sourceTree = "<group>"; }; + 5802EBC32A8E447000E5CE4C /* Routing */ = { + isa = PBXGroup; + children = ( + 5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */, + 5802EBC82A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift */, + 5802EBCA2A8E45DC00E5CE4C /* ApplicationRouterTypes.swift */, + 5802EBC62A8E457A00E5CE4C /* AppRouteProtocol.swift */, + 5802EBC42A8E44AC00E5CE4C /* AppRoutes.swift */, + ); + path = Routing; + sourceTree = "<group>"; + }; 580F8B88281A79A7002E0998 /* SettingsManager */ = { isa = PBXGroup; children = ( @@ -1649,7 +1669,6 @@ children = ( 583FE02029C1A0B1006E85F9 /* Account */, F0E8E4BF2A602C7D00ED26A3 /* AccountDeletion */, - 5878F4FA29CDA2D4003D4BE2 /* ChangeLog */, F0E8E4B92A55593300ED26A3 /* CreationAccount */, 583FE01D29C197C1006E85F9 /* DeviceList */, 583FE02529C1AD0E006E85F9 /* Launch */, @@ -1690,7 +1709,6 @@ 7A83C4012A57FAA800DFB83A /* SettingsDNSInfoCell.swift */, 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */, 7A7AD28E29DEDB1C00480EF1 /* SettingsHeaderView.swift */, - 7A42DEC82A05164100B209BE /* SettingsInputCell.swift */, 58677711290976FB006F721F /* SettingsInteractor.swift */, 5867770F290975E8006F721F /* SettingsInteractorFactory.swift */, 58ACF64A26553C3F00ACE4B7 /* SettingsSwitchCell.swift */, @@ -2028,7 +2046,6 @@ F0E8E4C62A604CBE00ED26A3 /* AccountDeletionCoordinator.swift */, F07C0A042A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift */, 58BBB39629717E0C00C8DB7C /* ApplicationCoordinator.swift */, - 5893C6FB29C311E9009090D1 /* ApplicationRouter.swift */, 5878F50129CDB989003D4BE2 /* ChangeLogCoordinator.swift */, 58CAF9F92983E0C600BE19F7 /* LoginCoordinator.swift */, 583FE00D29C0D586006E85F9 /* OutOfTimeCoordinator.swift */, @@ -2258,6 +2275,7 @@ 58C76A0A2A338E4300100D75 /* BackgroundTask.swift */, 583FE02829C1B079006E85F9 /* Classes */, 58C774C929AB543C003A1A56 /* Containers */, + 5802EBC32A8E447000E5CE4C /* Routing */, 58CAF9F22983D32200BE19F7 /* Coordinators */, 583FE02329C1AC9F006E85F9 /* Extensions */, A9D96B182A82479700A5C673 /* MigrationManager */, @@ -3472,6 +3490,7 @@ 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */, 068CE5742927B7A400A068BB /* Migration.swift in Sources */, A92ECC282A7802AB0052F1B1 /* StoredDeviceData.swift in Sources */, + 5802EBC92A8E45BA00E5CE4C /* ApplicationRouterDelegate.swift in Sources */, 58CCA010224249A1004F3011 /* TunnelViewController.swift in Sources */, 58B26E22294351EA00D5980C /* InAppNotificationProvider.swift in Sources */, 5893716A28817A45004EE76C /* DeviceManagementViewController.swift in Sources */, @@ -3535,6 +3554,7 @@ 5871FBA0254C26C00051A0A4 /* NSRegularExpression+IPAddress.swift in Sources */, F0E8E4BB2A56C9F100ED26A3 /* WelcomeInteractor.swift in Sources */, 5878A27729093A4F0096FC88 /* StorePaymentBlockObserver.swift in Sources */, + 5802EBC52A8E44AC00E5CE4C /* AppRoutes.swift in Sources */, F07C0A052A52D4C3009825CA /* AccountRedeemingVoucherCoordinator.swift in Sources */, 5868585524054096000B8131 /* AppButton.swift in Sources */, 5893C6FC29C311E9009090D1 /* ApplicationRouter.swift in Sources */, @@ -3559,7 +3579,9 @@ 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 58CAF9F82983D36800BE19F7 /* Coordinator.swift in Sources */, 5819C2172729595500D6EC38 /* SettingsAddDNSEntryCell.swift in Sources */, + 5802EBC72A8E457A00E5CE4C /* AppRouteProtocol.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, + 5802EBCB2A8E45DC00E5CE4C /* ApplicationRouterTypes.swift in Sources */, 587EB66A270EFACB00123C75 /* CharacterSet+IPAddress.swift in Sources */, 5888AD83227B11080051EB06 /* SelectLocationCell.swift in Sources */, 5891BF1C25E3E3EB006D6FB0 /* Bundle+ProductVersion.swift in Sources */, diff --git a/ios/MullvadVPN/Routing/AppRouteProtocol.swift b/ios/MullvadVPN/Routing/AppRouteProtocol.swift new file mode 100644 index 0000000000..9411f65e14 --- /dev/null +++ b/ios/MullvadVPN/Routing/AppRouteProtocol.swift @@ -0,0 +1,60 @@ +// +// AppRouteProtocol.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/** + Formal protocol describing a group of routes. + */ +protocol AppRouteGroupProtocol: Comparable, Equatable, Hashable { + /** + Returns `true` if group is presented modally, otherwise `false` if group is a part of root view + controller. + */ + var isModal: Bool { get } + + /** + Defines a modal level that's used for restricting modal presentation. + + A group with higher modal level can be presented above a group with lower level but not the other way around. For example, if a modal group is represented by + `UIAlertController`, it should have the highest level (i.e `Int.max`) to prevent other modals from being presented above it, however it enables alert + controller to be presented above any other modal. + */ + var modalLevel: Int { get } +} + +/** + Default implementation of `Comparable` for `AppRouteGroupProtocol` which compares `modalLevel` of both sides. + */ +extension AppRouteGroupProtocol { + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.modalLevel < rhs.modalLevel + } +} + +/** + Formal protocol describing a single route. + */ +protocol AppRouteProtocol: Equatable, Hashable { + associatedtype RouteGroupType: AppRouteGroupProtocol + + /** + Returns `true` when only one route of a kind can be displayed. + */ + var isExclusive: Bool { get } + + /** + Returns `true` if the route supports sub-navigation. + */ + var supportsSubNavigation: Bool { get } + + /** + Navigation group to which the route belongs to. + */ + var routeGroup: RouteGroupType { get } +} diff --git a/ios/MullvadVPN/Routing/AppRoutes.swift b/ios/MullvadVPN/Routing/AppRoutes.swift new file mode 100644 index 0000000000..4fda9aa253 --- /dev/null +++ b/ios/MullvadVPN/Routing/AppRoutes.swift @@ -0,0 +1,121 @@ +// +// AppRoutes.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +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 AppRouteGroup: AppRouteGroupProtocol { + /** + Primary horizontal navigation group. + */ + case primary + + /** + Select location group. + */ + case selectLocation + + /** + Account group. + */ + case account + + /** + Settings group. + */ + case settings + + /** + Changelog group. + */ + case changelog + + var isModal: Bool { + switch self { + case .primary: + return UIDevice.current.userInterfaceIdiom == .pad + + case .selectLocation, .account, .settings, .changelog: + return true + } + } + + var modalLevel: Int { + switch self { + case .primary: + return 0 + case .settings, .account, .selectLocation, .changelog: + return 1 + } + } +} + +/** + Enum type describing primary application routes. + */ +enum AppRoute: AppRouteProtocol { + /** + Account route. + */ + case account + + /** + Settings route. Contains sub-route to display. + */ + case settings(SettingsNavigationRoute?) + + /** + Select location route. + */ + case selectLocation + + /** + Changelog route. + */ + case changelog + + /** + Routes that are part of primary horizontal navigation group. + */ + case tos, login, main, revoked, outOfTime, welcome, setupAccountCompleted + + var isExclusive: Bool { + switch self { + case .selectLocation, .account, .settings, .changelog: + return true + default: + return false + } + } + + var supportsSubNavigation: Bool { + if case .settings = self { + return true + } else { + return false + } + } + + var routeGroup: AppRouteGroup { + switch self { + case .tos, .login, .main, .revoked, .outOfTime, .welcome, .setupAccountCompleted: + return .primary + case .changelog: + return .changelog + case .selectLocation: + return .selectLocation + case .account: + return .account + case .settings: + return .settings + } + } +} diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift b/ios/MullvadVPN/Routing/ApplicationRouter.swift index 2e77f3d590..b0580d19a6 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift +++ b/ios/MullvadVPN/Routing/ApplicationRouter.swift @@ -11,364 +11,6 @@ import MullvadLogging import UIKit /** - Formal protocol describing a group of routes. - */ -protocol AppRouteGroupProtocol: Comparable, Equatable, Hashable { - /** - Returns `true` if group is presented modally, otherwise `false` if group is a part of root view - controller. - */ - var isModal: Bool { get } - - /** - Defines a modal level that's used for restricting modal presentation. - - A group with higher modal level can be presented above a group with lower level but not the other way around. For example, if a modal group is represented by - `UIAlertController`, it should have the highest level (i.e `Int.max`) to prevent other modals from being presented above it, however it enables alert - controller to be presented above any other modal. - */ - var modalLevel: Int { get } -} - -/** - Default implementation of `Comparable` for `AppRouteGroupProtocol` which compares `modalLevel` of both sides. - */ -extension AppRouteGroupProtocol { - static func < (lhs: Self, rhs: Self) -> Bool { - lhs.modalLevel < rhs.modalLevel - } -} - -/** - Formal protocol describing a single route. - */ -protocol AppRouteProtocol: Equatable, Hashable { - associatedtype RouteGroupType: AppRouteGroupProtocol - - /** - Returns `true` when only one route of a kind can be displayed. - */ - var isExclusive: Bool { get } - - /** - Returns `true` if the route supports sub-navigation. - */ - var supportsSubNavigation: Bool { get } - - /** - Navigation group to which the route belongs to. - */ - var routeGroup: RouteGroupType { get } -} - -/** - 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 AppRouteGroup: AppRouteGroupProtocol { - /** - Primary horizontal navigation group. - */ - case primary - - /** - Select location group. - */ - case selectLocation - - /** - Account group. - */ - case account - - /** - Settings group. - */ - case settings - - /** - Changelog group. - */ - case changelog - - var isModal: Bool { - switch self { - case .primary: - return UIDevice.current.userInterfaceIdiom == .pad - - case .selectLocation, .account, .settings, .changelog: - return true - } - } - - var modalLevel: Int { - switch self { - case .primary: - return 0 - case .settings, .account, .selectLocation, .changelog: - return 1 - } - } -} - -/** - Enum type describing primary application routes. - */ -enum AppRoute: AppRouteProtocol { - /** - Account route. - */ - case account - - /** - Settings route. Contains sub-route to display. - */ - case settings(SettingsNavigationRoute?) - - /** - Select location route. - */ - case selectLocation - - /** - Changelog route. - */ - case changelog - - /** - Routes that are part of primary horizontal navigation group. - */ - case tos, login, main, revoked, outOfTime, welcome, setupAccountCompleted - - var isExclusive: Bool { - switch self { - case .selectLocation, .account, .settings, .changelog: - return true - default: - return false - } - } - - var supportsSubNavigation: Bool { - if case .settings = self { - return true - } else { - return false - } - } - - var routeGroup: AppRouteGroup { - switch self { - case .tos, .login, .main, .revoked, .outOfTime, .welcome, .setupAccountCompleted: - return .primary - case .changelog: - return .changelog - case .selectLocation: - return .selectLocation - case .account: - return .account - case .settings: - return .settings - } - } -} - -/** - Struct describing a routing request for presentation or dismissal. - */ -struct PendingRoute<RouteType: AppRouteProtocol>: Equatable { - var operation: RouteOperation<RouteType> - var animated: Bool -} - -/** - Enum type describing an attempt to fulfill the route presentation request. - **/ -enum PendingPresentationResult { - /** - Successfully presented the route. - */ - case success - - /** - The request to present this route should be dropped. - */ - case drop - - /** - The request to present this route cannot be fulfilled because the modal context does not allow - for that. - - For example, on iPad, primary context cannot be presented above settings, because it enables - access to settings by making the settings cog accessible via custom presentation controller. - In such case the router will attempt to fulfill other requests in hope that perhaps settings - can be dismissed first before getting back to that request. - */ - case blockedByModalContext -} - -/** - Enum type describing an attempt to fulfill the route dismissal request. - */ -enum PendingDismissalResult { - /** - Successfully dismissed the route. - */ - case success - - /** - The request to present this route should be dropped. - */ - case drop - - /** - The route cannot be dismissed immediately because it's blocked by another modal presented - above. - - The router will attempt to fulfill other requests first in hope to unblock the route by - dismissing the modal above before getting back to that request. - */ - case blockedByModalAbove -} - -/** - Enum describing operation over the route. - */ -enum RouteOperation<RouteType: AppRouteProtocol>: Equatable { - /** - Present route. - */ - case present(RouteType) - - /** - Dismiss route. - */ - case dismiss(DismissMatch<RouteType>) - - /** - Returns a group of affected routes. - */ - var routeGroup: RouteType.RouteGroupType { - switch self { - case let .present(route): - return route.routeGroup - case let .dismiss(dismissMatch): - return dismissMatch.routeGroup - } - } -} - -/** - Enum type describing a single route or a group of routes requested to be dismissed. - */ -enum DismissMatch<RouteType: AppRouteProtocol>: Equatable { - case group(RouteType.RouteGroupType) - case singleRoute(RouteType) - - /** - Returns a group of affected routes. - */ - var routeGroup: RouteType.RouteGroupType { - switch self { - case let .group(group): - return group - case let .singleRoute(route): - return route.routeGroup - } - } -} - -/** - Struct describing presented route. - */ -struct PresentedRoute<RouteType: AppRouteProtocol>: Equatable { - var route: RouteType - var coordinator: Coordinator -} - -/** - Struct holding information used by delegate to perform dismissal of the route(s) in subject. - */ -struct RouteDismissalContext<RouteType: AppRouteProtocol> { - /** - Specific routes that are being dismissed. - */ - var dismissedRoutes: [PresentedRoute<RouteType>] - - /** - Whether the entire group is being dismissed. - */ - var isClosing: Bool - - /** - Whether transition is animated. - */ - var isAnimated: Bool -} - -/** - Struct holding information used by delegate to perform sub-navigation of the route in subject. - */ -struct RouteSubnavigationContext<RouteType: AppRouteProtocol> { - var presentedRoute: PresentedRoute<RouteType> - var route: RouteType - var isAnimated: Bool -} - -/** - Application router delegate - */ -protocol ApplicationRouterDelegate<RouteType>: AnyObject { - associatedtype RouteType: AppRouteProtocol - - /** - Delegate should present the route and pass corresponding `Coordinator` upon completion. - */ - func applicationRouter( - _ router: ApplicationRouter<RouteType>, - route: RouteType, - animated: Bool, - completion: @escaping (Coordinator) -> Void - ) - - /** - Delegate should dismiss the route. - */ - func applicationRouter( - _ router: ApplicationRouter<RouteType>, - dismissWithContext context: RouteDismissalContext<RouteType>, - completion: @escaping () -> Void - ) - - /** - Delegate may reconsider if route presentation is still needed. - - Return `true` to proceed with presenation, otherwise `false` to prevent it. - */ - func applicationRouter(_ router: ApplicationRouter<RouteType>, shouldPresent route: RouteType) -> Bool - - /** - Delegate may reconsider if route dismissal should be done. - - Return `true` to proceed with dismissal, otherwise `false` to prevent it. - */ - func applicationRouter( - _ router: ApplicationRouter<RouteType>, - shouldDismissWithContext context: RouteDismissalContext<RouteType> - ) -> Bool - - /** - Delegate should handle sub-navigation for routes supporting it then call completion to tell - router when it's done. - */ - func applicationRouter( - _ router: ApplicationRouter<RouteType>, - handleSubNavigationWithContext context: RouteSubnavigationContext<RouteType>, - completion: @escaping () -> Void - ) -} - -/** Main application router. */ final class ApplicationRouter<RouteType: AppRouteProtocol> { diff --git a/ios/MullvadVPN/Routing/ApplicationRouterDelegate.swift b/ios/MullvadVPN/Routing/ApplicationRouterDelegate.swift new file mode 100644 index 0000000000..1daa35760e --- /dev/null +++ b/ios/MullvadVPN/Routing/ApplicationRouterDelegate.swift @@ -0,0 +1,62 @@ +// +// ApplicationRouterDelegate.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/** + Application router delegate + */ +protocol ApplicationRouterDelegate<RouteType>: AnyObject { + associatedtype RouteType: AppRouteProtocol + + /** + Delegate should present the route and pass corresponding `Coordinator` upon completion. + */ + func applicationRouter( + _ router: ApplicationRouter<RouteType>, + route: RouteType, + animated: Bool, + completion: @escaping (Coordinator) -> Void + ) + + /** + Delegate should dismiss the route. + */ + func applicationRouter( + _ router: ApplicationRouter<RouteType>, + dismissWithContext context: RouteDismissalContext<RouteType>, + completion: @escaping () -> Void + ) + + /** + Delegate may reconsider if route presentation is still needed. + + Return `true` to proceed with presenation, otherwise `false` to prevent it. + */ + func applicationRouter(_ router: ApplicationRouter<RouteType>, shouldPresent route: RouteType) -> Bool + + /** + Delegate may reconsider if route dismissal should be done. + + Return `true` to proceed with dismissal, otherwise `false` to prevent it. + */ + func applicationRouter( + _ router: ApplicationRouter<RouteType>, + shouldDismissWithContext context: RouteDismissalContext<RouteType> + ) -> Bool + + /** + Delegate should handle sub-navigation for routes supporting it then call completion to tell + router when it's done. + */ + func applicationRouter( + _ router: ApplicationRouter<RouteType>, + handleSubNavigationWithContext context: RouteSubnavigationContext<RouteType>, + completion: @escaping () -> Void + ) +} diff --git a/ios/MullvadVPN/Routing/ApplicationRouterTypes.swift b/ios/MullvadVPN/Routing/ApplicationRouterTypes.swift new file mode 100644 index 0000000000..5316e85fdd --- /dev/null +++ b/ios/MullvadVPN/Routing/ApplicationRouterTypes.swift @@ -0,0 +1,151 @@ +// +// ApplicationRouterTypes.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/** + Struct describing a routing request for presentation or dismissal. + */ +struct PendingRoute<RouteType: AppRouteProtocol>: Equatable { + var operation: RouteOperation<RouteType> + var animated: Bool +} + +/** + Enum type describing an attempt to fulfill the route presentation request. + **/ +enum PendingPresentationResult { + /** + Successfully presented the route. + */ + case success + + /** + The request to present this route should be dropped. + */ + case drop + + /** + The request to present this route cannot be fulfilled because the modal context does not allow + for that. + + For example, on iPad, primary context cannot be presented above settings, because it enables + access to settings by making the settings cog accessible via custom presentation controller. + In such case the router will attempt to fulfill other requests in hope that perhaps settings + can be dismissed first before getting back to that request. + */ + case blockedByModalContext +} + +/** + Enum type describing an attempt to fulfill the route dismissal request. + */ +enum PendingDismissalResult { + /** + Successfully dismissed the route. + */ + case success + + /** + The request to present this route should be dropped. + */ + case drop + + /** + The route cannot be dismissed immediately because it's blocked by another modal presented + above. + + The router will attempt to fulfill other requests first in hope to unblock the route by + dismissing the modal above before getting back to that request. + */ + case blockedByModalAbove +} + +/** + Enum describing operation over the route. + */ +enum RouteOperation<RouteType: AppRouteProtocol>: Equatable { + /** + Present route. + */ + case present(RouteType) + + /** + Dismiss route. + */ + case dismiss(DismissMatch<RouteType>) + + /** + Returns a group of affected routes. + */ + var routeGroup: RouteType.RouteGroupType { + switch self { + case let .present(route): + return route.routeGroup + case let .dismiss(dismissMatch): + return dismissMatch.routeGroup + } + } +} + +/** + Enum type describing a single route or a group of routes requested to be dismissed. + */ +enum DismissMatch<RouteType: AppRouteProtocol>: Equatable { + case group(RouteType.RouteGroupType) + case singleRoute(RouteType) + + /** + Returns a group of affected routes. + */ + var routeGroup: RouteType.RouteGroupType { + switch self { + case let .group(group): + return group + case let .singleRoute(route): + return route.routeGroup + } + } +} + +/** + Struct describing presented route. + */ +struct PresentedRoute<RouteType: AppRouteProtocol>: Equatable { + var route: RouteType + var coordinator: Coordinator +} + +/** + Struct holding information used by delegate to perform dismissal of the route(s) in subject. + */ +struct RouteDismissalContext<RouteType: AppRouteProtocol> { + /** + Specific routes that are being dismissed. + */ + var dismissedRoutes: [PresentedRoute<RouteType>] + + /** + Whether the entire group is being dismissed. + */ + var isClosing: Bool + + /** + Whether transition is animated. + */ + var isAnimated: Bool +} + +/** + Struct holding information used by delegate to perform sub-navigation of the route in subject. + */ +struct RouteSubnavigationContext<RouteType: AppRouteProtocol> { + var presentedRoute: PresentedRoute<RouteType> + var route: RouteType + var isAnimated: Bool +} |
