summaryrefslogtreecommitdiffhomepage
path: root/ios
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-08-17 14:20:54 +0200
committerAndrej Mihajlov <and@mullvad.net>2023-08-17 14:20:54 +0200
commitff9eb384a5d32516b7f17d625ceab29fd34c3271 (patch)
tree19f2de9f4cfa8eda6715412e8b97804442e4658d /ios
parenta21b89e36a7a1736e0379a5aa3c8278feae72124 (diff)
parent3abe87493beab24f33e248ef66cd6585c4d1df8a (diff)
downloadmullvadvpn-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.swift20
-rw-r--r--ios/MullvadVPN/Coordinators/App/ApplicationRouter.swift170
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] ?? []