summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-08-17 14:43:08 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-08-17 14:43:08 +0200
commit24fff949d10a7a865ae29029453298f56cc2f6db (patch)
treed8af625539ef4c2863993c6587d31c7d3ebf3e3d
parentff9eb384a5d32516b7f17d625ceab29fd34c3271 (diff)
parent3548a2298721c63e4cf463a6da154d28971b26ee (diff)
downloadmullvadvpn-24fff949d10a7a865ae29029453298f56cc2f6db.tar.xz
mullvadvpn-24fff949d10a7a865ae29029453298f56cc2f6db.zip
Merge branch 'extract-routing'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj28
-rw-r--r--ios/MullvadVPN/Routing/AppRouteProtocol.swift60
-rw-r--r--ios/MullvadVPN/Routing/AppRoutes.swift121
-rw-r--r--ios/MullvadVPN/Routing/ApplicationRouter.swift (renamed from ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift)358
-rw-r--r--ios/MullvadVPN/Routing/ApplicationRouterDelegate.swift62
-rw-r--r--ios/MullvadVPN/Routing/ApplicationRouterTypes.swift151
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
+}