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..f4006fa9d7d9 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(AlertPresentation, Coordinator) /** 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..c2d5ffdece59 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -48,7 +48,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 +133,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 +171,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { ) let presentation = AlertPresentation( + id: "account-device-info-alert", message: message, buttons: [AlertAction( title: NSLocalizedString( @@ -176,6 +184,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..0e63b60beddf 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -155,8 +155,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 let .alert(presentation, context): + presentAlert(presentation: presentation, context: context, animated: animated, completion: completion) } } @@ -564,11 +564,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 +588,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) @@ -643,18 +642,20 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentAlert( presentation: AlertPresentation, + context: Coordinator, animated: Bool, completion: @escaping (Coordinator) -> Void ) { let coordinator = AlertCoordinator(presentation: presentation) coordinator.didFinish = { [weak self] in - self?.router.dismiss(.alert(presentation)) + self?.router.dismiss(.alert(presentation, context)) } coordinator.start() - presentChild(coordinator, animated: animated) { + let context = (context as? (any Presenting)) + presentChild(coordinator, context: context, animated: animated) { completion(coordinator) } } diff --git a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift index 7d3a65df5bed..db919eb3acb3 100644 --- a/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/InAppPurchaseCoordinator.swift @@ -11,13 +11,17 @@ import Routing import StoreKit import UIKit -class InAppPurchaseCoordinator: Coordinator, Presentable { +class InAppPurchaseCoordinator: Coordinator, Presenting, Presentable { private let navigationController: RootContainerViewController private let interactor: InAppPurchaseInteractor var didFinish: ((InAppPurchaseCoordinator) -> Void)? var didCancel: ((InAppPurchaseCoordinator) -> Void)? + var presentationContext: UIViewController { + navigationController + } + var presentedViewController: UIViewController { navigationController } @@ -50,6 +54,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 +74,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..abadfb4ea9dd 100644 --- a/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/SettingsCoordinator.swift @@ -159,13 +159,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..a9dccec57861 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -102,6 +102,7 @@ extension WelcomeCoordinator: WelcomeViewControllerDelegate { ) let presentation = AlertPresentation( + id: "welcome-device-name-alert", icon: .info, message: message, buttons: [ @@ -117,7 +118,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 +152,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..a30d0695a915 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresentation.swift @@ -7,6 +7,7 @@ // import Foundation +import Routing struct AlertAction { let title: String @@ -15,7 +16,7 @@ struct AlertAction { } struct AlertPresentation: Identifiable, CustomDebugStringConvertible { - let id = UUID() + let id: String var header: String? var icon: AlertIcon? @@ -24,7 +25,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..8c6164c5613b 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift @@ -9,9 +9,13 @@ 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?.present(.alert(presentation, context), animated: animated) + } + + func dismissAlert(presentation: AlertPresentation, animated: Bool) { + context.applicationRouter?.dismiss(.alert(presentation, context), animated: animated) } } 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..39fe0c2b3603 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: [ @@ -131,7 +132,11 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel message = NSLocalizedString( "PREFERENCES_CONTENT_BLOCKERS_GENERAL", tableName: "ContentBlockers", - value: "When this feature is enabled it stops the device from contacting certain domains or websites known for distributing ads, malware, trackers and more. This might cause issues on certain websites, services, and programs.", + value: """ + When this feature is enabled it stops the device from contacting certain \ + domains or websites known for distributing ads, malware, trackers and more. \ + This might cause issues on certain websites, services, and programs. + """, comment: "" ) @@ -139,14 +144,16 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel message = NSLocalizedString( "PREFERENCES_CONTENT_BLOCKERS_MALWARE", tableName: "ContentBlockers", - value: "Warning: The malware blocker is not an anti-virus and should not be treated as such, this is just an extra layer of protection.", + value: """ + Warning: The malware blocker is not an anti-virus and should not be treated as such, \ + this is just an extra layer of protection. + """, comment: "" ) case .wireGuardPorts: let portsString = humanReadablePortRepresentation( - interactor.cachedRelays?.relays.wireguard - .portRanges ?? [] + interactor.cachedRelays?.relays.wireguard.portRanges ?? [] ) message = String( @@ -166,12 +173,15 @@ class PreferencesViewController: UITableViewController, PreferencesDataSourceDel ) #if DEBUG - case .wireGuardObfuscation: message = NSLocalizedString( "PREFERENCES_WIRE_GUARD_OBFUSCATION_GENERAL", tableName: "WireGuardObfuscation", - value: "Obfuscation hides the WireGuard traffic inside another protocol. It can be used to help circumvent censorship and other types of filtering, where a plain WireGuard connect would be blocked.", + value: """ + Obfuscation hides the WireGuard traffic inside another protocol. \ + It can be used to help circumvent censorship and other types of filtering, \ + where a plain WireGuard connect would be blocked. + """, comment: "" ) @@ -183,6 +193,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..c673de4e932c 100644 --- a/ios/Routing/Coordinator.swift +++ b/ios/Routing/Coordinator.swift @@ -108,6 +108,7 @@ extension Presenting { */ public func presentChild( _ child: some Presentable, + context: (any Presenting)? = nil, animated: Bool, configuration: ModalPresentationConfiguration = ModalPresentationConfiguration(), completion: (() -> Void)? = nil @@ -134,7 +135,7 @@ extension Presenting { addChild(child) - presentationContext.present( + (context?.presentationContext ?? presentationContext).present( child.presentedViewController, animated: animated, completion: completion