diff options
| author | Jon Petersson <jon.petersson@mullvad.net> | 2025-08-06 13:36:25 +0200 |
|---|---|---|
| committer | Jon Petersson <jon.petersson@mullvad.net> | 2025-08-06 13:36:25 +0200 |
| commit | 47a1c007f55c8351f01c7fa308f25d04b135e5ee (patch) | |
| tree | defc26abd7634e2eea4060484859b657f1ebfcc9 | |
| parent | b75055d5214f3f3136042c81a305131188fbd857 (diff) | |
| parent | 521c26810ec175c32debc577061f2fd64a27b99b (diff) | |
| download | mullvadvpn-47a1c007f55c8351f01c7fa308f25d04b135e5ee.tar.xz mullvadvpn-47a1c007f55c8351f01c7fa308f25d04b135e5ee.zip | |
Merge branch 'fix-swiftlin-warnings-ios'
20 files changed, 204 insertions, 135 deletions
diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 8729b4a656..408738d853 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -23,6 +23,7 @@ excluded: # case-sensitive paths to ignore during linting. Takes precedence ove - Build - Configurations - MullvadVPNScreenshots + - wireguard-apple allow_zero_lintable_files: false diff --git a/ios/MullvadREST/ApiHandlers/RESTError.swift b/ios/MullvadREST/ApiHandlers/RESTError.swift index d1c02fa835..28f65651b9 100644 --- a/ios/MullvadREST/ApiHandlers/RESTError.swift +++ b/ios/MullvadREST/ApiHandlers/RESTError.swift @@ -32,8 +32,8 @@ extension REST { case .createURLRequest: return "Failure to create URL request." - case .network: - return "Network error." + case let .network(error): + return "Network error due to \(error.localizedDescription)." case let .unhandledResponse(statusCode, serverResponse): var str = "Failure to handle server response: HTTP/\(statusCode)." diff --git a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift index ecfff2d930..d8ebda1921 100644 --- a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift +++ b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift @@ -9,7 +9,12 @@ import Foundation import MullvadTypes -extension URLSessionTask: Cancellable {} +struct URLSessionTaskWrapper: Cancellable { + let task: URLSessionTask + func cancel() { + task.cancel() + } +} public final class URLSessionTransport: RESTTransport { public var name: String { @@ -28,6 +33,6 @@ public final class URLSessionTransport: RESTTransport { ) -> Cancellable { let dataTask = urlSession.dataTask(with: request, completionHandler: completion) dataTask.resume() - return dataTask + return URLSessionTaskWrapper(task: dataTask) } } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift index 71f8e7cd16..dc2a67767a 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift @@ -60,6 +60,6 @@ public final class ShadowsocksTransport: RESTTransport { let dataTask = urlSession.dataTask(with: urlRequestCopy, completionHandler: completion) dataTask.resume() - return dataTask + return URLSessionTaskWrapper(task: dataTask) } } diff --git a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift index bb408ccf59..43c0fc6c7e 100644 --- a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift +++ b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift @@ -108,6 +108,6 @@ public final class URLSessionSocks5Transport: RESTTransport, Sendable { let dataTask = urlSession.dataTask(with: newRequest, completionHandler: completion) dataTask.resume() - return dataTask + return URLSessionTaskWrapper(task: dataTask) } } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 113776ad9f..3a91d75af9 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -5917,10 +5917,11 @@ outputFileListPaths = ( ); outputPaths = ( + "${DERIVED_FILE_DIR}/localization-cleanup-done.flag", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Run the following steps if the build configuration is NOT Debug\n# OR if the configuration is Staging (used for UITests).\nif [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$CONFIGURATION\" = \"Staging\" ]; then\n echo \"Removing non-English localizations for Release or Staging build\"\n find \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\" -type d -name \"*.lproj\" ! -name \"en.lproj\" -exec rm -r {} +\nfi\n"; + shellScript = "# Run the following steps if the build configuration is NOT Debug\n# OR if the configuration is Staging (used for UITests).\nif [ \"$CONFIGURATION\" != \"Debug\" ] || [ \"$CONFIGURATION\" = \"Staging\" ]; then\n echo \"Removing non-English localizations for Release or Staging build\"\n find \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\" -type d -name \"*.lproj\" ! -name \"en.lproj\" -exec rm -r {} +\nfi\n\n# Ensure the output directory exists\nmkdir -p \"${DERIVED_FILE_DIR}\"\n\n# Final output\necho \">> Creating output flag file at: ${DERIVED_FILE_DIR}/localization-cleanup-done.flag\"\ntouch \"${DERIVED_FILE_DIR}/localization-cleanup-done.flag\"\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index 228ba731f5..d3756aaf11 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -137,37 +137,18 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked devicesProxy: interactor.deviceProxy ), style: .deviceManagement, - onError: { title, error in - let errorDescription = if case let .network(urlError) = error as? REST.Error { - urlError.localizedDescription - } else { - error.localizedDescription - } - let presentation = AlertPresentation( - id: "device-management-error-alert", + onError: { [weak self] title, error in + self?.presentError( + "device-management-error-alert", title: title, - message: errorDescription, - buttons: [ - AlertAction( - title: NSLocalizedString( - "ERROR_ALERT_OK_ACTION", - tableName: "DeviceManagement", - value: "Got it!", - comment: "" - ), - style: .default - ), - ] + message: error.localizedDescription ) - - let presenter = AlertPresenter(context: self) - presenter.showAlert(presentation: presentation, animated: true) } ) ) controller.title = NSLocalizedString( "MANAGE_DEVICES_TITLE", - tableName: "Manage devices", + tableName: "DeviceManagement", value: "Manage devices", comment: "" ) @@ -178,16 +159,32 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked }) ) controller.navigationItem.rightBarButtonItem = doneButton - let subNavigationController = CustomNavigationController( - rootViewController: controller - ) + let subNavigationController = CustomNavigationController(rootViewController: controller) subNavigationController.navigationItem.largeTitleDisplayMode = .always subNavigationController.navigationBar.prefersLargeTitles = true - navigationController - .present( - subNavigationController, - animated: true - ) + navigationController.present(subNavigationController, animated: true) + } + + private func presentError(_ id: String, title: String, message: String) { + let presentation = AlertPresentation( + id: id, + title: title, + message: message, + buttons: [ + AlertAction( + title: NSLocalizedString( + "ERROR_ALERT_OK_ACTION", + tableName: "Common", + value: "Got it!", + comment: "" + ), + style: .default + ), + ] + ) + + let presenter = AlertPresenter(context: self) + presenter.showAlert(presentation: presentation, animated: true) } @MainActor diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift index 45eee26860..e1ac172e93 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift @@ -13,7 +13,7 @@ import Routing import UIKit @MainActor -class EditLocationsCoordinator: Coordinator, Presentable, Presenting, Sendable { +class EditLocationsCoordinator: Coordinator, Presentable, Presenting { private let navigationController: UINavigationController private let nodes: [LocationNode] private var subject: CurrentValueSubject<CustomListViewModel, Never> diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index 898a518411..b7435b0656 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -44,7 +44,6 @@ enum SettingsNavigationRoute: Equatable { } /// Top-level settings coordinator. -@MainActor final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsViewControllerDelegate, UINavigationControllerDelegate, Sendable { private let logger = Logger(label: "SettingsNavigationCoordinator") diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index c7fff5b197..137303b67c 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -138,15 +138,28 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchec func addPayment(_ payment: SKPayment, for accountNumber: String) { logger.debug("Validating account before the purchase.") + let productIdentifier = payment.productIdentifier + let quantity = payment.quantity + let requestData = payment.requestData + let applicationUsername = payment.applicationUsername + let simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox + // Validate account token before adding new payment to the queue. validateAccount(accountNumber: accountNumber) { error in + // Reconstruct a new SKMutablePayment with the same fields + let cloned = SKMutablePayment() + cloned.productIdentifier = productIdentifier + cloned.quantity = quantity + cloned.requestData = requestData + cloned.applicationUsername = applicationUsername + cloned.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox + if let error { self.logger.error("Failed to validate the account. Payment is ignored.") - let event = StorePaymentEvent.failure( StorePaymentFailure( transaction: nil, - payment: payment, + payment: cloned, accountNumber: accountNumber, error: error ) @@ -158,8 +171,8 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchec } else { self.logger.debug("Add payment to the queue.") - self.associateAccountNumber(accountNumber, and: payment) - self.paymentQueue.add(payment) + self.associateAccountNumber(accountNumber, and: cloned) + self.paymentQueue.add(cloned) } } } diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index 1e76a42e9f..39831b9464 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -14,7 +14,7 @@ protocol SettingsCellEventHandler { } @MainActor -final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable { +final class SettingsCellFactory: @preconcurrency CellFactoryProtocol { let tableView: UITableView var delegate: SettingsCellEventHandler? var viewModel: SettingsViewModel diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift index 783cac1ee1..9348e198aa 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsViewController.swift @@ -11,7 +11,7 @@ import MullvadSettings import Routing import UIKit -protocol SettingsViewControllerDelegate: AnyObject, Sendable { +protocol SettingsViewControllerDelegate: AnyObject { func settingsViewControllerDidFinish(_ controller: SettingsViewController) func settingsViewController( _ controller: SettingsViewController, diff --git a/ios/MullvadVPN/Views/MullvadAlert.swift b/ios/MullvadVPN/Views/MullvadAlert.swift index 7f97861122..bad2b8a08e 100644 --- a/ios/MullvadVPN/Views/MullvadAlert.swift +++ b/ios/MullvadVPN/Views/MullvadAlert.swift @@ -31,57 +31,85 @@ struct AlertModifier: ViewModifier { func body(content: Content) -> some View { content .fullScreenCover(item: $alert) { alert in - VStack { - Spacer() - VStack(spacing: 16) { - switch alert.type { - case .error, .warning: - Image.mullvadIconAlert - .resizable() - .frame(width: 48, height: 48) - } - HStack { - Text(alert.message) - .font(.mullvadSmall) - .foregroundColor(.mullvadTextPrimary.opacity(0.6)) - Spacer() - } - VStack(spacing: 16) { - if let action = alert.action { - MainButton( - text: action.title, - style: action.type, - action: { - Task { - loading = true - await action.handler() - loading = false - } - } - ) - .accessibilityIdentifier(action.identifier) - } - MainButton( - text: alert.dismissButtonTitle, - style: .default, - action: { self.alert = nil } - ) - } - } - .padding() - .background(Color.mullvadBackground) - .cornerRadius(8) - Spacer() - } - .accessibilityElement(children: .contain) - .accessibilityIdentifier(.alertContainerView) - .padding() - .background(ClearBackgroundView()) + alertView(for: alert) } .transaction { $0.disablesAnimations = true } } + + @ViewBuilder + private func alertView(for alert: MullvadAlert) -> some View { + VStack { + Spacer() + alertContent(for: alert) + Spacer() + } + .accessibilityElement(children: .contain) + .accessibilityIdentifier(.alertContainerView) + .padding() + .background(ClearBackgroundView()) + } + + @ViewBuilder + private func alertContent(for alert: MullvadAlert) -> some View { + VStack(spacing: 16) { + alertIcon(for: alert.type) + alertMessage(alert.message) + VStack(spacing: 16) { + alertAction(for: alert.action) + alertAction(for: MullvadAlert.Action( + type: .default, + title: alert.dismissButtonTitle, + identifier: nil, + handler: { self.alert = nil } + )) + } + } + .padding() + .background(Color.mullvadBackground) + .cornerRadius(8) + } + + @ViewBuilder + private func alertIcon(for type: MullvadAlert.AlertType) -> some View { + switch type { + case .error, .warning: + Image.mullvadIconAlert + .resizable() + .frame(width: 48, height: 48) + } + } + + @ViewBuilder + private func alertMessage(_ message: LocalizedStringKey) -> some View { + HStack { + Text(message) + .font(.mullvadSmall) + .foregroundColor(.mullvadTextPrimary.opacity(0.6)) + Spacer() + } + } + + @ViewBuilder + private func alertAction(for action: MullvadAlert.Action?) -> some View { + if let action = action { + MainButton( + text: action.title, + style: action.type, + action: { + Task { + loading = true + await action.handler() + loading = false + } + } + ) + .accessibilityIdentifier(action.identifier) + } else { + EmptyView() + } + } } extension View { diff --git a/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift index 2098a06b97..555411bea9 100644 --- a/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift +++ b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift @@ -14,7 +14,7 @@ class SpinnerActivityIndicatorView: UIView { private static let animationDuration = 0.6 @MainActor - enum Style: Sendable { + enum Style { case small, medium, large, custom var intrinsicSize: CGSize { diff --git a/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift b/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift index a86bebbd54..8d04e5d4f6 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/NEProviderStopReason+Debug.swift @@ -9,9 +9,11 @@ import Foundation import NetworkExtension -extension NEProviderStopReason: CustomStringConvertible { +struct ProviderStopReasonWrapper: CustomStringConvertible { + let reason: NEProviderStopReason + public var description: String { - switch self { + switch reason { case .none: return "none" case .userInitiated: @@ -49,7 +51,7 @@ extension NEProviderStopReason: CustomStringConvertible { case .internalError: return "internal error" @unknown default: - return "unknown value (\(rawValue))" + return "unknown value (\(reason.rawValue))" } } } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 68b09095a8..e949a36709 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -180,7 +180,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } override func stopTunnel(with reason: NEProviderStopReason) async { - providerLogger.debug("stopTunnel: \(reason)") + providerLogger.debug("stopTunnel: \(ProviderStopReasonWrapper(reason: reason))") stopObservingActorState() diff --git a/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift index fce388c9a0..89d4312fd9 100644 --- a/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift +++ b/ios/PacketTunnel/WireGuardAdapter/WireGuardAdapterError+Localization.swift @@ -9,9 +9,11 @@ import Foundation import WireGuardKit -extension WireGuardAdapterError: LocalizedError { +struct WireGuardAdapterErrorWrapper: LocalizedError { + let error: WireGuardAdapterError + public var errorDescription: String? { - switch self { + switch error { case .cannotLocateTunnelFileDescriptor: return "Failure to locate tunnel file descriptor." diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 7cd230e2fb..f1deb3f40b 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -85,21 +85,23 @@ public actor PacketTunnelActor { */ private nonisolated func consumeEvents(channel: EventChannel) { Task.detached { [weak self] in + guard let self else { return } for await event in channel { - guard let self else { return } + await self.handleEvent(event) + } + } + } - self.logger.debug("Received event: \(event.logFormat())") + private func handleEvent(_ event: Event) async { + self.logger.debug("Received event: \(event.logFormat())") - let effects = await self.runReducer(event) + let effects = self.runReducer(event) - for effect in effects { - await executeEffect(effect) - } - } + for effect in effects { + await executeEffect(effect) } } - // swiftlint:disable:next function_body_length func executeEffect(_ effect: Effect) async { switch effect { case .startDefaultPathObserver: @@ -113,46 +115,65 @@ public actor PacketTunnelActor { case let .updateTunnelMonitorPath(networkPath): handleDefaultPathChange(networkPath) case let .startConnection(nextRelays): - do { - try await tryStart(nextRelays: nextRelays) - } catch { - logger.error(error: error, message: "Failed to start the tunnel.") - await setErrorStateInternal(with: error) - } + await handleStartConnection(nextRelays: nextRelays) case let .restartConnection(nextRelays, reason): - do { - try await tryStart(nextRelays: nextRelays, reason: reason) - } catch { - logger.error(error: error, message: "Failed to reconnect the tunnel.") - await setErrorStateInternal(with: error) - } + await handleRestartConnection(nextRelays: nextRelays, reason: reason) case let .reconnect(nextRelay): eventChannel.send(.reconnect(nextRelay)) case .stopTunnelAdapter: - do { - try await tunnelAdapter.stop() - } catch { - logger.error(error: error, message: "Failed to stop adapter.") - } - state = .disconnected + await handleStopTunnelAdapter() case let .configureForErrorState(reason): await setErrorStateInternal(with: reason) case let .cacheActiveKey(lastKeyRotation): cacheActiveKey(lastKeyRotation: lastKeyRotation) case let .reconfigureForEphemeralPeer(configuration, configurationSemaphore): - do { - try await updateEphemeralPeerNegotiationState(configuration: configuration) - } catch { - logger.error(error: error, message: "Failed to reconfigure tunnel after each hop negotiation.") - await setErrorStateInternal(with: error) - } - configurationSemaphore.send() + await handleReconfigureForEphemeralPeer(configuration: configuration, semaphore: configurationSemaphore) case .connectWithEphemeralPeer: await connectWithEphemeralPeer() case .setDisconnectedState: self.state = .disconnected } } + + private func handleStartConnection(nextRelays: NextRelays) async { + do { + try await tryStart(nextRelays: nextRelays) + } catch { + logger.error(error: error, message: "Failed to start the tunnel.") + await setErrorStateInternal(with: error) + } + } + + private func handleRestartConnection(nextRelays: NextRelays, reason: ActorReconnectReason) async { + do { + try await tryStart(nextRelays: nextRelays, reason: reason) + } catch { + logger.error(error: error, message: "Failed to reconnect the tunnel.") + await setErrorStateInternal(with: error) + } + } + + private func handleStopTunnelAdapter() async { + do { + try await tunnelAdapter.stop() + } catch { + logger.error(error: error, message: "Failed to stop adapter.") + } + state = .disconnected + } + + private func handleReconfigureForEphemeralPeer( + configuration: EphemeralPeerNegotiationState, + semaphore: OneshotChannel + ) async { + do { + try await updateEphemeralPeerNegotiationState(configuration: configuration) + } catch { + logger.error(error: error, message: "Failed to reconfigure tunnel after each hop negotiation.") + await setErrorStateInternal(with: error) + } + semaphore.send() + } } // MARK: - diff --git a/ios/Routing/Coordinator.swift b/ios/Routing/Coordinator.swift index dd951329c0..bbe8c0ae9b 100644 --- a/ios/Routing/Coordinator.swift +++ b/ios/Routing/Coordinator.swift @@ -16,7 +16,7 @@ import UIKit more manageable and reusable. */ @MainActor -open class Coordinator: NSObject, Sendable { +open class Coordinator: NSObject { /// Private trace log. private lazy var logger = Logger(label: "\(Self.self)") diff --git a/ios/Routing/Router/ApplicationRouter.swift b/ios/Routing/Router/ApplicationRouter.swift index 0dde76f426..6fb49325d6 100644 --- a/ios/Routing/Router/ApplicationRouter.swift +++ b/ios/Routing/Router/ApplicationRouter.swift @@ -14,7 +14,7 @@ import UIKit Main application router. */ @MainActor -public final class ApplicationRouter<RouteType: AppRouteProtocol>: Sendable { +public final class ApplicationRouter<RouteType: AppRouteProtocol> { nonisolated(unsafe) private let logger = Logger(label: "ApplicationRouter") private(set) var modalStack: [RouteType.RouteGroupType] = [] |
