diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index b5634621a301..c43f3e8b43d9 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -31,6 +31,8 @@ line_length: ignores_interpolated_strings: true warning: 120 error: 300 +cyclomatic_complexity: + ignores_case_statements: true type_name: min_length: 4 diff --git a/ios/MullvadVPN/Classes/AppRoutes.swift b/ios/MullvadVPN/Classes/AppRoutes.swift index cf81ddf4445d..c86022f7073e 100644 --- a/ios/MullvadVPN/Classes/AppRoutes.swift +++ b/ios/MullvadVPN/Classes/AppRoutes.swift @@ -94,7 +94,7 @@ enum AppRoute: AppRouteProtocol { /** Alert route. */ - case alert(AlertPresentation) + case alert(String) /** Routes that are part of primary horizontal navigation group. diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index efc82800f8d7..a5f55fe28085 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -29,10 +29,6 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { navigationController } - var presentationContext: UIViewController { - navigationController - } - var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)? init( @@ -48,7 +44,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { let accountController = AccountViewController( interactor: interactor, - errorPresenter: PaymentAlertPresenter(coordinator: self) + errorPresenter: PaymentAlertPresenter(alertContext: self) ) accountController.actionHandler = handleViewControllerAction @@ -133,18 +129,25 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { // MARK: - Alerts private func logOut() { - let presentation = AlertPresentation(icon: .spinner, message: nil, buttons: []) + let presentation = AlertPresentation( + id: "account-logout-alert", + icon: .spinner, + message: nil, + buttons: [] + ) + + let alertPresenter = AlertPresenter(context: self) interactor.logout { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in guard let self else { return } - applicationRouter?.dismiss(.alert(presentation), animated: true) + alertPresenter.dismissAlert(presentation: presentation, animated: true) self.didFinish?(self, .userLoggedOut) } } - applicationRouter?.present(.alert(presentation)) + alertPresenter.showAlert(presentation: presentation, animated: true) } private func showAccountDeviceInfo() { @@ -164,6 +167,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { ) let presentation = AlertPresentation( + id: "account-device-info-alert", message: message, buttons: [AlertAction( title: NSLocalizedString( @@ -176,6 +180,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { )] ) - applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) } } diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index ae3300529042..e45e40d74754 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -16,8 +16,7 @@ import UIKit Application coordinator managing split view and two navigation contexts. */ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate, - UISplitViewControllerDelegate, ApplicationRouterDelegate, - NotificationManagerDelegate { + UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate { typealias RouteType = AppRoute /** @@ -120,11 +119,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo func applicationRouter( _ router: ApplicationRouter, - route: AppRoute, + presentWithContext context: RoutePresentationContext, animated: Bool, completion: @escaping (Coordinator) -> Void ) { - switch route { + switch context.route { case .account: presentAccount(animated: animated, completion: completion) @@ -155,8 +154,8 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo case .welcome: presentWelcome(animated: animated, completion: completion) - case let .alert(presentation): - presentAlert(presentation: presentation, animated: animated, completion: completion) + case .alert: + presentAlert(animated: animated, context: context, completion: completion) } } @@ -564,11 +563,10 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinishPayment = { [weak self] _ in - guard let self else { return } + guard let self = self else { return } if shouldDismissOutOfTime() { router.dismiss(.outOfTime, animated: true) - continueFlow(animated: true) } } @@ -589,7 +587,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinish = { [weak self] _ in - guard let self else { return } + guard let self = self else { return } appPreferences.isShownOnboarding = true router.dismiss(.welcome, animated: false) continueFlow(animated: false) @@ -642,19 +640,24 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo } private func presentAlert( - presentation: AlertPresentation, animated: Bool, + context: RoutePresentationContext, completion: @escaping (Coordinator) -> Void ) { - let coordinator = AlertCoordinator(presentation: presentation) + guard let metadata = context.metadata as? AlertMetadata else { + assertionFailure("Could not get AlertMetadata from RoutePresentationContext.") + return + } + + let coordinator = AlertCoordinator(presentation: metadata.presentation) coordinator.didFinish = { [weak self] in - self?.router.dismiss(.alert(presentation)) + self?.router.dismiss(.alert(metadata.presentation.id)) } coordinator.start() - presentChild(coordinator, animated: animated) { + metadata.context.presentChild(coordinator, animated: animated) { completion(coordinator) } } @@ -996,4 +999,6 @@ private protocol Poppable: Presentable { animated: Bool, completion: () -> Void ) + + // swiftlint:disable:next file_length } diff --git a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift index 7d3a65df5bed..e2ccb578df87 100644 --- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift @@ -11,7 +11,7 @@ import Routing import StoreKit import UIKit -class InAppPurchaseCoordinator: Coordinator, Presentable { +class InAppPurchaseCoordinator: Coordinator, Presenting, Presentable { private let navigationController: RootContainerViewController private let interactor: InAppPurchaseInteractor @@ -50,6 +50,7 @@ class InAppPurchaseCoordinator: Coordinator, Presentable { case let .failure(failure): let presentation = AlertPresentation( + id: "in-app-purchase-error-alert", icon: .alert, message: failure.error.localizedDescription, buttons: [ @@ -69,7 +70,8 @@ class InAppPurchaseCoordinator: Coordinator, Presentable { ] ) - applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) } } } diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index af989dc628be..9f01af0f77c7 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -12,7 +12,7 @@ import Operations import Routing import UIKit -final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegate { +final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewControllerDelegate { private let tunnelManager: TunnelManager private let devicesProxy: REST.DevicesProxy @@ -22,6 +22,10 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat var didFinish: ((LoginCoordinator) -> Void)? var didCreateAccount: (() -> Void)? + var presentationContext: UIViewController { + navigationController + } + let navigationController: RootContainerViewController init( @@ -107,11 +111,12 @@ final class LoginCoordinator: Coordinator, DeviceManagementViewControllerDelegat ) let controller = DeviceManagementViewController( interactor: interactor, - alertPresenter: AlertPresenter(coordinator: self) + alertPresenter: AlertPresenter(context: self) ) controller.delegate = self + controller.fetchDevices(animateUpdates: false) { [weak self] result in - guard let self else { return } + guard let self = self else { return } switch result { case .success: diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift index 137fbcf44665..d26cb5d2a7bc 100644 --- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift @@ -9,13 +9,17 @@ import Routing import UIKit -class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate { +class OutOfTimeCoordinator: Coordinator, Presenting, OutOfTimeViewControllerDelegate { let navigationController: RootContainerViewController let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager var didFinishPayment: ((OutOfTimeCoordinator) -> Void)? + var presentationContext: UIViewController { + navigationController + } + private(set) var isMakingPayment = false private var viewController: OutOfTimeViewController? @@ -42,7 +46,7 @@ class OutOfTimeCoordinator: Coordinator, OutOfTimeViewControllerDelegate { let controller = OutOfTimeViewController( interactor: interactor, - errorPresenter: PaymentAlertPresenter(coordinator: self) + errorPresenter: PaymentAlertPresenter(alertContext: self) ) controller.delegate = self diff --git a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift index 1cd368fc7944..88a32826b5e1 100644 --- a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift @@ -32,10 +32,6 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV navigationController } - var presentationContext: UIViewController { - navigationController - } - var willNavigate: (( _ coordinator: SettingsCoordinator, _ from: SettingsNavigationRoute?, @@ -159,13 +155,13 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV case .preferences: return PreferencesViewController( interactor: interactorFactory.makePreferencesInteractor(), - alertPresenter: AlertPresenter(coordinator: self) + alertPresenter: AlertPresenter(context: self) ) case .problemReport: return ProblemReportViewController( interactor: interactorFactory.makeProblemReportInteractor(), - alertPresenter: AlertPresenter(coordinator: self) + alertPresenter: AlertPresenter(context: self) ) case .faq: diff --git a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift index 5f59a6e3bfd8..255728ccef1b 100644 --- a/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/TunnelCoordinator.swift @@ -9,12 +9,16 @@ import Routing import UIKit -class TunnelCoordinator: Coordinator { +class TunnelCoordinator: Coordinator, Presenting { private let tunnelManager: TunnelManager private let controller: TunnelViewController private var tunnelObserver: TunnelObserver? + var presentationContext: UIViewController { + controller + } + var rootViewController: UIViewController { controller } @@ -59,6 +63,7 @@ class TunnelCoordinator: Coordinator { private func showCancelTunnelAlert() { let presentation = AlertPresentation( + id: "main-cancel-tunnel-alert", icon: .alert, message: NSLocalizedString( "CANCEL_TUNNEL_ALERT_MESSAGE", @@ -91,6 +96,7 @@ class TunnelCoordinator: Coordinator { ] ) - applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) } } diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index 7185321f009d..97a7c8e5413b 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -26,10 +26,6 @@ final class WelcomeCoordinator: Coordinator, Presentable, Presenting { navigationController } - var presentationContext: UIViewController { - navigationController - } - init( navigationController: RootContainerViewController, storePaymentManager: StorePaymentManager, @@ -102,6 +98,7 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { ) let presentation = AlertPresentation( + id: "welcome-device-name-alert", icon: .info, message: message, buttons: [ @@ -117,7 +114,8 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { ] ) - applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) } func didRequestToPurchaseCredit(controller: WelcomeViewController, accountNumber: String, product: SKProduct) { @@ -150,7 +148,7 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { ) coordinator.didCancel = { [weak self] coordinator in - guard let self else { return } + guard let self = self else { return } navigationController.popViewController(animated: true) coordinator.removeFromParent() } diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index 1f31e9297386..59a3bd37f0fe 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -27,6 +27,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand private var tunnelObserver: TunnelObserver? private var appDelegate: AppDelegate { + // swiftlint:disable:next force_cast UIApplication.shared.delegate as! AppDelegate } @@ -185,7 +186,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand // MARK: - SettingsMigrationUIHandler func showMigrationError(_ error: Error, completionHandler: @escaping () -> Void) { + guard let appCoordinator else { + completionHandler() + return + } + let presentation = AlertPresentation( + id: "settings-migration-error-alert", title: NSLocalizedString( "ALERT_TITLE", tableName: "SettingsMigrationUI", @@ -204,7 +211,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHand ] ) - appCoordinator?.router.present(.alert(presentation), animated: true) ?? completionHandler() + let presenter = AlertPresenter(context: appCoordinator) + presenter.showAlert(presentation: presentation, animated: true) } private static func migrationErrorReason(_ error: Error) -> String { diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift index 8ca856688d16..0192f3fdd306 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift @@ -10,7 +10,7 @@ import MullvadREST import Routing struct PaymentAlertPresenter { - let coordinator: Coordinator + let alertContext: any Presenting func showAlertForError( _ error: StorePaymentManagerError, @@ -18,6 +18,7 @@ struct PaymentAlertPresenter { completion: (() -> Void)? = nil ) { let presentation = AlertPresentation( + id: "payment-error-alert", title: context.errorTitle, message: error.displayErrorDescription, buttons: [ @@ -31,7 +32,8 @@ struct PaymentAlertPresenter { ] ) - coordinator.applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: alertContext) + presenter.showAlert(presentation: presentation, animated: true) } func showAlertForResponse( @@ -45,6 +47,7 @@ struct PaymentAlertPresenter { } let presentation = AlertPresentation( + id: "payment-response-alert", title: response.alertTitle(context: context), message: response.alertMessage(context: context), buttons: [ @@ -58,7 +61,8 @@ struct PaymentAlertPresenter { ] ) - coordinator.applicationRouter?.present(.alert(presentation), animated: true) + let presenter = AlertPresenter(context: alertContext) + presenter.showAlert(presentation: presentation, animated: true) } private func okButtonTextForKey(_ key: String) -> String { diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift index 8a1993311096..9fcaac842906 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift @@ -7,6 +7,12 @@ // import Foundation +import Routing + +struct AlertMetadata { + let presentation: AlertPresentation + let context: Presenting +} struct AlertAction { let title: String @@ -15,7 +21,7 @@ struct AlertAction { } struct AlertPresentation: Identifiable, CustomDebugStringConvertible { - let id = UUID() + let id: String var header: String? var icon: AlertIcon? @@ -24,7 +30,7 @@ struct AlertPresentation: Identifiable, CustomDebugStringConvertible { let buttons: [AlertAction] var debugDescription: String { - id.uuidString + return id } } diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift index dc4dbd23b090..a61b6b7dfe37 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift @@ -9,9 +9,16 @@ import Routing struct AlertPresenter { - let coordinator: Coordinator + let context: any Presenting func showAlert(presentation: AlertPresentation, animated: Bool) { - coordinator.applicationRouter?.present(.alert(presentation), animated: animated) + context.applicationRouter?.presentAlert( + route: .alert(presentation.id), + metadata: AlertMetadata(presentation: presentation, context: context) + ) + } + + func dismissAlert(presentation: AlertPresentation, animated: Bool) { + context.applicationRouter?.dismiss(.alert(presentation.id), animated: animated) } } diff --git a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift index 3241a0bd0164..ca42035dd199 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertViewController.swift @@ -8,7 +8,7 @@ import UIKit -enum AlertActionStyle { +enum AlertActionStyle: Codable { case `default` case destructive @@ -22,7 +22,7 @@ enum AlertActionStyle { } } -enum AlertIcon { +enum AlertIcon: Codable { case alert case info case spinner diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift index e9f38ea54f1f..7675490333c2 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift @@ -89,7 +89,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { completionHandler: ((Result) -> Void)? = nil ) { interactor.getDevices { [weak self] result in - guard let self else { return } + guard let self = self else { return } if let devices = result.value { setDevices(devices, animated: animateUpdates) @@ -130,7 +130,9 @@ class DeviceManagementViewController: UIViewController, RootContainment { return } - deleteDevice(identifier: device.id) { error in + deleteDevice(identifier: device.id) { [weak self] error in + guard let self = self else { return } + if let error { self.showErrorAlert( title: NSLocalizedString( @@ -158,6 +160,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { private func showErrorAlert(title: String, error: Error) { let presentation = AlertPresentation( + id: "delete-device-error-alert", title: title, message: getErrorDescription(error), buttons: [ @@ -181,6 +184,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { completion: @escaping (_ shouldDelete: Bool) -> Void ) { let presentation = AlertPresentation( + id: "logout-confirmation-alert", icon: .alert, message: String( format: NSLocalizedString( @@ -223,7 +227,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { private func deleteDevice(identifier: String, completionHandler: @escaping (Error?) -> Void) { interactor.deleteDevice(identifier) { [weak self] completion in - guard let self else { return } + guard let self = self else { return } switch completion { case .success: diff --git a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift index af93b4083f38..95139e754069 100644 --- a/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift +++ b/ios/MullvadVPN/View controllers/Preferences/PreferencesViewController.swift @@ -79,6 +79,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel private func showContentBlockerInfo(with message: String) { let presentation = AlertPresentation( + id: "preferences-content-blockers-alert", icon: .info, message: message, buttons: [ @@ -145,8 +146,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel case .wireGuardPorts: let portsString = humanReadablePortRepresentation( - interactor.cachedRelays?.relays.wireguard - .portRanges ?? [] + interactor.cachedRelays?.relays.wireguard.portRanges ?? [] ) message = String( @@ -166,7 +166,6 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel ) #if DEBUG - case .wireGuardObfuscation: message = NSLocalizedString( "PREFERENCES_WIRE_GUARD_OBFUSCATION_GENERAL", @@ -183,6 +182,7 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel comment: "" ) #endif + default: assertionFailure("No matching InfoButtonItem") } diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift index 20341499f271..2566f5f62d84 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift @@ -44,7 +44,11 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { textLabel.text = NSLocalizedString( "SUBHEAD_LABEL", tableName: "ProblemReport", - value: "To help you more effectively, your app’s log file will be attached to this message. Your data will remain secure and private, as it is anonymised before being sent over an encrypted channel.", + value: """ + To help you more effectively, your app’s log file will be attached to \ + this message. Your data will remain secure and private, as it is anonymised \ + before being sent over an encrypted channel. + """, comment: "" ) return textLabel @@ -83,7 +87,10 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { textView.placeholder = NSLocalizedString( "DESCRIPTION_TEXTVIEW_PLACEHOLDER", tableName: "ProblemReport", - value: "To assist you better, please write in English or Swedish and include which country you are connecting from.", + value: """ + To assist you better, please write in English or Swedish and \ + include which country you are connecting from. + """, comment: "" ) textView.contentInsetAdjustmentBehavior = .never @@ -504,6 +511,7 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { private func presentEmptyEmailConfirmationAlert(completion: @escaping (Bool) -> Void) { let presentation = AlertPresentation( + id: "problem-report-alert", icon: .alert, message: NSLocalizedString( "EMPTY_EMAIL_ALERT_MESSAGE", diff --git a/ios/Routing/Coordinator.swift b/ios/Routing/Coordinator.swift index c111b76ee58a..b69659ab7d34 100644 --- a/ios/Routing/Coordinator.swift +++ b/ios/Routing/Coordinator.swift @@ -100,6 +100,15 @@ public protocol Presenting: Coordinator { var presentationContext: UIViewController { get } } +extension Presenting where Self: Presentable { + /** + View controller providing modal presentation context. + */ + public var presentationContext: UIViewController { + return presentedViewController + } +} + extension Presenting { /** Present child coordinator. @@ -112,6 +121,11 @@ extension Presenting { configuration: ModalPresentationConfiguration = ModalPresentationConfiguration(), completion: (() -> Void)? = nil ) { + assert( + presentationContext.presentedViewController == nil, + "Presenting context (\(presentationContext)) is already presenting another controller." + ) + var configuration = configuration configuration.notifyInteractiveDismissal { [weak child] in diff --git a/ios/Routing/Router/ApplicationRouter.swift b/ios/Routing/Router/ApplicationRouter.swift index 957398f77451..84427f89b661 100644 --- a/ios/Routing/Router/ApplicationRouter.swift +++ b/ios/Routing/Router/ApplicationRouter.swift @@ -93,6 +93,7 @@ public final class ApplicationRouter { private func presentRoute( _ route: RouteType, animated: Bool, + metadata: Any?, completion: @escaping (PendingPresentationResult) -> Void ) { /** @@ -145,7 +146,9 @@ public final class ApplicationRouter { Consult with delegate whether the route should still be presented. */ if delegate.applicationRouter(self, shouldPresent: route) { - delegate.applicationRouter(self, route: route, animated: animated) { coordinator in + let context = RoutePresentationContext(route: route, isAnimated: animated, metadata: metadata) + + delegate.applicationRouter(self, presentWithContext: context, animated: animated) { coordinator in /* Synchronize router when modal controllers are removed by swipe. */ @@ -276,7 +279,7 @@ public final class ApplicationRouter { switch pendingRoute.operation { case let .present(route): - presentRoute(route, animated: pendingRoute.animated) { result in + presentRoute(route, animated: pendingRoute.animated, metadata: pendingRoute.metadata) { result in switch result { case .success, .drop: self.finishPendingRoute(pendingRoute) @@ -373,3 +376,13 @@ public final class ApplicationRouter { } } } + +extension ApplicationRouter { + public func presentAlert(route: RouteType, metadata: Any) { + enqueue(PendingRoute( + operation: .present(route), + animated: true, + metadata: metadata + )) + } +} diff --git a/ios/Routing/Router/ApplicationRouterDelegate.swift b/ios/Routing/Router/ApplicationRouterDelegate.swift index ecccb154158d..a98870d30383 100644 --- a/ios/Routing/Router/ApplicationRouterDelegate.swift +++ b/ios/Routing/Router/ApplicationRouterDelegate.swift @@ -19,7 +19,7 @@ public protocol ApplicationRouterDelegate: AnyObject { */ func applicationRouter( _ router: ApplicationRouter, - route: RouteType, + presentWithContext context: RoutePresentationContext, animated: Bool, completion: @escaping (Coordinator) -> Void ) diff --git a/ios/Routing/Router/ApplicationRouterTypes.swift b/ios/Routing/Router/ApplicationRouterTypes.swift index 78bef90da48c..26eabee9cd91 100644 --- a/ios/Routing/Router/ApplicationRouterTypes.swift +++ b/ios/Routing/Router/ApplicationRouterTypes.swift @@ -11,9 +11,16 @@ import Foundation /** Struct describing a routing request for presentation or dismissal. */ -struct PendingRoute: Equatable { +struct PendingRoute { var operation: RouteOperation var animated: Bool + var metadata: Any? +} + +extension PendingRoute: Equatable { + static func == (lhs: PendingRoute, rhs: PendingRoute) -> Bool { + lhs.operation == rhs.operation + } } /** @@ -161,6 +168,32 @@ public struct RouteDismissalContext { public var isAnimated: Bool } +/** + Struct holding information used by delegate to perform presentation of a specific route. + */ +public struct RoutePresentationContext { + /** + Route that's being presented. + */ + public var route: RouteType + + /** + Whether transition is animated. + */ + public var isAnimated: Bool + + /** + Metadata associated with the route. + */ + public var metadata: Any? +} + +extension RoutePresentationContext: Equatable { + public static func == (lhs: RoutePresentationContext, rhs: RoutePresentationContext) -> Bool { + lhs.route == rhs.route + } +} + /** Struct holding information used by delegate to perform sub-navigation of the route in subject. */ diff --git a/ios/RoutingTests/RouterBlockDelegate.swift b/ios/RoutingTests/RouterBlockDelegate.swift index b1c1f5890837..977454b7e42b 100644 --- a/ios/RoutingTests/RouterBlockDelegate.swift +++ b/ios/RoutingTests/RouterBlockDelegate.swift @@ -10,7 +10,7 @@ import Foundation import Routing class RouterBlockDelegate: ApplicationRouterDelegate { - var handleRoute: ((RouteType, Bool, (Coordinator) -> Void) -> Void)? + var handleRoute: ((RoutePresentationContext, Bool, (Coordinator) -> Void) -> Void)? var handleDismiss: ((RouteDismissalContext, () -> Void) -> Void)? var shouldPresent: ((RouteType) -> Bool)? var shouldDismiss: ((RouteDismissalContext) -> Bool)? @@ -18,11 +18,11 @@ class RouterBlockDelegate: ApplicationRouterDelegat func applicationRouter( _ router: ApplicationRouter, - route: RouteType, + presentWithContext context: RoutePresentationContext, animated: Bool, completion: @escaping (Coordinator) -> Void ) { - handleRoute?(route, animated, completion) ?? completion(Coordinator()) + handleRoute?(context, animated, completion) ?? completion(Coordinator()) } func applicationRouter(