diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-08-17 14:20:54 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-08-17 14:20:54 +0200 |
| commit | ff9eb384a5d32516b7f17d625ceab29fd34c3271 (patch) | |
| tree | 19f2de9f4cfa8eda6715412e8b97804442e4658d /ios | |
| parent | a21b89e36a7a1736e0379a5aa3c8278feae72124 (diff) | |
| parent | 3abe87493beab24f33e248ef66cd6585c4d1df8a (diff) | |
| download | mullvadvpn-ff9eb384a5d32516b7f17d625ceab29fd34c3271.tar.xz mullvadvpn-ff9eb384a5d32516b7f17d625ceab29fd34c3271.zip | |
Merge branch 'make-app-router-more-abstract-ios-279'
Diffstat (limited to 'ios')
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift | 20 | ||||
| -rw-r--r-- | ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift | 170 |
2 files changed, 114 insertions, 76 deletions
diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift index e19aa34181..b8d89f4831 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationCoordinator.swift @@ -17,10 +17,12 @@ import UIKit final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate, UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate { + typealias RouteType = AppRoute + /** Application router. */ - private var router: ApplicationRouter! + private var router: ApplicationRouter<AppRoute>! /** Primary navigation container. @@ -113,7 +115,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo // MARK: - ApplicationRouterDelegate func applicationRouter( - _ router: ApplicationRouter, + _ router: ApplicationRouter<RouteType>, route: AppRoute, animated: Bool, completion: @escaping (Coordinator) -> Void @@ -155,8 +157,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } func applicationRouter( - _ router: ApplicationRouter, - dismissWithContext context: RouteDismissalContext, + _ router: ApplicationRouter<RouteType>, + dismissWithContext context: RouteDismissalContext<RouteType>, completion: @escaping () -> Void ) { if context.isClosing { @@ -210,7 +212,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } } - func applicationRouter(_ router: ApplicationRouter, shouldPresent route: AppRoute) -> Bool { + func applicationRouter(_ router: ApplicationRouter<RouteType>, shouldPresent route: RouteType) -> Bool { switch route { case .revoked: // Check if device is still revoked. @@ -226,8 +228,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } func applicationRouter( - _ router: ApplicationRouter, - shouldDismissWithContext context: RouteDismissalContext + _ router: ApplicationRouter<RouteType>, + shouldDismissWithContext context: RouteDismissalContext<RouteType> ) -> Bool { context.dismissedRoutes.allSatisfy { dismissedRoute in /* @@ -245,8 +247,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } func applicationRouter( - _ router: ApplicationRouter, - handleSubNavigationWithContext context: RouteSubnavigationContext, + _ router: ApplicationRouter<RouteType>, + handleSubNavigationWithContext context: RouteSubnavigationContext<RouteType>, completion: @escaping () -> Void ) { switch context.route { diff --git a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift index 5a8b8ecd7d..2e77f3d590 100644 --- a/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift +++ b/ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift @@ -11,10 +11,61 @@ 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: Comparable, Equatable, Hashable { +enum AppRouteGroup: AppRouteGroupProtocol { /** Primary horizontal navigation group. */ @@ -40,10 +91,6 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { */ case changelog - /** - Returns `true` if group is presented modally, otherwise `false` if group is a part of root view - controller. - */ var isModal: Bool { switch self { case .primary: @@ -54,7 +101,7 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { } } - private var order: Int { + var modalLevel: Int { switch self { case .primary: return 0 @@ -62,16 +109,12 @@ enum AppRouteGroup: Comparable, Equatable, Hashable { return 1 } } - - static func < (lhs: AppRouteGroup, rhs: AppRouteGroup) -> Bool { - lhs.order < rhs.order - } } /** Enum type describing primary application routes. */ -enum AppRoute: Equatable, Hashable { +enum AppRoute: AppRouteProtocol { /** Account route. */ @@ -97,9 +140,6 @@ enum AppRoute: Equatable, Hashable { */ case tos, login, main, revoked, outOfTime, welcome, setupAccountCompleted - /** - Returns `true` when only one route of a kind can be displayed. - */ var isExclusive: Bool { switch self { case .selectLocation, .account, .settings, .changelog: @@ -109,9 +149,6 @@ enum AppRoute: Equatable, Hashable { } } - /** - Returns `true` if the route supports sub-navigation. - */ var supportsSubNavigation: Bool { if case .settings = self { return true @@ -120,9 +157,6 @@ enum AppRoute: Equatable, Hashable { } } - /** - Navigation group to which the route belongs to. - */ var routeGroup: AppRouteGroup { switch self { case .tos, .login, .main, .revoked, .outOfTime, .welcome, .setupAccountCompleted: @@ -142,8 +176,8 @@ enum AppRoute: Equatable, Hashable { /** Struct describing a routing request for presentation or dismissal. */ -struct PendingRoute: Equatable { - var operation: RouteOperation +struct PendingRoute<RouteType: AppRouteProtocol>: Equatable { + var operation: RouteOperation<RouteType> var animated: Bool } @@ -200,21 +234,21 @@ enum PendingDismissalResult { /** Enum describing operation over the route. */ -enum RouteOperation: Equatable { +enum RouteOperation<RouteType: AppRouteProtocol>: Equatable { /** Present route. */ - case present(AppRoute) + case present(RouteType) /** Dismiss route. */ - case dismiss(DismissMatch) + case dismiss(DismissMatch<RouteType>) /** Returns a group of affected routes. */ - var routeGroup: AppRouteGroup { + var routeGroup: RouteType.RouteGroupType { switch self { case let .present(route): return route.routeGroup @@ -227,14 +261,14 @@ enum RouteOperation: Equatable { /** Enum type describing a single route or a group of routes requested to be dismissed. */ -enum DismissMatch: Equatable { - case group(AppRouteGroup) - case singleRoute(AppRoute) +enum DismissMatch<RouteType: AppRouteProtocol>: Equatable { + case group(RouteType.RouteGroupType) + case singleRoute(RouteType) /** Returns a group of affected routes. */ - var routeGroup: AppRouteGroup { + var routeGroup: RouteType.RouteGroupType { switch self { case let .group(group): return group @@ -247,19 +281,19 @@ enum DismissMatch: Equatable { /** Struct describing presented route. */ -struct PresentedRoute: Equatable { - var route: AppRoute +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 { +struct RouteDismissalContext<RouteType: AppRouteProtocol> { /** Specific routes that are being dismissed. */ - var dismissedRoutes: [PresentedRoute] + var dismissedRoutes: [PresentedRoute<RouteType>] /** Whether the entire group is being dismissed. @@ -275,22 +309,24 @@ struct RouteDismissalContext { /** Struct holding information used by delegate to perform sub-navigation of the route in subject. */ -struct RouteSubnavigationContext { - var presentedRoute: PresentedRoute - var route: AppRoute +struct RouteSubnavigationContext<RouteType: AppRouteProtocol> { + var presentedRoute: PresentedRoute<RouteType> + var route: RouteType var isAnimated: Bool } /** Application router delegate */ -protocol ApplicationRouterDelegate: AnyObject { +protocol ApplicationRouterDelegate<RouteType>: AnyObject { + associatedtype RouteType: AppRouteProtocol + /** Delegate should present the route and pass corresponding `Coordinator` upon completion. */ func applicationRouter( - _ router: ApplicationRouter, - route: AppRoute, + _ router: ApplicationRouter<RouteType>, + route: RouteType, animated: Bool, completion: @escaping (Coordinator) -> Void ) @@ -299,8 +335,8 @@ protocol ApplicationRouterDelegate: AnyObject { Delegate should dismiss the route. */ func applicationRouter( - _ router: ApplicationRouter, - dismissWithContext context: RouteDismissalContext, + _ router: ApplicationRouter<RouteType>, + dismissWithContext context: RouteDismissalContext<RouteType>, completion: @escaping () -> Void ) @@ -309,7 +345,7 @@ protocol ApplicationRouterDelegate: AnyObject { Return `true` to proceed with presenation, otherwise `false` to prevent it. */ - func applicationRouter(_ router: ApplicationRouter, shouldPresent route: AppRoute) -> Bool + func applicationRouter(_ router: ApplicationRouter<RouteType>, shouldPresent route: RouteType) -> Bool /** Delegate may reconsider if route dismissal should be done. @@ -317,8 +353,8 @@ protocol ApplicationRouterDelegate: AnyObject { Return `true` to proceed with dismissal, otherwise `false` to prevent it. */ func applicationRouter( - _ router: ApplicationRouter, - shouldDismissWithContext context: RouteDismissalContext + _ router: ApplicationRouter<RouteType>, + shouldDismissWithContext context: RouteDismissalContext<RouteType> ) -> Bool /** @@ -326,8 +362,8 @@ protocol ApplicationRouterDelegate: AnyObject { router when it's done. */ func applicationRouter( - _ router: ApplicationRouter, - handleSubNavigationWithContext context: RouteSubnavigationContext, + _ router: ApplicationRouter<RouteType>, + handleSubNavigationWithContext context: RouteSubnavigationContext<RouteType>, completion: @escaping () -> Void ) } @@ -335,37 +371,37 @@ protocol ApplicationRouterDelegate: AnyObject { /** Main application router. */ -final class ApplicationRouter { +final class ApplicationRouter<RouteType: AppRouteProtocol> { private let logger = Logger(label: "ApplicationRouter") - private(set) var modalStack: [AppRouteGroup] = [] - private var presentedRoutes: [AppRouteGroup: [PresentedRoute]] = [:] + private(set) var modalStack: [RouteType.RouteGroupType] = [] + private var presentedRoutes: [RouteType.RouteGroupType: [PresentedRoute<RouteType>]] = [:] - private var pendingRoutes = [PendingRoute]() + private var pendingRoutes = [PendingRoute<RouteType>]() private var isProcessingPendingRoutes = false - private unowned let delegate: ApplicationRouterDelegate + private unowned let delegate: any ApplicationRouterDelegate<RouteType> /** Designated initializer. Delegate object is unonwed and the caller has to guarantee that the router does not outlive it. */ - init(_ delegate: ApplicationRouterDelegate) { + init(_ delegate: some ApplicationRouterDelegate<RouteType>) { self.delegate = delegate } /** Returns `true` is the given route group is currently being presented. */ - func isPresenting(group: AppRouteGroup) -> Bool { + func isPresenting(group: RouteType.RouteGroupType) -> Bool { modalStack.contains(group) } /** Returns `true` if is the given route is currently being presented. */ - func isPresenting(route: AppRoute) -> Bool { + func isPresenting(route: RouteType) -> Bool { guard let presentedRoute = presentedRoutes[route.routeGroup] else { return false } @@ -375,7 +411,7 @@ final class ApplicationRouter { /** Enqueue route for presetnation. */ - func present(_ route: AppRoute, animated: Bool = true) { + func present(_ route: RouteType, animated: Bool = true) { enqueue(PendingRoute( operation: .present(route), animated: animated @@ -385,7 +421,7 @@ final class ApplicationRouter { /** Enqueue dismissal of the route. */ - func dismiss(_ route: AppRoute, animated: Bool = true) { + func dismiss(_ route: RouteType, animated: Bool = true) { enqueue(PendingRoute( operation: .dismiss(.singleRoute(route)), animated: animated @@ -395,14 +431,14 @@ final class ApplicationRouter { /** Enqueue dismissal of a group of routes. */ - func dismissAll(_ group: AppRouteGroup, animated: Bool = true) { + func dismissAll(_ group: RouteType.RouteGroupType, animated: Bool = true) { enqueue(PendingRoute( operation: .dismiss(.group(group)), animated: animated )) } - private func enqueue(_ pendingRoute: PendingRoute) { + private func enqueue(_ pendingRoute: PendingRoute<RouteType>) { logger.debug("Enqueue \(pendingRoute.operation).") pendingRoutes.append(pendingRoute) @@ -413,7 +449,7 @@ final class ApplicationRouter { } private func presentRoute( - _ route: AppRoute, + _ route: RouteType, animated: Bool, completion: @escaping (PendingPresentationResult) -> Void ) { @@ -487,7 +523,7 @@ final class ApplicationRouter { } private func dismissGroup( - _ dismissGroup: AppRouteGroup, + _ dismissGroup: RouteType.RouteGroupType, animated: Bool, completion: @escaping (PendingDismissalResult) -> Void ) { @@ -533,7 +569,7 @@ final class ApplicationRouter { } private func dismissRoute( - _ dismissRoute: AppRoute, + _ dismissRoute: RouteType, animated: Bool, completion: @escaping (PendingDismissalResult) -> Void ) { @@ -584,7 +620,7 @@ final class ApplicationRouter { } } - private func processPendingRoutes(skipRouteGroups: Set<AppRouteGroup> = []) { + private func processPendingRoutes(skipRouteGroups: Set<RouteType.RouteGroupType> = []) { isProcessingPendingRoutes = true let pendingRoute = pendingRoutes.first { pendingRoute in @@ -634,7 +670,7 @@ final class ApplicationRouter { } private func handleDismissal( - _ dismissMatch: DismissMatch, + _ dismissMatch: DismissMatch<RouteType>, animated: Bool, completion: @escaping (PendingDismissalResult) -> Void ) { @@ -647,7 +683,7 @@ final class ApplicationRouter { } } - private func finishPendingRoute(_ pendingRoute: PendingRoute) { + private func finishPendingRoute(_ pendingRoute: PendingRoute<RouteType>) { if let index = pendingRoutes.firstIndex(of: pendingRoute) { pendingRoutes.remove(at: index) } @@ -655,7 +691,7 @@ final class ApplicationRouter { processPendingRoutes() } - private func handleInteractiveDismissal(route: AppRoute, coordinator: Coordinator) { + private func handleInteractiveDismissal(route: RouteType, coordinator: Coordinator) { var routes = presentedRoutes[route.routeGroup] ?? [] routes.removeAll { presentedRoute in @@ -674,7 +710,7 @@ final class ApplicationRouter { } } - private func addPresentedRoute(_ presented: PresentedRoute) { + private func addPresentedRoute(_ presented: PresentedRoute<RouteType>) { let group = presented.route.routeGroup var routes = presentedRoutes[group] ?? [] |
