Skip to content

Commit

Permalink
Coordinate alert presentation by integrating it into routing system
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson committed Sep 1, 2023
1 parent ab84091 commit 26a812b
Show file tree
Hide file tree
Showing 26 changed files with 522 additions and 368 deletions.
38 changes: 27 additions & 11 deletions ios/MullvadVPN.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions ios/MullvadVPN/Classes/AppRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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 {
enum AppRouteGroup: String, AppRouteGroupProtocol {
/**
Primary horizontal navigation group.
*/
Expand All @@ -39,12 +39,17 @@ enum AppRouteGroup: AppRouteGroupProtocol {
*/
case changelog

/**
Alert group.
*/
case alert

var isModal: Bool {
switch self {
case .primary:
return UIDevice.current.userInterfaceIdiom == .pad

case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
return true
}
}
Expand All @@ -55,6 +60,9 @@ enum AppRouteGroup: AppRouteGroupProtocol {
return 0
case .settings, .account, .selectLocation, .changelog:
return 1
case .alert:
// Alerts should always be topmost.
return 999
}
}
}
Expand Down Expand Up @@ -83,14 +91,19 @@ enum AppRoute: AppRouteProtocol {
*/
case changelog

/**
Alert route.
*/
case alert(AlertPresentation)

/**
Routes that are part of primary horizontal navigation group.
*/
case tos, login, main, revoked, outOfTime, welcome

var isExclusive: Bool {
switch self {
case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
return true
default:
return false
Expand All @@ -117,6 +130,8 @@ enum AppRoute: AppRouteProtocol {
return .account
case .settings:
return .settings
case .alert:
return .alert
}
}
}
47 changes: 20 additions & 27 deletions ios/MullvadVPN/Coordinators/AccountCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ enum AddedMoreCreditOption: Equatable {
final class AccountCoordinator: Coordinator, Presentable, Presenting {
private let interactor: AccountInteractor
private var accountController: AccountViewController?
private let alertPresenter = AlertPresenter()

let navigationController: UINavigationController
var presentedViewController: UIViewController {
Expand All @@ -49,10 +48,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {

let accountController = AccountViewController(
interactor: interactor,
errorPresenter: PaymentAlertPresenter(
presentationController: presentationContext,
alertPresenter: alertPresenter
)
errorPresenter: PaymentAlertPresenter(coordinator: self)
)

accountController.actionHandler = handleViewControllerAction
Expand Down Expand Up @@ -137,19 +133,18 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
// MARK: - Alerts

private func logOut() {
let alertController = CustomAlertViewController(icon: .spinner)
let presentation = AlertPresentation(icon: .spinner, message: nil, buttons: [])

alertPresenter.enqueue(alertController, presentingController: presentationContext) {
self.interactor.logout {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }
interactor.logout {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }

alertController.dismiss(animated: true) {
self.didFinish?(self, .userLoggedOut)
}
}
applicationRouter?.dismiss(.alert(presentation), animated: true)
self.didFinish?(self, .userLoggedOut)
}
}

applicationRouter?.present(.alert(presentation))
}

private func showAccountDeviceInfo() {
Expand All @@ -168,21 +163,19 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting {
comment: ""
)

let alertController = CustomAlertViewController(
let presentation = AlertPresentation(
message: message,
icon: .info
)

alertController.addAction(
title: NSLocalizedString(
"DEVICE_INFO_DIALOG_OK_ACTION",
tableName: "Account",
value: "Got it!",
comment: ""
),
style: .default
buttons: [AlertAction(
title: NSLocalizedString(
"DEVICE_INFO_DIALOG_OK_ACTION",
tableName: "Account",
value: "Got it!",
comment: ""
),
style: .default
)]
)

alertPresenter.enqueue(alertController, presentingController: presentationContext)
applicationRouter?.present(.alert(presentation), animated: true)
}
}
45 changes: 45 additions & 0 deletions ios/MullvadVPN/Coordinators/AlertCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// AlertCoordinator.swift
// MullvadVPN
//
// Created by Jon Petersson on 2023-08-23.
// Copyright © 2023 Mullvad VPN AB. All rights reserved.
//

import MullvadLogging
import Routing
import UIKit

final class AlertCoordinator: Coordinator, Presentable {
private var alertController: AlertViewController?
private let presentation: AlertPresentation

var didFinish: (() -> Void)?

var presentedViewController: UIViewController {
return alertController!
}

init(presentation: AlertPresentation) {
self.presentation = presentation
}

func start() {
let alertController = AlertViewController(
header: presentation.header,
title: presentation.title,
message: presentation.message,
icon: presentation.icon
)

self.alertController = alertController

alertController.onDismiss = { [weak self] in
self?.didFinish?()
}

presentation.buttons.forEach { action in
alertController.addAction(title: action.title, style: action.style, handler: action.handler)
}
}
}
57 changes: 37 additions & 20 deletions ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
/**
Application router.
*/
private var router: ApplicationRouter<AppRoute>!
private(set) var router: ApplicationRouter<AppRoute>!

/**
Primary navigation container.
Expand Down Expand Up @@ -154,6 +154,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo

case .welcome:
presentWelcome(animated: animated, completion: completion)

case let .alert(presentation):
presentAlert(presentation: presentation, animated: animated, completion: completion)
}
}

Expand All @@ -162,15 +165,15 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
dismissWithContext context: RouteDismissalContext<RouteType>,
completion: @escaping () -> Void
) {
if context.isClosing {
let dismissedRoute = context.dismissedRoutes.first!
let dismissedRoute = context.dismissedRoutes.first!

if context.isClosing {
switch dismissedRoute.route.routeGroup {
case .primary:
endHorizontalFlow(animated: context.isAnimated, completion: completion)
context.dismissedRoutes.forEach { $0.coordinator.removeFromParent() }

case .selectLocation, .account, .settings, .changelog:
case .selectLocation, .account, .settings, .changelog, .alert:
guard let coordinator = dismissedRoute.coordinator as? Presentable else {
completion()
return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)")
Expand All @@ -179,13 +182,13 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
coordinator.dismiss(animated: context.isAnimated, completion: completion)
}
} else {
let dismissedRoute = context.dismissedRoutes.first!
assert(context.dismissedRoutes.count == 1)

if dismissedRoute.route == .outOfTime {
guard let coordinator = dismissedRoute.coordinator as? OutOfTimeCoordinator else {
switch dismissedRoute.route {
case .outOfTime, .welcome:
guard let coordinator = dismissedRoute.coordinator as? Poppable else {
completion()
return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
return assertionFailure("Expected presentable coordinator for \(dismissedRoute.route)")
}

coordinator.popFromNavigationStack(
Expand All @@ -194,19 +197,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
)

coordinator.removeFromParent()
} else if dismissedRoute.route == .welcome {
guard let coordinator = dismissedRoute.coordinator as? WelcomeCoordinator else {
completion()
return assertionFailure("Unhandled coordinator for \(dismissedRoute.route)")
}

coordinator.popFromNavigationStack(
animated: context.isAnimated,
completion: completion
)

coordinator.removeFromParent()
} else {
default:
assertionFailure("Unhandled dismissal for \(dismissedRoute.route)")
completion()
}
Expand Down Expand Up @@ -649,6 +641,24 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo
}
}

private func presentAlert(
presentation: AlertPresentation,
animated: Bool,
completion: @escaping (Coordinator) -> Void
) {
let coordinator = AlertCoordinator(presentation: presentation)

coordinator.didFinish = { [weak self] in
self?.router.dismiss(.alert(presentation))
}

coordinator.start()

presentChild(coordinator, animated: animated) {
completion(coordinator)
}
}

private func makeTunnelCoordinator() -> TunnelCoordinator {
let tunnelCoordinator = TunnelCoordinator(tunnelManager: tunnelManager)

Expand Down Expand Up @@ -980,3 +990,10 @@ fileprivate extension AppPreferencesDataSource {
self.lastSeenChangeLogVersion = Bundle.main.shortVersion
}
}

private protocol Poppable: Presentable {
func popFromNavigationStack(
animated: Bool,
completion: () -> Void
)
}
5 changes: 3 additions & 2 deletions ios/MullvadVPN/Coordinators/ChangeLogCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Routing
import UIKit

final class ChangeLogCoordinator: Coordinator, Presentable {
private var alertController: CustomAlertViewController?
private var alertController: AlertViewController?
private let interactor: ChangeLogInteractor
var didFinish: ((ChangeLogCoordinator) -> Void)?

Expand All @@ -24,7 +24,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable {
}

func start() {
let alertController = CustomAlertViewController(
let alertController = AlertViewController(
header: interactor.viewModel.header,
title: interactor.viewModel.title,
attributedMessage: interactor.viewModel.body
Expand All @@ -43,6 +43,7 @@ final class ChangeLogCoordinator: Coordinator, Presentable {
didFinish?(self)
}
)

self.alertController = alertController
}
}
32 changes: 18 additions & 14 deletions ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,27 @@ class InAppPurchaseCoordinator: Coordinator, Presentable {
coordinator.start()

case let .failure(failure):
let alertController = CustomAlertViewController(
let presentation = AlertPresentation(
icon: .alert,
message: failure.error.localizedDescription,
icon: .alert
buttons: [
AlertAction(
title: NSLocalizedString(
"IN_APP_PURCHASE_ERROR_DIALOG_OK_ACTION",
tableName: "Welcome",
value: "Got it!",
comment: ""
),
style: .default,
handler: { [weak self] in
guard let self = self else { return }
self.didCancel?(self)
}
),
]
)

alertController.addAction(
title: NSLocalizedString(
"IN_APP_PURCHASE_ERROR_DIALOG_OK_ACTION",
tableName: "Welcome",
value: "Got it!",
comment: ""
),
style: .default
)
presentedViewController.present(alertController, animated: true) {
self.didCancel?(self)
}
applicationRouter?.present(.alert(presentation), animated: true)
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion ios/MullvadVPN/Coordinators/LoginCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat
accountNumber: accountNumber,
devicesProxy: devicesProxy
)
let controller = DeviceManagementViewController(interactor: interactor)
let controller = DeviceManagementViewController(
interactor: interactor,
alertPresenter: AlertPresenter(coordinator: self)
)
controller.delegate = self
controller.fetchDevices(animateUpdates: false) { [weak self] result in
guard let self else { return }
Expand Down
5 changes: 1 addition & 4 deletions ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate {

let controller = OutOfTimeViewController(
interactor: interactor,
errorPresenter: PaymentAlertPresenter(
presentationController: navigationController,
alertPresenter: AlertPresenter()
)
errorPresenter: PaymentAlertPresenter(coordinator: self)
)

controller.delegate = self
Expand Down
Loading

0 comments on commit 26a812b

Please sign in to comment.