From a24fed8a81059f0b60d32f3131e9206ae14628c7 Mon Sep 17 00:00:00 2001 From: Bug Magnet Date: Wed, 4 Dec 2024 14:43:23 +0100 Subject: [PATCH] Enable compilation with Swift 6 for most targets --- ios/MullvadLogging/LogFileOutputStream.swift | 2 +- ios/MullvadLogging/OSLogHandler.swift | 2 +- .../AccessMethodRepository+Stub.swift | 2 +- .../RESTRequestExecutor+Stubs.swift | 2 +- .../APIAvailabilityTestRequest.swift | 4 +- .../ApiHandlers/AddressCache.swift | 2 +- .../ApiHandlers/RESTAPIProxy.swift | 32 ++--- .../ApiHandlers/RESTAccessTokenManager.swift | 8 +- .../ApiHandlers/RESTAccountsProxy.swift | 8 +- .../ApiHandlers/RESTAuthenticationProxy.swift | 6 +- .../ApiHandlers/RESTAuthorization.swift | 4 +- .../ApiHandlers/RESTDefaults.swift | 2 +- .../ApiHandlers/RESTDevicesProxy.swift | 24 ++-- ios/MullvadREST/ApiHandlers/RESTError.swift | 6 +- .../ApiHandlers/RESTNetworkOperation.swift | 2 +- ios/MullvadREST/ApiHandlers/RESTProxy.swift | 16 +-- .../ApiHandlers/RESTRequestExecutor.swift | 6 +- .../ApiHandlers/RESTRequestHandler.swift | 4 +- .../ApiHandlers/RESTTaskIdentifier.swift | 2 +- .../SSLPinningURLSessionDelegate.swift | 4 +- .../ApiHandlers/ServerRelaysResponse.swift | 14 +- ios/MullvadREST/Relay/IPOverrideWrapper.swift | 2 +- .../NoRelaysSatisfyingConstraintsError.swift | 4 +- ios/MullvadREST/Relay/RelayCache.swift | 6 +- .../Relay/RelaySelectorProtocol.swift | 4 +- .../Relay/RelaySelectorWrapper.swift | 2 +- ios/MullvadREST/RetryStrategy/Jittered.swift | 2 +- .../RetryStrategy/RetryStrategy.swift | 16 +-- .../Transport/AccessMethodIterator.swift | 2 +- .../Direct/URLSessionTransport.swift | 2 +- .../EncryptedDNS/EncryptedDNSTransport.swift | 8 +- ios/MullvadREST/Transport/RESTTransport.swift | 5 +- .../Transport/RESTTransportProvider.swift | 2 +- .../ShadowsocksConfiguration.swift | 2 +- .../ShadowsocksConfigurationCache.swift | 4 +- .../Shadowsocks/ShadowsocksLoader.swift | 8 +- .../ShadowsocksRelaySelector.swift | 2 +- .../Shadowsocks/ShadowsocksTransport.swift | 2 +- .../Socks5/NWConnection+Extensions.swift | 2 +- .../Transport/Socks5/Socks5AddressType.swift | 2 +- .../Socks5/Socks5Authentication.swift | 6 +- .../Socks5/Socks5Configuration.swift | 2 +- .../Socks5/Socks5ConnectNegotiation.swift | 4 +- .../Transport/Socks5/Socks5Connection.swift | 8 +- .../Socks5/Socks5DataStreamHandler.swift | 4 +- .../Transport/Socks5/Socks5Endpoint.swift | 4 +- .../Socks5/Socks5EndpointReader.swift | 8 +- .../Transport/Socks5/Socks5Error.swift | 2 +- .../Socks5/Socks5ForwardingProxy.swift | 16 +-- .../Socks5/Socks5HandshakeNegotiation.swift | 6 +- .../Transport/Socks5/Socks5StatusCode.swift | 2 +- .../Socks5/URLSessionSocks5Transport.swift | 12 +- .../Transport/TransportProvider.swift | 12 +- .../Transport/TransportStrategy.swift | 2 +- ios/MullvadRESTTests/Mocks/AnyTransport.swift | 8 +- .../Mocks/RESTTransportStub.swift | 2 +- .../Mocks/TimeServerProxy.swift | 2 +- .../RequestExecutorTests.swift | 3 +- ios/MullvadRustRuntime/ShadowSocksProxy.swift | 2 +- .../UnsafeListener.swift | 2 +- .../AccessMethodRepository.swift | 2 +- .../AccessMethodRepositoryProtocol.swift | 2 +- ios/MullvadSettings/DAITASettings.swift | 6 +- ios/MullvadSettings/DNSSettings.swift | 4 +- ios/MullvadSettings/DeviceState.swift | 2 +- ios/MullvadSettings/IPOverride.swift | 4 +- .../IPOverrideRepository.swift | 6 +- .../KeychainSettingsStore.swift | 2 +- ios/MullvadSettings/Migration.swift | 2 +- ios/MullvadSettings/MigrationManager.swift | 6 +- ios/MullvadSettings/MultihopSettings.swift | 4 +- .../QuantumResistanceSettings.swift | 2 +- ios/MullvadSettings/SettingsManager.swift | 6 +- ios/MullvadSettings/SettingsStore.swift | 4 +- .../ShadowsocksCipherOptions.swift | 4 +- ios/MullvadSettings/StoredAccountData.swift | 2 +- ios/MullvadSettings/StoredDeviceData.swift | 4 +- ios/MullvadSettings/StoredWgKeyData.swift | 4 +- ios/MullvadSettings/TunnelSettings.swift | 4 +- .../TunnelSettingsPropagator.swift | 8 +- .../TunnelSettingsStrategy.swift | 4 +- .../TunnelSettingsUpdate.swift | 2 +- ios/MullvadSettings/TunnelSettingsV1.swift | 2 +- ios/MullvadSettings/TunnelSettingsV6.swift | 2 +- .../WireGuardObfuscationSettings.swift | 10 +- ios/MullvadTypes/AnyIPEndpoint.swift | 2 +- ios/MullvadTypes/Cancellable.swift | 2 +- ios/MullvadTypes/IPv4Endpoint.swift | 2 +- ios/MullvadTypes/IPv6Endpoint.swift | 2 +- ios/MullvadTypes/Location.swift | 2 +- ios/MullvadTypes/MullvadEndpoint.swift | 2 +- ios/MullvadTypes/ObserverList.swift | 8 +- ios/MullvadTypes/Promise.swift | 2 +- .../NetworkExtension+HiddenSymbols.swift | 17 +++ ios/MullvadTypes/RESTTypes.swift | 6 +- ios/MullvadTypes/RelayConstraints.swift | 2 +- ios/MullvadTypes/RelayLocation.swift | 6 +- ios/MullvadTypes/TransportLayer.swift | 2 +- ios/MullvadVPN.xcodeproj/project.pbxproj | 57 ++++++-- .../ProxyConfigurationTester.swift | 2 +- .../ProxyConfigurationTesterProtocol.swift | 2 +- .../AddressCacheTracker.swift | 4 +- ios/MullvadVPN/AppDelegate.swift | 135 ++++++++++-------- .../Classes/AccessbilityIdentifier.swift | 1 + .../Classes/AutomaticKeyboardResponder.swift | 1 + .../Classes/ConsolidatedApplicationLog.swift | 6 +- .../Root/RootContainerViewController.swift | 9 +- .../Coordinators/AccountCoordinator.swift | 15 +- .../AccountDeletionCoordinator.swift | 6 +- .../AddCreditSucceededCoordinator.swift | 2 +- .../Coordinators/ApplicationCoordinator.swift | 43 ++++-- .../CreateAccountVoucherCoordinator.swift | 2 +- .../AddCustomListCoordinator.swift | 2 +- .../CustomLists/AddLocationsCoordinator.swift | 2 +- .../CustomLists/AddLocationsDataSource.swift | 4 +- .../AddLocationsViewController.swift | 2 +- .../CustomListCellConfiguration.swift | 1 + .../CustomListDataSourceConfiguration.swift | 1 + .../EditCustomListCoordinator.swift | 6 +- .../EditLocationsCoordinator.swift | 7 +- .../Coordinators/LocationCoordinator.swift | 4 +- .../Coordinators/LoginCoordinator.swift | 19 +-- .../Coordinators/OutOfTimeCoordinator.swift | 4 +- .../ProfileVoucherCoordinator.swift | 4 +- .../Coordinators/RelayFilterCoordinator.swift | 2 +- .../Coordinators/SafariCoordinator.swift | 5 +- .../Add/AddAccessMethodCoordinator.swift | 2 +- .../APIAccess/Cells/TextCellContentView.swift | 5 +- .../Common/ShadowsocksSectionHandler.swift | 1 + .../Common/SocksSectionHandler.swift | 6 + ...CurrentValueSubject+UIActionBindings.swift | 4 +- .../Edit/EditAccessMethodCoordinator.swift | 4 +- .../Edit/EditAccessMethodInteractor.swift | 4 +- .../MethodSettingsCellConfiguration.swift | 3 +- ...ethodSettingsDataSourceConfiguration.swift | 2 + .../MethodSettingsViewController.swift | 4 +- ...ProxyConfigurationInteractorProtocol.swift | 2 +- .../List/ListAccessMethodCoordinator.swift | 2 +- .../Pickers/AccessMethodProtocolPicker.swift | 1 + .../Pickers/ShadowsocksCipherPicker.swift | 1 + .../IPOverride/IPOverrideCoordinator.swift | 2 +- .../Settings/SettingsCoordinator.swift | 27 +++- .../SettingsViewControllerFactory.swift | 1 + .../SetupAccountCompletedCoordinator.swift | 2 +- .../Coordinators/VPNSettingsCoordinator.swift | 2 +- .../Coordinators/WelcomeCoordinator.swift | 2 +- .../Extensions/UITextField+Appearance.swift | 2 + .../Extensions/UIView+AutoLayoutBuilder.swift | 7 +- ios/MullvadVPN/Extensions/View+Size.swift | 4 +- ...countExpiryInAppNotificationProvider.swift | 3 +- ...ountExpirySystemNotificationProvider.swift | 3 +- ...teredDeviceInAppNotificationProvider.swift | 2 +- .../TunnelStatusNotificationProvider.swift | 2 +- .../Notifications/NotificationManager.swift | 2 +- .../Notifications/NotificationProvider.swift | 4 +- .../Operations/ProductsRequestOperation.swift | 2 +- .../RelayCacheTracker/RelayCacheTracker.swift | 10 +- ios/MullvadVPN/SceneDelegate.swift | 2 +- .../SimulatorTunnelInfo.swift | 3 +- .../SimulatorTunnelProvider.swift | 8 +- .../SimulatorTunnelProviderHost.swift | 20 +-- .../SimulatorTunnelProviderManager.swift | 2 +- .../SimulatorVPNConnection.swift | 2 +- .../SendStoreReceiptOperation.swift | 3 +- .../StorePaymentBlockObserver.swift | 2 +- .../StorePaymentEvent.swift | 6 +- .../StorePaymentManager.swift | 12 +- .../StorePaymentManagerDelegate.swift | 2 +- .../StorePaymentObserver.swift | 2 +- .../StoreTransactionLog.swift | 2 +- .../PacketTunnelTransport.swift | 2 +- .../BackgroundTaskProvider.swift | 8 +- .../LoadTunnelConfigurationOperation.swift | 2 +- .../MapConnectionStatusOperation.swift | 4 +- .../RedeemVoucherOperation.swift | 2 +- .../TunnelManager/RotateKeyOperation.swift | 4 +- .../SendTunnelProviderMessageOperation.swift | 2 +- .../TunnelManager/SetAccountOperation.swift | 38 +++-- .../TunnelManager/StartTunnelOperation.swift | 9 +- .../TunnelManager/StopTunnelOperation.swift | 2 +- .../TunnelManager/Tunnel+Messaging.swift | 8 +- ios/MullvadVPN/TunnelManager/Tunnel.swift | 4 +- .../TunnelManager/TunnelBlockObserver.swift | 2 +- .../TunnelManager/TunnelInteractor.swift | 2 +- .../TunnelManager/TunnelManager.swift | 48 ++++--- .../TunnelManager/TunnelObserver.swift | 2 +- .../TunnelManager/TunnelState.swift | 6 +- .../TunnelStatusBlockObserver.swift | 4 +- .../TunnelManager/TunnelStore.swift | 4 +- .../UpdateAccountDataOperation.swift | 2 +- .../UpdateDeviceDataOperation.swift | 4 +- .../TunnelManager/WgKeyRotation.swift | 4 +- .../Account/AccountInteractor.swift | 14 +- .../Account/AccountViewController.swift | 37 +++-- .../Account/PaymentAlertPresenter.swift | 7 +- .../AccountDeletionInteractor.swift | 2 +- .../AccountDeletionViewController.swift | 5 +- .../Alert/AlertPresenter.swift | 1 + .../SetupAccountCompletedController.swift | 4 +- .../InAppPurchaseInteractor.swift | 2 +- .../Welcome/WelcomeContentView.swift | 6 +- .../Welcome/WelcomeInteractor.swift | 2 +- .../Welcome/WelcomeViewController.swift | 4 +- .../DeviceManagementContentView.swift | 10 +- .../DeviceManagementInteractor.swift | 9 +- .../DeviceManagementViewController.swift | 54 ++++--- .../Login/LoginInteractor.swift | 8 +- .../Login/LoginViewController.swift | 4 +- .../OutOfTime/OutOfTimeInteractor.swift | 22 +-- .../OutOfTime/OutOfTimeViewController.swift | 45 ++++-- .../ProblemReportInteractor.swift | 8 +- .../ProblemReportReviewViewController.swift | 22 +-- .../ProblemReportViewController.swift | 4 +- .../RedeemVoucherInteractor.swift | 4 +- .../RedeemVoucherViewController.swift | 19 +-- .../RelayFilter/RelayFilterCellFactory.swift | 3 +- .../RelayFilter/RelayFilterView.swift | 8 +- .../LocationCellViewModel.swift | 2 +- .../SelectLocation/LocationDataSource.swift | 54 ++++--- .../SelectLocation/LocationNode.swift | 6 +- .../SelectLocation/LocationRelays.swift | 2 +- .../SelectLocation/LocationSection.swift | 2 +- .../LocationViewController.swift | 14 +- .../LocationViewControllerWrapper.swift | 2 +- .../SelectLocation/RelaySelection.swift | 2 +- .../Settings/SettingsCellFactory.swift | 3 +- .../Settings/SettingsDataSource.swift | 2 +- .../Settings/SettingsViewController.swift | 4 +- .../Tunnel/MapViewController.swift | 3 +- .../TunnelViewControllerInteractor.swift | 2 +- .../VPNSettings/CustomDNSCellFactory.swift | 3 +- .../VPNSettings/CustomDNSDataSource.swift | 2 +- .../VPNSettings/CustomDNSViewController.swift | 2 +- .../VPNSettings/VPNSettingsCellFactory.swift | 3 +- .../VPNSettings/VPNSettingsDataSource.swift | 2 +- .../VPNSettings/VPNSettingsInteractor.swift | 4 +- .../VPNSettingsViewController.swift | 2 +- ios/MullvadVPN/Views/CustomButton.swift | 2 +- ios/MullvadVPN/Views/CustomTextView.swift | 6 +- .../Views/SpinnerActivityIndicatorView.swift | 12 +- .../Shadowsocks/ShadowsocksLoaderTests.swift | 6 +- .../InMemorySettingsStore.swift | 2 +- .../Log/ConsolidatedApplicationLogTests.swift | 16 +-- .../MullvadVPN/TunnelManager/MockTunnel.swift | 2 +- .../TunnelManager/MockTunnelInteractor.swift | 2 +- .../StartTunnelOperationTests.swift | 2 +- .../TunnelManager/TunnelStore+Stubs.swift | 4 +- .../CustomListRepositoryTests.swift | 2 +- ios/Operations/AsyncBlockOperation.swift | 13 +- ios/Operations/AsyncOperation.swift | 4 +- ios/Operations/AsyncOperationQueue.swift | 4 +- ios/Operations/BackgroundObserver.swift | 6 + ios/Operations/GroupOperation.swift | 2 +- ios/Operations/OutputOperation.swift | 2 +- ios/Operations/ResultBlockOperation.swift | 14 +- ios/Operations/ResultOperation.swift | 6 +- .../AsyncBlockOperationTests.swift | 1 + .../AsyncResultBlockOperationTests.swift | 1 + .../OperationConditionTests.swift | 1 + .../DeviceCheck/DeviceCheckOperation.swift | 6 +- .../DeviceCheckRemoteService.swift | 6 +- .../DeviceCheckRemoteServiceProtocol.swift | 10 +- .../PacketTunnelPathObserver.swift | 4 +- .../PacketTunnelProvider.swift | 4 +- .../WireGuardAdapter/WgAdapter.swift | 6 +- ios/PacketTunnelCore/Actor/AnyTask.swift | 2 +- .../Actor/AutoCancellingTask.swift | 2 +- .../Actor/EphemeralPeerNegotiationState.swift | 8 +- .../Actor/ObservedState.swift | 8 +- .../Actor/PacketTunnelActor+Extensions.swift | 1 + .../Actor/PacketTunnelActor.swift | 6 +- .../Actor/PacketTunnelActorCommand.swift | 2 +- .../Actor/PacketTunnelActorReducer.swift | 4 +- .../Protocols/SettingsReaderProtocol.swift | 6 +- .../Protocols/TunnelAdapterProtocol.swift | 4 +- ios/PacketTunnelCore/Actor/StartOptions.swift | 4 +- ios/PacketTunnelCore/Actor/State.swift | 14 +- .../Actor/Task+Duration.swift | 2 +- ios/PacketTunnelCore/Actor/Timings.swift | 2 +- .../DefaultPathObserverProtocol.swift | 6 +- .../TunnelMonitor/TunnelMonitorProtocol.swift | 4 +- .../URLRequestProxy/ProxyURLResponse.swift | 6 +- .../Mocks/DefaultPathObserverFake.swift | 2 +- .../Mocks/TunnelAdapterDummy.swift | 2 +- .../Mocks/TunnelMonitorStub.swift | 2 +- ios/Routing/Coordinator.swift | 9 +- .../ModalPresentationConfiguration.swift | 1 + ...tationControllerDismissalInterceptor.swift | 2 +- ios/Routing/Router/AppRouteProtocol.swift | 4 +- ios/Routing/Router/ApplicationRouter.swift | 57 ++++---- .../Router/ApplicationRouterDelegate.swift | 8 +- .../Router/ApplicationRouterTypes.swift | 20 +-- ios/RoutingTests/RouterBlockDelegate.swift | 20 +-- ios/RoutingTests/RoutingTests.swift | 9 +- 294 files changed, 1117 insertions(+), 850 deletions(-) create mode 100644 ios/MullvadTypes/Protocols/NetworkExtension+HiddenSymbols.swift diff --git a/ios/MullvadLogging/LogFileOutputStream.swift b/ios/MullvadLogging/LogFileOutputStream.swift index 7f4eec3a0bb5..6d63d43b7713 100644 --- a/ios/MullvadLogging/LogFileOutputStream.swift +++ b/ios/MullvadLogging/LogFileOutputStream.swift @@ -13,7 +13,7 @@ import MullvadTypes /// the first place, or when writing to it. private let reopenFileLogInterval: Duration = .seconds(5) -class LogFileOutputStream: TextOutputStream { +class LogFileOutputStream: TextOutputStream, @unchecked Sendable { private let queue = DispatchQueue(label: "LogFileOutputStreamQueue", qos: .utility) private let baseFileURL: URL diff --git a/ios/MullvadLogging/OSLogHandler.swift b/ios/MullvadLogging/OSLogHandler.swift index 308e35124949..d9778e7f876b 100644 --- a/ios/MullvadLogging/OSLogHandler.swift +++ b/ios/MullvadLogging/OSLogHandler.swift @@ -22,7 +22,7 @@ public struct OSLogHandler: LogHandler { let category: String } - private static var osLogRegistry: [RegistryKey: OSLog] = [:] + nonisolated(unsafe) private static var osLogRegistry: [RegistryKey: OSLog] = [:] private static let registryLock = NSLock() private static func getOSLog(subsystem: String, category: String) -> OSLog { diff --git a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift index 7dbf88dbb50a..a8d889975297 100644 --- a/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift +++ b/ios/MullvadMockData/MullvadREST/AccessMethodRepository+Stub.swift @@ -9,7 +9,7 @@ import Combine import MullvadSettings -public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource { +public struct AccessMethodRepositoryStub: AccessMethodRepositoryDataSource, @unchecked Sendable { public var directAccess: PersistentAccessMethod public var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { diff --git a/ios/MullvadMockData/MullvadREST/RESTRequestExecutor+Stubs.swift b/ios/MullvadMockData/MullvadREST/RESTRequestExecutor+Stubs.swift index 25aef84e5bd0..c044269792d6 100644 --- a/ios/MullvadMockData/MullvadREST/RESTRequestExecutor+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/RESTRequestExecutor+Stubs.swift @@ -10,7 +10,7 @@ import Foundation import MullvadREST import MullvadTypes -struct RESTRequestExecutorStub: RESTRequestExecutor { +struct RESTRequestExecutorStub: RESTRequestExecutor { var success: (() -> Success)? func execute(completionHandler: @escaping (Result) -> Void) -> Cancellable { diff --git a/ios/MullvadREST/ApiHandlers/APIAvailabilityTestRequest.swift b/ios/MullvadREST/ApiHandlers/APIAvailabilityTestRequest.swift index 03c180c34929..8292ca7f45b2 100644 --- a/ios/MullvadREST/ApiHandlers/APIAvailabilityTestRequest.swift +++ b/ios/MullvadREST/ApiHandlers/APIAvailabilityTestRequest.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes extension REST { - public struct APIAvailabilityTestRequest { + public struct APIAvailabilityTestRequest: Sendable { let transport: RESTTransport public init(transport: RESTTransport) { @@ -21,7 +21,7 @@ extension REST { /// /// - Parameter completion: Completes with `nil` if the request was successful, and `Error` otherwise. /// - Returns: A cancellable token to cancel the request inflight. - public func makeRequest(completion: @escaping (Swift.Error?) -> Void) -> Cancellable { + public func makeRequest(completion: @escaping @Sendable (Swift.Error?) -> Void) -> Cancellable { do { let factory = RequestFactory( hostname: defaultAPIHostname, diff --git a/ios/MullvadREST/ApiHandlers/AddressCache.swift b/ios/MullvadREST/ApiHandlers/AddressCache.swift index 8db610fc4875..2d7a988c7a31 100644 --- a/ios/MullvadREST/ApiHandlers/AddressCache.swift +++ b/ios/MullvadREST/ApiHandlers/AddressCache.swift @@ -11,7 +11,7 @@ import MullvadLogging import MullvadTypes extension REST { - public final class AddressCache { + public final class AddressCache: @unchecked Sendable { /// Logger. private let logger = Logger(label: "AddressCache") diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift index 691781ef4ce5..23ec4d872d40 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -10,16 +10,16 @@ import Foundation import MullvadTypes import WireGuardKitTypes -public protocol APIQuerying { +public protocol APIQuerying: Sendable { func getAddressList( retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> ) -> Cancellable func getRelays( etag: String?, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func createApplePayment( @@ -30,19 +30,19 @@ public protocol APIQuerying { func sendProblemReport( _ body: REST.ProblemReportRequest, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func submitVoucher( voucherCode: String, accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable } extension REST { - public final class APIProxy: Proxy, APIQuerying { + public final class APIProxy: Proxy, APIQuerying, @unchecked Sendable { public init(configuration: AuthProxyConfiguration) { super.init( name: "APIProxy", @@ -57,7 +57,7 @@ extension REST { public func getAddressList( retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<[AnyIPEndpoint]> + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in try self.requestFactory.createRequest( @@ -84,7 +84,7 @@ extension REST { public func getRelays( etag: String?, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler { endpoint in var requestBuilder = try self.requestFactory.createRequestBuilder( @@ -240,7 +240,7 @@ extension REST { voucherCode: String, accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -283,16 +283,16 @@ extension REST { // MARK: - Response types - public enum ServerRelaysCacheResponse { + public enum ServerRelaysCacheResponse: Sendable { case notModified case newContent(_ etag: String?, _ rawData: Data) } - private struct CreateApplePaymentRequest: Encodable { + private struct CreateApplePaymentRequest: Encodable, Sendable { let receiptString: Data } - public enum CreateApplePaymentResponse { + public enum CreateApplePaymentResponse: Sendable { case noTimeAdded(_ expiry: Date) case timeAdded(_ timeAdded: Int, _ newExpiry: Date) @@ -322,12 +322,12 @@ extension REST { } } - private struct CreateApplePaymentRawResponse: Decodable { + private struct CreateApplePaymentRawResponse: Decodable, Sendable { let timeAdded: Int let newExpiry: Date } - public struct ProblemReportRequest: Encodable { + public struct ProblemReportRequest: Encodable, Sendable { public let address: String public let message: String public let log: String @@ -341,11 +341,11 @@ extension REST { } } - private struct SubmitVoucherRequest: Encodable { + private struct SubmitVoucherRequest: Encodable, Sendable { let voucherCode: String } - public struct SubmitVoucherResponse: Decodable { + public struct SubmitVoucherResponse: Decodable, Sendable { public let timeAdded: Int public let newExpiry: Date diff --git a/ios/MullvadREST/ApiHandlers/RESTAccessTokenManager.swift b/ios/MullvadREST/ApiHandlers/RESTAccessTokenManager.swift index d15bd6b9cb1e..6c5a59da4e3a 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAccessTokenManager.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAccessTokenManager.swift @@ -11,17 +11,17 @@ import MullvadLogging import MullvadTypes import Operations -public protocol RESTAccessTokenManagement { +public protocol RESTAccessTokenManagement: Sendable { func getAccessToken( accountNumber: String, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func invalidateAllTokens() } extension REST { - public final class AccessTokenManager: RESTAccessTokenManagement { + public final class AccessTokenManager: RESTAccessTokenManagement, @unchecked Sendable { private let logger = Logger(label: "REST.AccessTokenManager") private let operationQueue = AsyncOperationQueue.makeSerial() private let dispatchQueue = DispatchQueue(label: "REST.AccessTokenManager.dispatchQueue") @@ -34,7 +34,7 @@ extension REST { public func getAccessToken( accountNumber: String, - completionHandler: @escaping ProxyCompletionHandler + completionHandler: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable { let operation = ResultBlockOperation(dispatchQueue: dispatchQueue) { finish -> Cancellable in diff --git a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift index 568e24facec5..49c24bcd8f36 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAccountsProxy.swift @@ -9,10 +9,10 @@ import Foundation import MullvadTypes -public protocol RESTAccountHandling { +public protocol RESTAccountHandling: Sendable { func createAccount( retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func getAccountData(accountNumber: String) -> any RESTRequestExecutor @@ -25,7 +25,7 @@ public protocol RESTAccountHandling { } extension REST { - public final class AccountsProxy: Proxy, RESTAccountHandling { + public final class AccountsProxy: Proxy, RESTAccountHandling, @unchecked Sendable { public init(configuration: AuthProxyConfiguration) { super.init( name: "AccountsProxy", @@ -135,7 +135,7 @@ extension REST { } } - public struct NewAccountData: Decodable { + public struct NewAccountData: Decodable, Sendable { public let id: String public let expiry: Date public let maxPorts: Int diff --git a/ios/MullvadREST/ApiHandlers/RESTAuthenticationProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAuthenticationProxy.swift index cbcd6fdc0702..f0b3c5913313 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAuthenticationProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAuthenticationProxy.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes extension REST { - public final class AuthenticationProxy: Proxy { + public final class AuthenticationProxy: Proxy, @unchecked Sendable { public init(configuration: ProxyConfiguration) { super.init( name: "AuthenticationProxy", @@ -57,12 +57,12 @@ extension REST { } } - public struct AccessTokenData: Decodable { + public struct AccessTokenData: Decodable, Sendable { let accessToken: String let expiry: Date } - private struct AccessTokenRequest: Encodable { + private struct AccessTokenRequest: Encodable, Sendable { let accountNumber: String } } diff --git a/ios/MullvadREST/ApiHandlers/RESTAuthorization.swift b/ios/MullvadREST/ApiHandlers/RESTAuthorization.swift index 340bd63f5c63..1351d9f0cc36 100644 --- a/ios/MullvadREST/ApiHandlers/RESTAuthorization.swift +++ b/ios/MullvadREST/ApiHandlers/RESTAuthorization.swift @@ -11,7 +11,7 @@ import MullvadTypes import Operations protocol RESTAuthorizationProvider { - func getAuthorization(completion: @escaping (Result) -> Void) + func getAuthorization(completion: @escaping @Sendable (Result) -> Void) -> Cancellable } @@ -28,7 +28,7 @@ extension REST { } func getAuthorization( - completion: @escaping (Result) + completion: @escaping @Sendable (Result) -> Void ) -> Cancellable { accessTokenManager.getAccessToken(accountNumber: accountNumber) { result in diff --git a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift index 8875096931e3..c56deb5ca3a9 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDefaults.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDefaults.swift @@ -13,7 +13,7 @@ import MullvadTypes extension REST { /// The API hostname and endpoint are defined in the Info.plist of the MullvadREST framework bundle /// This is due to not being able to target `Bundle.main` from a Unit Test environment as it gets its own bundle that would not contain the above variables. - private static let infoDictionary = Bundle(for: AddressCache.self).infoDictionary! + nonisolated(unsafe) private static let infoDictionary = Bundle(for: AddressCache.self).infoDictionary! /// Default API hostname. public static let defaultAPIHostname = infoDictionary["ApiHostName"] as! String diff --git a/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift b/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift index dd49179c3037..402767a6ec1a 100644 --- a/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTDevicesProxy.swift @@ -8,34 +8,34 @@ import Foundation import MullvadTypes -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes -public protocol DeviceHandling { +public protocol DeviceHandling: Sendable { func getDevice( accountNumber: String, identifier: String, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func getDevices( accountNumber: String, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler<[Device]> + completion: @escaping @Sendable ProxyCompletionHandler<[Device]> ) -> Cancellable func createDevice( accountNumber: String, request: REST.CreateDeviceRequest, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func deleteDevice( accountNumber: String, identifier: String, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable func rotateDeviceKey( @@ -43,12 +43,12 @@ public protocol DeviceHandling { identifier: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable } extension REST { - public final class DevicesProxy: Proxy, DeviceHandling { + public final class DevicesProxy: Proxy, DeviceHandling, @unchecked Sendable { public init(configuration: AuthProxyConfiguration) { super.init( name: "DevicesProxy", @@ -161,7 +161,7 @@ extension REST { accountNumber: String, request: CreateDeviceRequest, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -262,7 +262,7 @@ extension REST { identifier: String, publicKey: PublicKey, retryStrategy: REST.RetryStrategy, - completion: @escaping ProxyCompletionHandler + completion: @escaping @Sendable ProxyCompletionHandler ) -> Cancellable { let requestHandler = AnyRequestHandler( createURLRequest: { endpoint, authorization in @@ -310,7 +310,7 @@ extension REST { } } - public struct CreateDeviceRequest: Encodable { + public struct CreateDeviceRequest: Encodable, Sendable { let publicKey: PublicKey let hijackDNS: Bool @@ -332,7 +332,7 @@ extension REST { } } - private struct RotateDeviceKeyRequest: Encodable { + private struct RotateDeviceKeyRequest: Encodable, Sendable { let publicKey: PublicKey private enum CodingKeys: String, CodingKey { diff --git a/ios/MullvadREST/ApiHandlers/RESTError.swift b/ios/MullvadREST/ApiHandlers/RESTError.swift index 1fdb255b81b5..5fdd88e0d2b5 100644 --- a/ios/MullvadREST/ApiHandlers/RESTError.swift +++ b/ios/MullvadREST/ApiHandlers/RESTError.swift @@ -11,7 +11,7 @@ import MullvadTypes extension REST { /// An error type returned by REST API classes. - public enum Error: LocalizedError, WrappingError { + public enum Error: LocalizedError, WrappingError, Sendable { /// Failure to create URL request. case createURLRequest(Swift.Error) @@ -80,7 +80,7 @@ extension REST { } } - public struct ServerErrorResponse: Decodable { + public struct ServerErrorResponse: Decodable, Sendable { public let code: ServerResponseCode public let detail: String? @@ -103,7 +103,7 @@ extension REST { } } - public struct ServerResponseCode: RawRepresentable, Equatable { + public struct ServerResponseCode: RawRepresentable, Equatable, Sendable { public static let invalidAccount = ServerResponseCode(rawValue: "INVALID_ACCOUNT") public static let keyLimitReached = ServerResponseCode(rawValue: "KEY_LIMIT_REACHED") public static let publicKeyNotFound = ServerResponseCode(rawValue: "PUBKEY_NOT_FOUND") diff --git a/ios/MullvadREST/ApiHandlers/RESTNetworkOperation.swift b/ios/MullvadREST/ApiHandlers/RESTNetworkOperation.swift index 0321c7be79de..9d0f074f6788 100644 --- a/ios/MullvadREST/ApiHandlers/RESTNetworkOperation.swift +++ b/ios/MullvadREST/ApiHandlers/RESTNetworkOperation.swift @@ -12,7 +12,7 @@ import MullvadTypes import Operations extension REST { - class NetworkOperation: ResultOperation { + class NetworkOperation: ResultOperation, @unchecked Sendable { private let requestHandler: RESTRequestHandler private let responseHandler: any RESTResponseHandler diff --git a/ios/MullvadREST/ApiHandlers/RESTProxy.swift b/ios/MullvadREST/ApiHandlers/RESTProxy.swift index 49d4fc8968b2..f5815b669084 100644 --- a/ios/MullvadREST/ApiHandlers/RESTProxy.swift +++ b/ios/MullvadREST/ApiHandlers/RESTProxy.swift @@ -10,10 +10,10 @@ import Foundation import MullvadTypes import Operations -public typealias ProxyCompletionHandler = (Result) -> Void +public typealias ProxyCompletionHandler = @Sendable (Result) -> Void extension REST { - public class Proxy { + public class Proxy: @unchecked Sendable { /// Synchronization queue used by network operations. let dispatchQueue: DispatchQueue @@ -43,7 +43,7 @@ extension REST { self.responseDecoder = responseDecoder } - func makeRequestExecutor( + func makeRequestExecutor( name: String, requestHandler: RESTRequestHandler, responseHandler: some RESTResponseHandler @@ -61,7 +61,7 @@ extension REST { } /// Factory object producing instances of `NetworkOperation`. - private struct NetworkOperationFactory { + private struct NetworkOperationFactory { let dispatchQueue: DispatchQueue let configuration: ConfigurationType @@ -87,7 +87,7 @@ extension REST { } /// Network request executor that supports block-based and async execution flows. - private struct RequestExecutor: RESTRequestExecutor { + private struct RequestExecutor: RESTRequestExecutor { let operationFactory: NetworkOperationFactory let operationQueue: AsyncOperationQueue @@ -120,7 +120,7 @@ extension REST { } } - func execute(completionHandler: @escaping ProxyCompletionHandler) -> Cancellable { + func execute(completionHandler: @escaping @Sendable ProxyCompletionHandler) -> Cancellable { return execute(retryStrategy: .noRetry, completionHandler: completionHandler) } @@ -129,7 +129,7 @@ extension REST { } } - public class ProxyConfiguration { + public class ProxyConfiguration: @unchecked Sendable { public let transportProvider: RESTTransportProvider public let addressCacheStore: AddressCache @@ -142,7 +142,7 @@ extension REST { } } - public class AuthProxyConfiguration: ProxyConfiguration { + public class AuthProxyConfiguration: ProxyConfiguration, @unchecked Sendable { public let accessTokenManager: RESTAccessTokenManagement public init( diff --git a/ios/MullvadREST/ApiHandlers/RESTRequestExecutor.swift b/ios/MullvadREST/ApiHandlers/RESTRequestExecutor.swift index a705d1cda2a7..3b7a01f1c8c7 100644 --- a/ios/MullvadREST/ApiHandlers/RESTRequestExecutor.swift +++ b/ios/MullvadREST/ApiHandlers/RESTRequestExecutor.swift @@ -10,15 +10,15 @@ import Foundation import protocol MullvadTypes.Cancellable public protocol RESTRequestExecutor { - associatedtype Success + associatedtype Success: Sendable /// Execute new network request with `.noRetry` strategy and receive the result in a completion handler on main queue. - func execute(completionHandler: @escaping (Result) -> Void) -> Cancellable + func execute(completionHandler: @escaping @Sendable (Result) -> Void) -> Cancellable /// Execute new network request and receive the result in a completion handler on main queue. func execute( retryStrategy: REST.RetryStrategy, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable /// Execute new network request with `.noRetry` strategy and receive the result back via async flow. diff --git a/ios/MullvadREST/ApiHandlers/RESTRequestHandler.swift b/ios/MullvadREST/ApiHandlers/RESTRequestHandler.swift index a40212dcf511..169cf7e72da6 100644 --- a/ios/MullvadREST/ApiHandlers/RESTRequestHandler.swift +++ b/ios/MullvadREST/ApiHandlers/RESTRequestHandler.swift @@ -29,7 +29,7 @@ extension REST { let authorizationProvider: RESTAuthorizationProvider? - init(createURLRequest: @escaping (AnyIPEndpoint) throws -> REST.Request) { + init(createURLRequest: @escaping @Sendable (AnyIPEndpoint) throws -> REST.Request) { _createURLRequest = { endpoint, _ in try createURLRequest(endpoint) } @@ -37,7 +37,7 @@ extension REST { } init( - createURLRequest: @escaping (AnyIPEndpoint, REST.Authorization) throws -> REST.Request, + createURLRequest: @escaping @Sendable (AnyIPEndpoint, REST.Authorization) throws -> REST.Request, authorizationProvider: RESTAuthorizationProvider ) { _createURLRequest = { endpoint, authorization in diff --git a/ios/MullvadREST/ApiHandlers/RESTTaskIdentifier.swift b/ios/MullvadREST/ApiHandlers/RESTTaskIdentifier.swift index f892483567f4..e62030ed3b31 100644 --- a/ios/MullvadREST/ApiHandlers/RESTTaskIdentifier.swift +++ b/ios/MullvadREST/ApiHandlers/RESTTaskIdentifier.swift @@ -10,7 +10,7 @@ import Foundation extension REST { private static let nslock = NSLock() - private static var taskCount: UInt32 = 0 + nonisolated(unsafe) private static var taskCount: UInt32 = 0 static func getTaskIdentifier(name: String) -> String { nslock.lock() diff --git a/ios/MullvadREST/ApiHandlers/SSLPinningURLSessionDelegate.swift b/ios/MullvadREST/ApiHandlers/SSLPinningURLSessionDelegate.swift index a5fd86777071..e0021e8a1841 100644 --- a/ios/MullvadREST/ApiHandlers/SSLPinningURLSessionDelegate.swift +++ b/ios/MullvadREST/ApiHandlers/SSLPinningURLSessionDelegate.swift @@ -11,7 +11,7 @@ import MullvadLogging import Network import Security -final class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate { +final class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate, @unchecked Sendable { private let sslHostname: String private let trustedRootCertificates: [SecCertificate] private let addressCache: REST.AddressCache @@ -29,7 +29,7 @@ final class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate { func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust { diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift index bab0962bba49..fbc9c5660418 100644 --- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift @@ -11,7 +11,7 @@ import MullvadTypes import Network extension REST { - public struct ServerLocation: Codable, Equatable { + public struct ServerLocation: Codable, Equatable, Sendable { public let country: String public let city: String public let latitude: Double @@ -25,7 +25,7 @@ extension REST { } } - public struct BridgeRelay: Codable, Equatable { + public struct BridgeRelay: Codable, Equatable, Sendable { public let hostname: String public let active: Bool public let owned: Bool @@ -50,7 +50,7 @@ extension REST { } } - public struct ServerRelay: Codable, Equatable { + public struct ServerRelay: Codable, Equatable, Sendable { public let hostname: String public let active: Bool public let owned: Bool @@ -99,7 +99,7 @@ extension REST { } } - public struct ServerWireguardTunnels: Codable, Equatable { + public struct ServerWireguardTunnels: Codable, Equatable, Sendable { public let ipv4Gateway: IPv4Address public let ipv6Gateway: IPv6Address public let portRanges: [[UInt16]] @@ -121,19 +121,19 @@ extension REST { } } - public struct ServerShadowsocks: Codable, Equatable { + public struct ServerShadowsocks: Codable, Equatable, Sendable { public let `protocol`: String public let port: UInt16 public let cipher: String public let password: String } - public struct ServerBridges: Codable, Equatable { + public struct ServerBridges: Codable, Equatable, Sendable { public let shadowsocks: [ServerShadowsocks] public let relays: [BridgeRelay] } - public struct ServerRelaysResponse: Codable, Equatable { + public struct ServerRelaysResponse: Codable, Equatable, Sendable { public let locations: [String: ServerLocation] public let wireguard: ServerWireguardTunnels public let bridge: ServerBridges diff --git a/ios/MullvadREST/Relay/IPOverrideWrapper.swift b/ios/MullvadREST/Relay/IPOverrideWrapper.swift index 3e11591317b0..ac4946ee80e5 100644 --- a/ios/MullvadREST/Relay/IPOverrideWrapper.swift +++ b/ios/MullvadREST/Relay/IPOverrideWrapper.swift @@ -9,7 +9,7 @@ import MullvadSettings import MullvadTypes -public class IPOverrideWrapper: RelayCacheProtocol { +public final class IPOverrideWrapper: RelayCacheProtocol { private let relayCache: RelayCacheProtocol private let ipOverrideRepository: any IPOverrideRepositoryProtocol diff --git a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift index bfed08a41044..991a7df1ea29 100644 --- a/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift +++ b/ios/MullvadREST/Relay/NoRelaysSatisfyingConstraintsError.swift @@ -8,7 +8,7 @@ import Foundation -public enum NoRelaysSatisfyingConstraintsReason { +public enum NoRelaysSatisfyingConstraintsReason: Sendable { case filterConstraintNotMatching case invalidPort case entryEqualsExit @@ -18,7 +18,7 @@ public enum NoRelaysSatisfyingConstraintsReason { case relayConstraintNotMatching } -public struct NoRelaysSatisfyingConstraintsError: LocalizedError { +public struct NoRelaysSatisfyingConstraintsError: LocalizedError, Sendable { public let reason: NoRelaysSatisfyingConstraintsReason public var errorDescription: String? { diff --git a/ios/MullvadREST/Relay/RelayCache.swift b/ios/MullvadREST/Relay/RelayCache.swift index 966d3a810d83..e71c3ee347fa 100644 --- a/ios/MullvadREST/Relay/RelayCache.swift +++ b/ios/MullvadREST/Relay/RelayCache.swift @@ -9,7 +9,7 @@ import Foundation import MullvadTypes -public protocol RelayCacheProtocol { +public protocol RelayCacheProtocol: Sendable { /// Reads from a cached list, /// which falls back to reading from prebundled relays if there was no cache hit func read() throws -> StoredRelays @@ -23,9 +23,9 @@ public protocol RelayCacheProtocol { /// - Warning: `RelayCache` should not be used directly. It should be used through `IPOverrideWrapper` to have /// ip overrides applied. -public final class RelayCache: RelayCacheProtocol { +public final class RelayCache: RelayCacheProtocol, Sendable { private let fileURL: URL - private let fileCache: any FileCacheProtocol + nonisolated(unsafe) private let fileCache: any FileCacheProtocol /// Designated initializer public init(cacheDirectory: URL) { diff --git a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift index 92d48a823938..d7070055e72c 100644 --- a/ios/MullvadREST/Relay/RelaySelectorProtocol.swift +++ b/ios/MullvadREST/Relay/RelaySelectorProtocol.swift @@ -19,7 +19,7 @@ public protocol RelaySelectorProtocol { } /// Struct describing the selected relay. -public struct SelectedRelay: Equatable, Codable { +public struct SelectedRelay: Equatable, Codable, Sendable { /// Selected relay endpoint. public let endpoint: MullvadEndpoint @@ -43,7 +43,7 @@ extension SelectedRelay: CustomDebugStringConvertible { } } -public struct SelectedRelays: Equatable, Codable { +public struct SelectedRelays: Equatable, Codable, Sendable { public let entry: SelectedRelay? public let exit: SelectedRelay public let retryAttempt: UInt diff --git a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift index e7a15aa78f8b..200704cf7c10 100644 --- a/ios/MullvadREST/Relay/RelaySelectorWrapper.swift +++ b/ios/MullvadREST/Relay/RelaySelectorWrapper.swift @@ -9,7 +9,7 @@ import MullvadSettings import MullvadTypes -public final class RelaySelectorWrapper: RelaySelectorProtocol { +public final class RelaySelectorWrapper: RelaySelectorProtocol, Sendable { let relayCache: RelayCacheProtocol public init(relayCache: RelayCacheProtocol) { diff --git a/ios/MullvadREST/RetryStrategy/Jittered.swift b/ios/MullvadREST/RetryStrategy/Jittered.swift index 0f930b3aabf2..c13b9e26543a 100644 --- a/ios/MullvadREST/RetryStrategy/Jittered.swift +++ b/ios/MullvadREST/RetryStrategy/Jittered.swift @@ -34,7 +34,7 @@ struct Transformer: IteratorProtocol { private var inner: Inner private let transformer: (Inner.Element?) -> Inner.Element? - init(inner: Inner, transform: @escaping (Inner.Element?) -> Inner.Element?) { + init(inner: Inner, transform: @escaping @Sendable (Inner.Element?) -> Inner.Element?) { self.inner = inner self.transformer = transform } diff --git a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift index eac644103999..896cceadcac3 100644 --- a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift +++ b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes extension REST { - public struct RetryStrategy { + public struct RetryStrategy: Sendable { public var maxRetryCount: Int public var delay: RetryDelay public var applyJitter: Bool @@ -42,34 +42,34 @@ extension REST { } /// Strategy configured to never retry. - public static var noRetry = RetryStrategy( + public static let noRetry = RetryStrategy( maxRetryCount: 0, delay: .never, applyJitter: false ) /// Strategy configured with 2 retry attempts and exponential backoff. - public static var `default` = RetryStrategy( + public static let `default` = RetryStrategy( maxRetryCount: 2, delay: defaultRetryDelay, applyJitter: true ) /// Strategy configured with 10 retry attempts and exponential backoff. - public static var aggressive = RetryStrategy( + public static let aggressive = RetryStrategy( maxRetryCount: 10, delay: defaultRetryDelay, applyJitter: true ) /// Default retry delay. - public static var defaultRetryDelay: RetryDelay = .exponentialBackoff( + public static let defaultRetryDelay: RetryDelay = .exponentialBackoff( initial: .seconds(2), multiplier: 2, maxDelay: .seconds(8) ) - public static var postQuantumKeyExchange = RetryStrategy( + public static let postQuantumKeyExchange = RetryStrategy( maxRetryCount: 10, delay: .exponentialBackoff( initial: .seconds(10), @@ -79,7 +79,7 @@ extension REST { applyJitter: true ) - public static var failedMigrationRecovery = RetryStrategy( + public static let failedMigrationRecovery = RetryStrategy( maxRetryCount: .max, delay: .exponentialBackoff( initial: .seconds(5), @@ -90,7 +90,7 @@ extension REST { ) } - public enum RetryDelay: Equatable { + public enum RetryDelay: Equatable, Sendable { /// Never wait to retry. case never diff --git a/ios/MullvadREST/Transport/AccessMethodIterator.swift b/ios/MullvadREST/Transport/AccessMethodIterator.swift index 245bca30f5c7..8a3bf2b84a4a 100644 --- a/ios/MullvadREST/Transport/AccessMethodIterator.swift +++ b/ios/MullvadREST/Transport/AccessMethodIterator.swift @@ -10,7 +10,7 @@ import Combine import Foundation import MullvadSettings -class AccessMethodIterator { +final class AccessMethodIterator: @unchecked Sendable { private let dataSource: AccessMethodRepositoryDataSource private var index = 0 diff --git a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift index 77c5b48e5981..ac40ded63d33 100644 --- a/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift +++ b/ios/MullvadREST/Transport/Direct/URLSessionTransport.swift @@ -24,7 +24,7 @@ public final class URLSessionTransport: RESTTransport { public func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Swift.Error?) -> Void ) -> Cancellable { let dataTask = urlSession.dataTask(with: request, completionHandler: completion) dataTask.resume() diff --git a/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift b/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift index 2910a14eed96..748e48e13afd 100644 --- a/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift +++ b/ios/MullvadREST/Transport/EncryptedDNS/EncryptedDNSTransport.swift @@ -9,7 +9,7 @@ import Foundation import MullvadRustRuntime import MullvadTypes -public final class EncryptedDNSTransport: RESTTransport { +public final class EncryptedDNSTransport: RESTTransport, @unchecked Sendable { public var name: String { "encrypted-dns-url-session" } @@ -39,7 +39,7 @@ public final class EncryptedDNSTransport: RESTTransport { /// most of the time starting the DNS proxy was already spent. public func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, (any Error)?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void ) -> any Cancellable { dispatchQueue.async { [weak self] in guard let self else { return } @@ -58,7 +58,7 @@ public final class EncryptedDNSTransport: RESTTransport { return components?.url } - let wrappedCompletionHandler: (Data?, URLResponse?, (any Error)?) + let wrappedCompletionHandler: @Sendable (Data?, URLResponse?, (any Error)?) -> Void = { [weak self] data, response, maybeError in if maybeError != nil { self?.encryptedDnsProxy.stop() @@ -77,7 +77,7 @@ public final class EncryptedDNSTransport: RESTTransport { } return AnyCancellable { [weak self] in - self?.dispatchQueue.async { + self?.dispatchQueue.async { [weak self] in self?.dnsProxyTask?.cancel() } } diff --git a/ios/MullvadREST/Transport/RESTTransport.swift b/ios/MullvadREST/Transport/RESTTransport.swift index eb87b6db994c..e7c1c8cde89a 100644 --- a/ios/MullvadREST/Transport/RESTTransport.swift +++ b/ios/MullvadREST/Transport/RESTTransport.swift @@ -9,8 +9,9 @@ import Foundation import MullvadTypes -public protocol RESTTransport { +public protocol RESTTransport: Sendable { var name: String { get } - func sendRequest(_ request: URLRequest, completion: @escaping (Data?, URLResponse?, Error?) -> Void) -> Cancellable + func sendRequest(_ request: URLRequest, completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) + -> Cancellable } diff --git a/ios/MullvadREST/Transport/RESTTransportProvider.swift b/ios/MullvadREST/Transport/RESTTransportProvider.swift index 33e661bc9c33..e7c2a36d8c51 100644 --- a/ios/MullvadREST/Transport/RESTTransportProvider.swift +++ b/ios/MullvadREST/Transport/RESTTransportProvider.swift @@ -17,7 +17,7 @@ extension REST { public struct AnyTransportProvider: RESTTransportProvider { private let block: () -> RESTTransport? - public init(_ block: @escaping () -> RESTTransport?) { + public init(_ block: @escaping @Sendable () -> RESTTransport?) { self.block = block } diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift index 4c9b1a7d6bd2..36119d2af8f0 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfiguration.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes import Network -public struct ShadowsocksConfiguration: Codable, Equatable { +public struct ShadowsocksConfiguration: Codable, Equatable, Sendable { public let address: AnyIPAddress public let port: UInt16 public let password: String diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift index e5c68b631cb2..455be3b10372 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksConfigurationCache.swift @@ -9,14 +9,14 @@ import Foundation import MullvadTypes -public protocol ShadowsocksConfigurationCacheProtocol { +public protocol ShadowsocksConfigurationCacheProtocol: Sendable { func read() throws -> ShadowsocksConfiguration func write(_ configuration: ShadowsocksConfiguration) throws func clear() throws } /// Holds a shadowsocks configuration object backed by a caching mechanism shared across processes -public final class ShadowsocksConfigurationCache: ShadowsocksConfigurationCacheProtocol { +public final class ShadowsocksConfigurationCache: ShadowsocksConfigurationCacheProtocol, @unchecked Sendable { private let configurationLock = NSLock() private var cachedConfiguration: ShadowsocksConfiguration? private let fileCache: FileCache diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift index c35b0692b5cc..44092b1192ba 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksLoader.swift @@ -10,18 +10,18 @@ import Foundation import MullvadSettings import MullvadTypes -public protocol ShadowsocksLoaderProtocol { +public protocol ShadowsocksLoaderProtocol: Sendable { func load() throws -> ShadowsocksConfiguration func clear() throws } -public class ShadowsocksLoader: ShadowsocksLoaderProtocol { +public final class ShadowsocksLoader: ShadowsocksLoaderProtocol, Sendable { let cache: ShadowsocksConfigurationCacheProtocol let relaySelector: ShadowsocksRelaySelectorProtocol let settingsUpdater: SettingsUpdater - private var observer: SettingsObserverBlock! - private var tunnelSettings = LatestTunnelSettings() + nonisolated(unsafe) private var observer: SettingsObserverBlock! + nonisolated(unsafe) private var tunnelSettings = LatestTunnelSettings() private let settingsStrategy = TunnelSettingsStrategy() deinit { diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift index c0d83bc701fb..9d5156a94c71 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksRelaySelector.swift @@ -10,7 +10,7 @@ import Foundation import MullvadSettings import MullvadTypes -public protocol ShadowsocksRelaySelectorProtocol { +public protocol ShadowsocksRelaySelectorProtocol: Sendable { func selectRelay(with settings: LatestTunnelSettings) throws -> REST.BridgeRelay? func getBridges() throws -> REST.ServerShadowsocks? diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift index 16d419785635..684185a99429 100644 --- a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift +++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksTransport.swift @@ -44,7 +44,7 @@ public final class ShadowsocksTransport: RESTTransport { public func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Swift.Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Swift.Error?) -> Void ) -> Cancellable { // Start the Shadowsocks proxy in order to get a local port shadowsocksProxy.start() diff --git a/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift b/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift index d99f9e808185..7d4dc674bf9f 100644 --- a/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift +++ b/ios/MullvadREST/Transport/Socks5/NWConnection+Extensions.swift @@ -16,7 +16,7 @@ extension NWConnection { - exactLength: exact number of bytes to read. - completion: a completion handler. */ - func receive(exactLength: Int, completion: @escaping (Data?, ContentContext?, Bool, NWError?) -> Void) { + func receive(exactLength: Int, completion: @Sendable @escaping (Data?, ContentContext?, Bool, NWError?) -> Void) { receive(minimumIncompleteLength: exactLength, maximumLength: exactLength, completion: completion) } } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift b/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift index ad013d1ae966..8fe190baba08 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5AddressType.swift @@ -8,7 +8,7 @@ import Foundation /// Address type supported by socks protocol -enum Socks5AddressType: UInt8 { +enum Socks5AddressType: UInt8, Sendable { case ipv4 = 0x01 case domainName = 0x03 case ipv6 = 0x04 diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift index a72dfc610868..83f9316b8d4d 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift @@ -14,13 +14,13 @@ enum Socks5AuthenticationMethod: UInt8 { case usernamePassword = 0x02 } -struct Socks5Authentication { +struct Socks5Authentication: Sendable { let connection: NWConnection let endpoint: Socks5Endpoint let configuration: Socks5Configuration - typealias AuthenticationComplete = () -> Void - typealias AuthenticationFailure = (Error) -> Void + typealias AuthenticationComplete = @Sendable () -> Void + typealias AuthenticationFailure = @Sendable (Error) -> Void func authenticate(onComplete: @escaping AuthenticationComplete, onFailure: @escaping AuthenticationFailure) { guard let username = configuration.username, let password = configuration.password else { diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift index 9bf2eb87dc2b..03aa623a1f15 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift @@ -11,7 +11,7 @@ import MullvadTypes /// Socks5 configuration. /// - See: ``URLSessionSocks5Transport`` -public struct Socks5Configuration: Equatable { +public struct Socks5Configuration: Equatable, Sendable { /// The socks proxy endpoint. public var proxyEndpoint: AnyIPEndpoint diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift b/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift index cbd4f0875e13..bff35c58607d 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5ConnectNegotiation.swift @@ -17,10 +17,10 @@ struct Socks5ConnectNegotiation { let endpoint: Socks5Endpoint /// Completion handler invoked on success. - let onComplete: (Socks5ConnectReply) -> Void + let onComplete: @Sendable (Socks5ConnectReply) -> Void /// Failure handler invoked on error. - let onFailure: (Error) -> Void + let onFailure: @Sendable (Error) -> Void /// Initiate negotiation by sending a connect command to the socks proxy. func perform() { diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift index be232f8437e9..670a9bc63ca9 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift @@ -9,7 +9,7 @@ import Foundation import Network /// A bidirectional data connection between a local endpoint and remote endpoint over socks proxy. -final class Socks5Connection { +final class Socks5Connection: Sendable { /// The remote endpoint to which the client wants to establish connection over the socks proxy. let remoteServerEndpoint: Socks5Endpoint let configuration: Socks5Configuration @@ -75,7 +75,7 @@ final class Socks5Connection { - Parameter newStateHandler: state handler block. */ - func setStateHandler(_ newStateHandler: ((Socks5Connection, State) -> Void)?) { + func setStateHandler(_ newStateHandler: (@Sendable (Socks5Connection, State) -> Void)?) { queue.async { [self] in stateHandler = newStateHandler } @@ -110,8 +110,8 @@ final class Socks5Connection { private let queue: DispatchQueue private let localConnection: NWConnection private let remoteConnection: NWConnection - private var stateHandler: ((Socks5Connection, State) -> Void)? - private var state: State = .initialized { + nonisolated(unsafe) private var stateHandler: (@Sendable (Socks5Connection, State) -> Void)? + nonisolated(unsafe) private var state: State = .initialized { didSet { stateHandler?(self, state) } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift b/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift index 1089cecb8ff8..7eedcbcdc9e0 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5DataStreamHandler.swift @@ -9,7 +9,7 @@ import Foundation import Network /// The object handling bidirectional streaming of data between local and remote connection. -struct Socks5DataStreamHandler { +struct Socks5DataStreamHandler: Sendable { /// How many bytes the handler can receive at one time, when streaming data between local and remote connection. static let maxBytesToRead = Int(UInt16.max) @@ -20,7 +20,7 @@ struct Socks5DataStreamHandler { let remoteConnection: NWConnection /// Error handler. - let errorHandler: (Error) -> Void + let errorHandler: @Sendable (Error) -> Void /// Start streaming data between local and remote connection. func start() { diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift b/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift index 1587991bf7ce..dd967cb09e88 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Endpoint.swift @@ -10,7 +10,7 @@ import MullvadTypes import Network /// A network endpoint specified by DNS name and port. -public struct Socks5HostEndpoint { +public struct Socks5HostEndpoint: Sendable { /// The endpoint's hostname. public let hostname: String @@ -43,7 +43,7 @@ public struct Socks5HostEndpoint { } /// The endpoint type used by objects implementing socks protocol. -public enum Socks5Endpoint { +public enum Socks5Endpoint: Sendable { /// IPv4 endpoint. case ipv4(IPv4Endpoint) diff --git a/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift b/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift index aff939e8be54..b27bd8a3d0dd 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5EndpointReader.swift @@ -10,7 +10,7 @@ import MullvadTypes import Network /// The object reading the endpoint data from connection. -struct Socks5EndpointReader { +struct Socks5EndpointReader: Sendable { /// Connection to the socks proxy. let connection: NWConnection @@ -18,10 +18,10 @@ struct Socks5EndpointReader { let addressType: Socks5AddressType /// Completion handler called upon success. - let onComplete: (Socks5Endpoint) -> Void + let onComplete: @Sendable (Socks5Endpoint) -> Void /// Failure handler. - let onFailure: (Error) -> Void + let onFailure: @Sendable (Error) -> Void /// Start reading endpoint from connection. func perform() { @@ -69,7 +69,7 @@ struct Socks5EndpointReader { } } - private func readBoundDomainNameLength(completion: @escaping (Int) -> Void) { + private func readBoundDomainNameLength(completion: @escaping @Sendable (Int) -> Void) { // The length of domain length parameter in bytes. let domainLengthLength = MemoryLayout.size diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift index 8be764cbb10d..62e7c4711d4a 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift @@ -9,7 +9,7 @@ import Foundation import Network /// The errors returned by objects implementing socks proxy. -public enum Socks5Error: Error { +public enum Socks5Error: Error, Sendable { /// Unexpected end of stream. case unexpectedEndOfStream diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift index 29a1b2f70abb..503e033ff7d6 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift @@ -19,7 +19,7 @@ import Network Refer to RFC1928 for more info on socks5: */ -public final class Socks5ForwardingProxy { +public final class Socks5ForwardingProxy: Sendable { /// Socks proxy endpoint. public let socksProxyEndpoint: NWEndpoint @@ -70,7 +70,7 @@ public final class Socks5ForwardingProxy { - Parameter completion: completion handler that is called once the TCP listener is ready in the first time or failed before moving to the ready state. Invoked on main queue. */ - public func start(completion: @escaping (Error?) -> Void) { + public func start(completion: @escaping @Sendable (Error?) -> Void) { queue.async { self.startListener { error in DispatchQueue.main.async { @@ -85,7 +85,7 @@ public final class Socks5ForwardingProxy { - Parameter completion: completion handler that's called immediately after cancelling the TCP listener. Invoked on main queue. */ - public func stop(completion: (() -> Void)? = nil) { + public func stop(completion: (@Sendable () -> Void)? = nil) { queue.async { self.stopInner() @@ -100,7 +100,7 @@ public final class Socks5ForwardingProxy { - Parameter errorHandler: an error handler block. Invoked on main queue. */ - public func setErrorHandler(_ errorHandler: ((Error) -> Void)?) { + public func setErrorHandler(_ errorHandler: (@Sendable (Error) -> Void)?) { queue.async { self.errorHandler = errorHandler } @@ -108,7 +108,7 @@ public final class Socks5ForwardingProxy { // MARK: - Private - private enum State { + private enum State: @unchecked Sendable { /// Proxy is starting up. case starting(listener: NWListener, completion: (Error?) -> Void) @@ -120,15 +120,15 @@ public final class Socks5ForwardingProxy { } private let queue = DispatchQueue(label: "Socks5ForwardingProxy-queue") - private var state: State = .stopped - private var errorHandler: ((Error) -> Void)? + nonisolated(unsafe) private var state: State = .stopped + nonisolated(unsafe) private var errorHandler: (@Sendable (Error) -> Void)? /** Start TCP listener. - Parameter completion: completion handler that is called once the TCP listener is ready or failed. */ - private func startListener(completion: @escaping (Error?) -> Void) { + private func startListener(completion: @escaping @Sendable (Error?) -> Void) { switch state { case .started: completion(nil) diff --git a/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift b/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift index 7b1059de1ae4..a06eecb0a6cc 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5HandshakeNegotiation.swift @@ -9,11 +9,11 @@ import Foundation import Network /// The object handling a handshake negotiation with socks proxy. -struct Socks5HandshakeNegotiation { +struct Socks5HandshakeNegotiation: Sendable { let connection: NWConnection let handshake: Socks5Handshake - let onComplete: (Socks5HandshakeReply) -> Void - let onFailure: (Error) -> Void + let onComplete: @Sendable (Socks5HandshakeReply) -> Void + let onFailure: @Sendable (Error) -> Void func perform() { connection.send(content: handshake.rawData, completion: .contentProcessed { [self] error in diff --git a/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift b/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift index 9832c156a13a..6c2a5f7995be 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5StatusCode.swift @@ -8,7 +8,7 @@ import Foundation /// Status code used in socks protocol. -public enum Socks5StatusCode: UInt8 { +public enum Socks5StatusCode: UInt8, Sendable { case succeeded = 0x00 case failure = 0x01 case connectionNotAllowedByRuleset = 0x02 diff --git a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift index 6c3a67159b5d..feefa588a444 100644 --- a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift +++ b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift @@ -11,7 +11,7 @@ import MullvadLogging import MullvadTypes /// Transport that passes URL requests over the local socks forwarding proxy. -public class URLSessionSocks5Transport: RESTTransport { +public final class URLSessionSocks5Transport: RESTTransport, Sendable { /// Socks5 forwarding proxy. private let socksProxy: Socks5ForwardingProxy @@ -25,7 +25,7 @@ public class URLSessionSocks5Transport: RESTTransport { "socks5-url-session" } - private let logger = Logger(label: "URLSessionSocks5Transport") + nonisolated(unsafe) private let logger = Logger(label: "URLSessionSocks5Transport") /** Instantiates new socks5 transport. @@ -57,7 +57,7 @@ public class URLSessionSocks5Transport: RESTTransport { public func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { // Listen port should be set when socks proxy is ready. Otherwise start proxy and only then start the data task. if let localPort = socksProxy.listenPort { @@ -70,9 +70,9 @@ public class URLSessionSocks5Transport: RESTTransport { /// Starts socks proxy then executes the data task. private func sendDeferred( request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { - let chain = CancellableChain() + nonisolated(unsafe) let chain = CancellableChain() socksProxy.start { [weak self, weak socksProxy] error in if let error { @@ -94,7 +94,7 @@ public class URLSessionSocks5Transport: RESTTransport { private func startDataTask( request: URLRequest, localPort: UInt16, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { // Copy the URL request and rewrite the host and port to point to the socks5 forwarding proxy instance var newRequest = request diff --git a/ios/MullvadREST/Transport/TransportProvider.swift b/ios/MullvadREST/Transport/TransportProvider.swift index b511bdfa6bce..b92878a25582 100644 --- a/ios/MullvadREST/Transport/TransportProvider.swift +++ b/ios/MullvadREST/Transport/TransportProvider.swift @@ -10,12 +10,12 @@ import Foundation import Logging import MullvadTypes -public final class TransportProvider: RESTTransportProvider { +public final class TransportProvider: RESTTransportProvider, Sendable { private let urlSessionTransport: URLSessionTransport private let addressCache: REST.AddressCache - private var transportStrategy: TransportStrategy - private var currentTransport: RESTTransport? - private var currentTransportType: TransportStrategy.Transport + nonisolated(unsafe) private var transportStrategy: TransportStrategy + nonisolated(unsafe) private var currentTransport: RESTTransport? + nonisolated(unsafe) private var currentTransportType: TransportStrategy.Transport private let parallelRequestsMutex = NSLock() private let encryptedDNSTransport: RESTTransport @@ -123,7 +123,7 @@ private extension URLError { /// Interstitial implementation of `RESTTransport` that intercepts the completion of the wrapped transport. private struct TransportWrapper: RESTTransport { let wrapped: RESTTransport - let onComplete: (Error?) -> Void + let onComplete: @Sendable (Error?) -> Void var name: String { return wrapped.name @@ -131,7 +131,7 @@ private struct TransportWrapper: RESTTransport { func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { return wrapped.sendRequest(request) { data, response, error in onComplete(error) diff --git a/ios/MullvadREST/Transport/TransportStrategy.swift b/ios/MullvadREST/Transport/TransportStrategy.swift index 394e7cac40b9..00037786d766 100644 --- a/ios/MullvadREST/Transport/TransportStrategy.swift +++ b/ios/MullvadREST/Transport/TransportStrategy.swift @@ -11,7 +11,7 @@ import Logging import MullvadSettings import MullvadTypes -public struct TransportStrategy: Equatable { +public struct TransportStrategy: Equatable, Sendable { /// The different transports suggested by the strategy public enum Transport: Equatable { /// Connecting a direct connection diff --git a/ios/MullvadRESTTests/Mocks/AnyTransport.swift b/ios/MullvadRESTTests/Mocks/AnyTransport.swift index b21a72b33f99..7d5ff242bd70 100644 --- a/ios/MullvadRESTTests/Mocks/AnyTransport.swift +++ b/ios/MullvadRESTTests/Mocks/AnyTransport.swift @@ -6,12 +6,12 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // -import Foundation +@preconcurrency import Foundation @testable import MullvadREST import MullvadTypes /// Mock implementation of REST transport that can be used to handle requests without doing any actual networking. -class AnyTransport: RESTTransport { +class AnyTransport: RESTTransport, @unchecked Sendable { typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void private let handleRequest: () -> AnyResponse @@ -19,7 +19,7 @@ class AnyTransport: RESTTransport { private let completionLock = NSLock() private var completionHandlers: [UUID: CompletionHandler] = [:] - init(block: @escaping () -> AnyResponse) { + init(block: @escaping @Sendable () -> AnyResponse) { handleRequest = block } @@ -29,7 +29,7 @@ class AnyTransport: RESTTransport { func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { let response = handleRequest() let id = storeCompletion(completionHandler: completion) diff --git a/ios/MullvadRESTTests/Mocks/RESTTransportStub.swift b/ios/MullvadRESTTests/Mocks/RESTTransportStub.swift index 5e0915554b22..2b7cee771197 100644 --- a/ios/MullvadRESTTests/Mocks/RESTTransportStub.swift +++ b/ios/MullvadRESTTests/Mocks/RESTTransportStub.swift @@ -19,7 +19,7 @@ struct RESTTransportStub: RESTTransport { func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { completion(data, response, error) return AnyCancellable() diff --git a/ios/MullvadRESTTests/Mocks/TimeServerProxy.swift b/ios/MullvadRESTTests/Mocks/TimeServerProxy.swift index 76f8da18a8a5..2d3093e60e7b 100644 --- a/ios/MullvadRESTTests/Mocks/TimeServerProxy.swift +++ b/ios/MullvadRESTTests/Mocks/TimeServerProxy.swift @@ -10,7 +10,7 @@ import Foundation @testable import MullvadREST /// Simple API proxy used for testing purposes. -final class TimeServerProxy: REST.Proxy { +final class TimeServerProxy: REST.Proxy, @unchecked Sendable { init(configuration: REST.ProxyConfiguration) { super.init( name: "TimeServerProxy", diff --git a/ios/MullvadRESTTests/RequestExecutorTests.swift b/ios/MullvadRESTTests/RequestExecutorTests.swift index 644e6bf24733..d0bcdc78d73c 100644 --- a/ios/MullvadRESTTests/RequestExecutorTests.swift +++ b/ios/MullvadRESTTests/RequestExecutorTests.swift @@ -11,9 +11,10 @@ @testable import MullvadTypes import XCTest +@MainActor final class RequestExecutorTests: XCTestCase { let addressCache = REST.AddressCache(canWriteToCache: false, fileCache: MemoryCache()) - var timerServerProxy: TimeServerProxy! + nonisolated(unsafe) var timerServerProxy: TimeServerProxy! override func setUp() { super.setUp() diff --git a/ios/MullvadRustRuntime/ShadowSocksProxy.swift b/ios/MullvadRustRuntime/ShadowSocksProxy.swift index cd83f2129ae6..19f07f1a427e 100644 --- a/ios/MullvadRustRuntime/ShadowSocksProxy.swift +++ b/ios/MullvadRustRuntime/ShadowSocksProxy.swift @@ -11,7 +11,7 @@ import MullvadRustRuntimeProxy import Network /// A Swift wrapper around a Rust implementation of Shadowsocks proxy instance -public class ShadowsocksProxy { +public class ShadowsocksProxy: @unchecked Sendable { private var proxyConfig: ProxyHandle private let forwardAddress: IPAddress private let forwardPort: UInt16 diff --git a/ios/MullvadRustRuntimeTests/UnsafeListener.swift b/ios/MullvadRustRuntimeTests/UnsafeListener.swift index 2ccf0c1aaf57..80ab97289a15 100644 --- a/ios/MullvadRustRuntimeTests/UnsafeListener.swift +++ b/ios/MullvadRustRuntimeTests/UnsafeListener.swift @@ -9,7 +9,7 @@ import Network /// > Warning: Do not use this implementation in production code. See the warning in `start()`. -class UnsafeListener { +class UnsafeListener: @unchecked Sendable { private let dispatchQueue = DispatchQueue(label: "com.test.unsafeListener") private let listener: NWListener diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift index 74591bcbfea0..652e7d748856 100644 --- a/ios/MullvadSettings/AccessMethodRepository.swift +++ b/ios/MullvadSettings/AccessMethodRepository.swift @@ -11,7 +11,7 @@ import Foundation import MullvadLogging import MullvadTypes -public class AccessMethodRepository: AccessMethodRepositoryProtocol { +public class AccessMethodRepository: AccessMethodRepositoryProtocol, @unchecked Sendable { public static let directId = UUID(uuidString: "C9DB7457-2A55-42C3-A926-C07F82131994")! public static let bridgeId = UUID(uuidString: "8586E75A-CA7B-4432-B70D-EE65F3F95084")! public static let encryptedDNSId = UUID(uuidString: "831CB1F8-1829-42DD-B9DC-82902F298EC0")! diff --git a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift index 0239919f4cd5..21d8067790f0 100644 --- a/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift +++ b/ios/MullvadSettings/AccessMethodRepositoryProtocol.swift @@ -8,7 +8,7 @@ import Combine -public protocol AccessMethodRepositoryDataSource { +public protocol AccessMethodRepositoryDataSource: Sendable { /// Publisher that propagates a snapshot of all access methods upon modifications. var accessMethodsPublisher: AnyPublisher<[PersistentAccessMethod], Never> { get } diff --git a/ios/MullvadSettings/DAITASettings.swift b/ios/MullvadSettings/DAITASettings.swift index 7fd3170bf9f3..ca0aa059b378 100644 --- a/ios/MullvadSettings/DAITASettings.swift +++ b/ios/MullvadSettings/DAITASettings.swift @@ -9,7 +9,7 @@ import Foundation /// Whether DAITA is enabled. -public enum DAITAState: Codable { +public enum DAITAState: Codable, Sendable { case on case off @@ -24,7 +24,7 @@ public enum DAITAState: Codable { } /// Whether "direct only" is enabled, meaning no automatic routing to DAITA relays. -public enum DirectOnlyState: Codable { +public enum DirectOnlyState: Codable, Sendable { case on case off @@ -43,7 +43,7 @@ public enum DAITASettingsCompatibilityError { case singlehop, multihop } -public struct DAITASettings: Codable, Equatable { +public struct DAITASettings: Codable, Equatable, Sendable { @available(*, deprecated, renamed: "daitaState") public let state: DAITAState = .off diff --git a/ios/MullvadSettings/DNSSettings.swift b/ios/MullvadSettings/DNSSettings.swift index 4b7a3ae7cb6c..f515b0c60107 100644 --- a/ios/MullvadSettings/DNSSettings.swift +++ b/ios/MullvadSettings/DNSSettings.swift @@ -11,7 +11,7 @@ import MullvadTypes import Network /// A struct describing Mullvad DNS blocking options. -public struct DNSBlockingOptions: OptionSet, Codable { +public struct DNSBlockingOptions: OptionSet, Codable, Sendable { public let rawValue: UInt32 public static let blockAdvertising = DNSBlockingOptions(rawValue: 1 << 0) @@ -48,7 +48,7 @@ public struct DNSBlockingOptions: OptionSet, Codable { } /// A struct that holds DNS settings. -public struct DNSSettings: Codable, Equatable { +public struct DNSSettings: Codable, Equatable, Sendable { /// Maximum number of allowed DNS domains. public static let maxAllowedCustomDNSDomains = 3 diff --git a/ios/MullvadSettings/DeviceState.swift b/ios/MullvadSettings/DeviceState.swift index 052511d328d6..37b14927a13d 100644 --- a/ios/MullvadSettings/DeviceState.swift +++ b/ios/MullvadSettings/DeviceState.swift @@ -8,7 +8,7 @@ import Foundation -public enum DeviceState: Codable, Equatable { +public enum DeviceState: Codable, Equatable, Sendable { case loggedIn(StoredAccountData, StoredDeviceData) case loggedOut case revoked diff --git a/ios/MullvadSettings/IPOverride.swift b/ios/MullvadSettings/IPOverride.swift index c25fdf4604af..1d3cec101cf3 100644 --- a/ios/MullvadSettings/IPOverride.swift +++ b/ios/MullvadSettings/IPOverride.swift @@ -8,7 +8,7 @@ import Network -public struct RelayOverrides: Codable { +public struct RelayOverrides: Codable, Sendable { public let overrides: [IPOverride] private enum CodingKeys: String, CodingKey { @@ -20,7 +20,7 @@ public struct IPOverrideFormatError: LocalizedError { public let errorDescription: String? } -public struct IPOverride: Codable, Equatable { +public struct IPOverride: Codable, Equatable, Sendable { public let hostname: String public var ipv4Address: IPv4Address? public var ipv6Address: IPv6Address? diff --git a/ios/MullvadSettings/IPOverrideRepository.swift b/ios/MullvadSettings/IPOverrideRepository.swift index 867a1c077fd5..5407ab0bd975 100644 --- a/ios/MullvadSettings/IPOverrideRepository.swift +++ b/ios/MullvadSettings/IPOverrideRepository.swift @@ -9,15 +9,15 @@ import Foundation import MullvadLogging -public protocol IPOverrideRepositoryProtocol { +public protocol IPOverrideRepositoryProtocol: Sendable { func add(_ overrides: [IPOverride]) func fetchAll() -> [IPOverride] func deleteAll() func parse(data: Data) throws -> [IPOverride] } -public class IPOverrideRepository: IPOverrideRepositoryProtocol { - private let logger = Logger(label: "IPOverrideRepository") +public final class IPOverrideRepository: IPOverrideRepositoryProtocol { + nonisolated(unsafe) private let logger = Logger(label: "IPOverrideRepository") private let readWriteLock = NSLock() public init() {} diff --git a/ios/MullvadSettings/KeychainSettingsStore.swift b/ios/MullvadSettings/KeychainSettingsStore.swift index ba0612de0d3a..30b054d1006d 100644 --- a/ios/MullvadSettings/KeychainSettingsStore.swift +++ b/ios/MullvadSettings/KeychainSettingsStore.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes import Security -public class KeychainSettingsStore: SettingsStore { +final public class KeychainSettingsStore: SettingsStore, Sendable { public let serviceName: String public let accessGroup: String diff --git a/ios/MullvadSettings/Migration.swift b/ios/MullvadSettings/Migration.swift index 0d9678deb5a3..e8cc6a0564e5 100644 --- a/ios/MullvadSettings/Migration.swift +++ b/ios/MullvadSettings/Migration.swift @@ -12,6 +12,6 @@ public protocol Migration { func migrate( with store: SettingsStore, parser: SettingsParser, - completion: @escaping (Error?) -> Void + completion: @escaping @Sendable (Error?) -> Void ) } diff --git a/ios/MullvadSettings/MigrationManager.swift b/ios/MullvadSettings/MigrationManager.swift index c13fd9baa580..c16209f4d700 100644 --- a/ios/MullvadSettings/MigrationManager.swift +++ b/ios/MullvadSettings/MigrationManager.swift @@ -10,7 +10,7 @@ import Foundation import MullvadLogging import MullvadTypes -public enum SettingsMigrationResult { +public enum SettingsMigrationResult: Sendable { /// Nothing to migrate. case nothing @@ -42,7 +42,7 @@ public struct MigrationManager { /// - migrationCompleted: Completion handler called with a migration result. public func migrateSettings( store: SettingsStore, - migrationCompleted: @escaping (SettingsMigrationResult) -> Void + migrationCompleted: @escaping @Sendable (SettingsMigrationResult) -> Void ) { let fileCoordinator = NSFileCoordinator(filePresenter: nil) var error: NSError? @@ -79,7 +79,7 @@ public struct MigrationManager { private func upgradeSettingsToLatestVersion( store: SettingsStore, - migrationCompleted: @escaping (SettingsMigrationResult) -> Void + migrationCompleted: @escaping @Sendable (SettingsMigrationResult) -> Void ) throws { let parser = SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder()) let settingsData = try store.read(key: SettingsKey.settings) diff --git a/ios/MullvadSettings/MultihopSettings.swift b/ios/MullvadSettings/MultihopSettings.swift index a9a17f2b4358..1308f24535bd 100644 --- a/ios/MullvadSettings/MultihopSettings.swift +++ b/ios/MullvadSettings/MultihopSettings.swift @@ -9,8 +9,8 @@ import Foundation import MullvadTypes -/// Whether multihop is enabled. -public enum MultihopState: Codable { +/// Whether Multi-hop is enabled +public enum MultihopState: Codable, Sendable { case on case off diff --git a/ios/MullvadSettings/QuantumResistanceSettings.swift b/ios/MullvadSettings/QuantumResistanceSettings.swift index 956b2fd0ded2..2ea3c2ee536d 100644 --- a/ios/MullvadSettings/QuantumResistanceSettings.swift +++ b/ios/MullvadSettings/QuantumResistanceSettings.swift @@ -8,7 +8,7 @@ import Foundation -public enum TunnelQuantumResistance: Codable { +public enum TunnelQuantumResistance: Codable, Sendable { case automatic case on case off diff --git a/ios/MullvadSettings/SettingsManager.swift b/ios/MullvadSettings/SettingsManager.swift index d414d7ed0aa7..7600b22ad310 100644 --- a/ios/MullvadSettings/SettingsManager.swift +++ b/ios/MullvadSettings/SettingsManager.swift @@ -15,16 +15,16 @@ private let accountTokenKey = "accountToken" private let accountExpiryKey = "accountExpiry" public enum SettingsManager { - private static let logger = Logger(label: "SettingsManager") + nonisolated(unsafe) private static let logger = Logger(label: "SettingsManager") #if DEBUG - private static var _store = KeychainSettingsStore( + nonisolated(unsafe) private static var _store = KeychainSettingsStore( serviceName: keychainServiceName, accessGroup: ApplicationConfiguration.securityGroupIdentifier ) /// Alternative store used for tests. - internal static var unitTestStore: SettingsStore? + nonisolated(unsafe) internal static var unitTestStore: SettingsStore? public static var store: SettingsStore { if let unitTestStore { return unitTestStore } diff --git a/ios/MullvadSettings/SettingsStore.swift b/ios/MullvadSettings/SettingsStore.swift index 2901ae2bb3b9..8353d8711acb 100644 --- a/ios/MullvadSettings/SettingsStore.swift +++ b/ios/MullvadSettings/SettingsStore.swift @@ -8,7 +8,7 @@ import Foundation -public enum SettingsKey: String, CaseIterable { +public enum SettingsKey: String, CaseIterable, Sendable { case settings = "Settings" case deviceState = "DeviceState" case apiAccessMethods = "ApiAccessMethods" @@ -18,7 +18,7 @@ public enum SettingsKey: String, CaseIterable { case shouldWipeSettings = "ShouldWipeSettings" } -public protocol SettingsStore { +public protocol SettingsStore: Sendable { func read(key: SettingsKey) throws -> Data func write(_ data: Data, for key: SettingsKey) throws func delete(key: SettingsKey) throws diff --git a/ios/MullvadSettings/ShadowsocksCipherOptions.swift b/ios/MullvadSettings/ShadowsocksCipherOptions.swift index 6d1bcab7cb43..c7703b592635 100644 --- a/ios/MullvadSettings/ShadowsocksCipherOptions.swift +++ b/ios/MullvadSettings/ShadowsocksCipherOptions.swift @@ -8,7 +8,7 @@ import Foundation -public struct ShadowsocksCipherOptions: RawRepresentable, Codable, Hashable { +public struct ShadowsocksCipherOptions: RawRepresentable, Codable, Hashable, Sendable { public let rawValue: CipherIdentifiers public init(rawValue: CipherIdentifiers) { @@ -22,7 +22,7 @@ public struct ShadowsocksCipherOptions: RawRepresentable, Codable, Hashable { public static let all = CipherIdentifiers.allCases.map { ShadowsocksCipherOptions(rawValue: $0) } } -public enum CipherIdentifiers: String, CaseIterable, CustomStringConvertible, Codable { +public enum CipherIdentifiers: String, CaseIterable, CustomStringConvertible, Codable, Sendable { // Stream ciphers. case CFB_AES128 = "aes-128-cfb" case CFB1_AES128 = "aes-128-cfb1" diff --git a/ios/MullvadSettings/StoredAccountData.swift b/ios/MullvadSettings/StoredAccountData.swift index 276982805c0e..e54eaeb1df16 100644 --- a/ios/MullvadSettings/StoredAccountData.swift +++ b/ios/MullvadSettings/StoredAccountData.swift @@ -8,7 +8,7 @@ import Foundation -public struct StoredAccountData: Codable, Equatable { +public struct StoredAccountData: Codable, Equatable, Sendable { /// Account identifier. public var identifier: String diff --git a/ios/MullvadSettings/StoredDeviceData.swift b/ios/MullvadSettings/StoredDeviceData.swift index a0d784af614c..dcb674da320e 100644 --- a/ios/MullvadSettings/StoredDeviceData.swift +++ b/ios/MullvadSettings/StoredDeviceData.swift @@ -8,9 +8,9 @@ import Foundation import MullvadTypes -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes -public struct StoredDeviceData: Codable, Equatable { +public struct StoredDeviceData: Codable, Equatable, Sendable { /// Device creation date. public var creationDate: Date diff --git a/ios/MullvadSettings/StoredWgKeyData.swift b/ios/MullvadSettings/StoredWgKeyData.swift index df07e0e8c94e..57915098d491 100644 --- a/ios/MullvadSettings/StoredWgKeyData.swift +++ b/ios/MullvadSettings/StoredWgKeyData.swift @@ -7,9 +7,9 @@ // import Foundation -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes -public struct StoredWgKeyData: Codable, Equatable { +public struct StoredWgKeyData: Codable, Equatable, Sendable { /// Private key creation date. public var creationDate: Date diff --git a/ios/MullvadSettings/TunnelSettings.swift b/ios/MullvadSettings/TunnelSettings.swift index 3298e84356e7..f530c4376256 100644 --- a/ios/MullvadSettings/TunnelSettings.swift +++ b/ios/MullvadSettings/TunnelSettings.swift @@ -12,12 +12,12 @@ import Foundation public typealias LatestTunnelSettings = TunnelSettingsV6 /// Protocol all TunnelSettings must adhere to, for upgrade purposes. -public protocol TunnelSettings: Codable { +public protocol TunnelSettings: Codable, Sendable { func upgradeToNextVersion() -> any TunnelSettings } /// Settings and device state schema versions. -public enum SchemaVersion: Int, Equatable { +public enum SchemaVersion: Int, Equatable, Sendable { /// Legacy settings format, stored as `TunnelSettingsV1`. case v1 = 1 diff --git a/ios/MullvadSettings/TunnelSettingsPropagator.swift b/ios/MullvadSettings/TunnelSettingsPropagator.swift index 3f0a3395adbd..b38ffb48fa0c 100644 --- a/ios/MullvadSettings/TunnelSettingsPropagator.swift +++ b/ios/MullvadSettings/TunnelSettingsPropagator.swift @@ -8,7 +8,7 @@ import MullvadTypes -public protocol SettingsPropagation { +public protocol SettingsPropagation: Sendable { typealias SettingsHandler = (LatestTunnelSettings) -> Void var onNewSettings: SettingsHandler? { get set } } @@ -30,7 +30,7 @@ public class SettingsObserverBlock: SettingsObserver { } } -public final class TunnelSettingsListener: SettingsPropagation { +public final class TunnelSettingsListener: SettingsPropagation, @unchecked Sendable { public var onNewSettings: SettingsHandler? public init(onNewSettings: SettingsHandler? = nil) { @@ -38,10 +38,10 @@ public final class TunnelSettingsListener: SettingsPropagation { } } -public class SettingsUpdater { +public final class SettingsUpdater: Sendable { /// Observers. private let observerList = ObserverList() - private var listener: SettingsPropagation + nonisolated(unsafe) private var listener: SettingsPropagation public init(listener: SettingsPropagation) { self.listener = listener diff --git a/ios/MullvadSettings/TunnelSettingsStrategy.swift b/ios/MullvadSettings/TunnelSettingsStrategy.swift index 22a3721cc4ef..a0d399be0d6f 100644 --- a/ios/MullvadSettings/TunnelSettingsStrategy.swift +++ b/ios/MullvadSettings/TunnelSettingsStrategy.swift @@ -7,11 +7,11 @@ // import Foundation -public protocol TunnelSettingsStrategyProtocol { +public protocol TunnelSettingsStrategyProtocol: Sendable { func shouldReconnectToNewRelay(oldSettings: LatestTunnelSettings, newSettings: LatestTunnelSettings) -> Bool } -public struct TunnelSettingsStrategy: TunnelSettingsStrategyProtocol { +public struct TunnelSettingsStrategy: TunnelSettingsStrategyProtocol, Sendable { public init() {} public func shouldReconnectToNewRelay( oldSettings: LatestTunnelSettings, diff --git a/ios/MullvadSettings/TunnelSettingsUpdate.swift b/ios/MullvadSettings/TunnelSettingsUpdate.swift index 6fd333d003d5..0dbf818baf85 100644 --- a/ios/MullvadSettings/TunnelSettingsUpdate.swift +++ b/ios/MullvadSettings/TunnelSettingsUpdate.swift @@ -9,7 +9,7 @@ import Foundation import MullvadTypes -public enum TunnelSettingsUpdate { +public enum TunnelSettingsUpdate: Sendable { case dnsSettings(DNSSettings) case obfuscation(WireGuardObfuscationSettings) case relayConstraints(RelayConstraints) diff --git a/ios/MullvadSettings/TunnelSettingsV1.swift b/ios/MullvadSettings/TunnelSettingsV1.swift index aaa3c278458e..e5a62217d53a 100644 --- a/ios/MullvadSettings/TunnelSettingsV1.swift +++ b/ios/MullvadSettings/TunnelSettingsV1.swift @@ -22,7 +22,7 @@ public struct TunnelSettingsV1: Codable, Equatable, TunnelSettings { } /// A struct that holds a tun interface configuration. -public struct InterfaceSettings: Codable, Equatable { +public struct InterfaceSettings: Codable, Equatable, @unchecked Sendable { public var privateKey: PrivateKeyWithMetadata public var nextPrivateKey: PrivateKeyWithMetadata? diff --git a/ios/MullvadSettings/TunnelSettingsV6.swift b/ios/MullvadSettings/TunnelSettingsV6.swift index 4f81d9549d9e..7ce6ed17842f 100644 --- a/ios/MullvadSettings/TunnelSettingsV6.swift +++ b/ios/MullvadSettings/TunnelSettingsV6.swift @@ -9,7 +9,7 @@ import Foundation import MullvadTypes -public struct TunnelSettingsV6: Codable, Equatable, TunnelSettings { +public struct TunnelSettingsV6: Codable, Equatable, TunnelSettings, Sendable { /// Relay constraints. public var relayConstraints: RelayConstraints diff --git a/ios/MullvadSettings/WireGuardObfuscationSettings.swift b/ios/MullvadSettings/WireGuardObfuscationSettings.swift index f067114cc634..10ab8ffc4630 100644 --- a/ios/MullvadSettings/WireGuardObfuscationSettings.swift +++ b/ios/MullvadSettings/WireGuardObfuscationSettings.swift @@ -11,7 +11,7 @@ import Foundation /// Whether obfuscation is enabled and which method is used. /// /// `.automatic` means an algorithm will decide whether to use obfuscation or not. -public enum WireGuardObfuscationState: Codable { +public enum WireGuardObfuscationState: Codable, Sendable { @available(*, deprecated, renamed: "udpOverTcp") case on @@ -48,7 +48,7 @@ public enum WireGuardObfuscationState: Codable { } } -public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible { +public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomStringConvertible, Sendable { case automatic case port80 case port5001 @@ -81,7 +81,7 @@ public enum WireGuardObfuscationUdpOverTcpPort: Codable, Equatable, CustomString } } -public enum WireGuardObfuscationShadowsocksPort: Codable, Equatable, CustomStringConvertible { +public enum WireGuardObfuscationShadowsocksPort: Codable, Equatable, CustomStringConvertible, Sendable { case automatic case custom(UInt16) @@ -111,7 +111,7 @@ public enum WireGuardObfuscationShadowsocksPort: Codable, Equatable, CustomStrin // Can't deprecate the whole type since it'll yield a lint warning when decoding // port in `WireGuardObfuscationSettings`. -private enum WireGuardObfuscationPort: UInt16, Codable { +private enum WireGuardObfuscationPort: UInt16, Codable, Sendable { @available(*, deprecated, message: "Use `udpOverTcpPort` instead") case automatic = 0 @available(*, deprecated, message: "Use `udpOverTcpPort` instead") @@ -120,7 +120,7 @@ private enum WireGuardObfuscationPort: UInt16, Codable { case port5001 = 5001 } -public struct WireGuardObfuscationSettings: Codable, Equatable { +public struct WireGuardObfuscationSettings: Codable, Equatable, Sendable { @available(*, deprecated, message: "Use `udpOverTcpPort` instead") private var port: WireGuardObfuscationPort = .automatic diff --git a/ios/MullvadTypes/AnyIPEndpoint.swift b/ios/MullvadTypes/AnyIPEndpoint.swift index cf294c70282d..27b995729d32 100644 --- a/ios/MullvadTypes/AnyIPEndpoint.swift +++ b/ios/MullvadTypes/AnyIPEndpoint.swift @@ -9,7 +9,7 @@ import Foundation import protocol Network.IPAddress -public enum AnyIPEndpoint: Hashable, Equatable, Codable, CustomStringConvertible { +public enum AnyIPEndpoint: Hashable, Equatable, Codable, CustomStringConvertible, @unchecked Sendable { case ipv4(IPv4Endpoint) case ipv6(IPv6Endpoint) diff --git a/ios/MullvadTypes/Cancellable.swift b/ios/MullvadTypes/Cancellable.swift index 8f658a6da1a8..3fa640f198e2 100644 --- a/ios/MullvadTypes/Cancellable.swift +++ b/ios/MullvadTypes/Cancellable.swift @@ -19,7 +19,7 @@ public final class AnyCancellable: Cancellable { private let block: (() -> Void)? /// Create cancellation token with block handler. - public init(block: @escaping () -> Void) { + public init(block: @escaping @Sendable () -> Void) { self.block = block } diff --git a/ios/MullvadTypes/IPv4Endpoint.swift b/ios/MullvadTypes/IPv4Endpoint.swift index c0ed1e477112..2e771aaa3317 100644 --- a/ios/MullvadTypes/IPv4Endpoint.swift +++ b/ios/MullvadTypes/IPv4Endpoint.swift @@ -9,7 +9,7 @@ import Foundation import Network -public struct IPv4Endpoint: Hashable, Equatable, Codable, CustomStringConvertible { +public struct IPv4Endpoint: Hashable, Equatable, Codable, CustomStringConvertible, Sendable { public let ip: IPv4Address public let port: UInt16 diff --git a/ios/MullvadTypes/IPv6Endpoint.swift b/ios/MullvadTypes/IPv6Endpoint.swift index 5dd56a49829c..e65ce0f225fd 100644 --- a/ios/MullvadTypes/IPv6Endpoint.swift +++ b/ios/MullvadTypes/IPv6Endpoint.swift @@ -9,7 +9,7 @@ import Foundation import Network -public struct IPv6Endpoint: Hashable, Equatable, Codable, CustomStringConvertible { +public struct IPv6Endpoint: Hashable, Equatable, Codable, CustomStringConvertible, Sendable { public let ip: IPv6Address public let port: UInt16 diff --git a/ios/MullvadTypes/Location.swift b/ios/MullvadTypes/Location.swift index 13cf93542558..508454122083 100644 --- a/ios/MullvadTypes/Location.swift +++ b/ios/MullvadTypes/Location.swift @@ -9,7 +9,7 @@ import CoreLocation import Foundation -public struct Location: Codable, Equatable { +public struct Location: Codable, Equatable, Sendable { public var country: String public var countryCode: String public var city: String diff --git a/ios/MullvadTypes/MullvadEndpoint.swift b/ios/MullvadTypes/MullvadEndpoint.swift index 1361df2e46e1..0702a5ee80f4 100644 --- a/ios/MullvadTypes/MullvadEndpoint.swift +++ b/ios/MullvadTypes/MullvadEndpoint.swift @@ -10,7 +10,7 @@ import Foundation import Network /// Contains server data needed to connect to a single mullvad endpoint. -public struct MullvadEndpoint: Equatable, Codable { +public struct MullvadEndpoint: Equatable, Codable, Sendable { public let ipv4Relay: IPv4Endpoint public let ipv6Relay: IPv6Endpoint? public let ipv4Gateway: IPv4Address diff --git a/ios/MullvadTypes/ObserverList.swift b/ios/MullvadTypes/ObserverList.swift index 2f26085c446f..fa659fb44335 100644 --- a/ios/MullvadTypes/ObserverList.swift +++ b/ios/MullvadTypes/ObserverList.swift @@ -8,12 +8,12 @@ import Foundation -public struct WeakBox { +public struct WeakBox: Sendable { public var value: T? { valueProvider() } - private let valueProvider: () -> T? + nonisolated(unsafe) private let valueProvider: () -> T? public init(_ value: T) { let reference = value as AnyObject @@ -28,9 +28,9 @@ public struct WeakBox { } } -final public class ObserverList { +final public class ObserverList: Sendable { private let lock = NSLock() - private var observers = [WeakBox]() + nonisolated(unsafe) private var observers = [WeakBox]() public init() {} diff --git a/ios/MullvadTypes/Promise.swift b/ios/MullvadTypes/Promise.swift index 886f00f63354..55351c5e7b80 100644 --- a/ios/MullvadTypes/Promise.swift +++ b/ios/MullvadTypes/Promise.swift @@ -8,7 +8,7 @@ import Foundation -public final class Promise { +public final class Promise: @unchecked Sendable { public typealias Result = Swift.Result private let nslock = NSLock() diff --git a/ios/MullvadTypes/Protocols/NetworkExtension+HiddenSymbols.swift b/ios/MullvadTypes/Protocols/NetworkExtension+HiddenSymbols.swift new file mode 100644 index 000000000000..a43aa37b0e9c --- /dev/null +++ b/ios/MullvadTypes/Protocols/NetworkExtension+HiddenSymbols.swift @@ -0,0 +1,17 @@ +// +// NetworkExtension+HiddenSymbols.swift +// MullvadTypes +// +// Created by Marco Nikic on 2024-12-04. +// Copyright © 2024 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import NetworkExtension + +#if swift(>=6) +#if compiler(>=6) +public typealias NWTCPConnection = __NWTCPConnection +public typealias NWHostEndpoint = __NWHostEndpoint +#endif +#endif diff --git a/ios/MullvadTypes/RESTTypes.swift b/ios/MullvadTypes/RESTTypes.swift index bb5c27002216..f5fe9bdd6e8d 100644 --- a/ios/MullvadTypes/RESTTypes.swift +++ b/ios/MullvadTypes/RESTTypes.swift @@ -7,9 +7,9 @@ // import Foundation -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes -public struct Account: Codable, Equatable { +public struct Account: Codable, Equatable, Sendable { public let id: String public let expiry: Date public let maxDevices: Int @@ -23,7 +23,7 @@ public struct Account: Codable, Equatable { } } -public struct Device: Codable, Equatable { +public struct Device: Codable, Equatable, Sendable { public let id: String public let name: String public let pubkey: PublicKey diff --git a/ios/MullvadTypes/RelayConstraints.swift b/ios/MullvadTypes/RelayConstraints.swift index b6396767d8a3..391b5e310b70 100644 --- a/ios/MullvadTypes/RelayConstraints.swift +++ b/ios/MullvadTypes/RelayConstraints.swift @@ -8,7 +8,7 @@ import Foundation -public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible { +public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible, @unchecked Sendable { @available(*, deprecated, renamed: "locations") private var location: RelayConstraint = .only(.country("se")) diff --git a/ios/MullvadTypes/RelayLocation.swift b/ios/MullvadTypes/RelayLocation.swift index 279f3cb6bc8e..b2ef8fa95448 100644 --- a/ios/MullvadTypes/RelayLocation.swift +++ b/ios/MullvadTypes/RelayLocation.swift @@ -8,7 +8,7 @@ import Foundation -public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible { +public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible, Sendable { case country(String) case city(String, String) case hostname(String, String, String) @@ -107,7 +107,7 @@ public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible { } } -public struct UserSelectedRelays: Codable, Equatable { +public struct UserSelectedRelays: Codable, Equatable, Sendable { public let locations: [RelayLocation] public let customListSelection: CustomListSelection? @@ -118,7 +118,7 @@ public struct UserSelectedRelays: Codable, Equatable { } extension UserSelectedRelays { - public struct CustomListSelection: Codable, Equatable { + public struct CustomListSelection: Codable, Equatable, Sendable { /// The ID of the custom list that the selected relays belong to. public let listId: UUID /// Whether the selected relays are subnodes or the custom list itself. diff --git a/ios/MullvadTypes/TransportLayer.swift b/ios/MullvadTypes/TransportLayer.swift index b4a7e6c3cdd2..cb25c7249084 100644 --- a/ios/MullvadTypes/TransportLayer.swift +++ b/ios/MullvadTypes/TransportLayer.swift @@ -8,7 +8,7 @@ import Foundation -public enum TransportLayer: Codable { +public enum TransportLayer: Codable, Sendable { case udp case tcp } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 05a800581dcf..fc52fdd36413 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -1292,6 +1292,13 @@ remoteGlobalIDString = 58D223D4294C8E5E0029F5F8; remoteInfo = MullvadTypes; }; + A9609B6D2D004D1F0065A3D3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 58CE5E58224146200008646E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 58FBDA9722A519BC00EB69A3; + remoteInfo = WireGuardGoBridge; + }; A992DA212C24709F00DE7CE5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 58CE5E58224146200008646E /* Project object */; @@ -4719,6 +4726,7 @@ buildRules = ( ); dependencies = ( + A9609B6E2D004D1F0065A3D3 /* PBXTargetDependency */, 58D223E9294C8F120029F5F8 /* PBXTargetDependency */, 58D223F8294C8FF00029F5F8 /* PBXTargetDependency */, 06799AD028F98E1D00ACD94E /* PBXTargetDependency */, @@ -6606,6 +6614,11 @@ target = 58D223D4294C8E5E0029F5F8 /* MullvadTypes */; targetProxy = A9173C332C36CCFB00F6A08C /* PBXContainerItemProxy */; }; + A9609B6E2D004D1F0065A3D3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 58FBDA9722A519BC00EB69A3 /* WireGuardGoBridge */; + targetProxy = A9609B6D2D004D1F0065A3D3 /* PBXContainerItemProxy */; + }; A992DA222C24709F00DE7CE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A992DA1C2C24709F00DE7CE5 /* MullvadRustRuntime */; @@ -6771,6 +6784,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6790,6 +6804,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -6900,6 +6915,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6939,6 +6955,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -6961,6 +6978,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -6981,6 +6999,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -7044,7 +7063,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -7102,7 +7121,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -7180,6 +7199,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -7199,6 +7219,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(APPLICATION_IDENTIFIER).PacketTunnel"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -7300,6 +7321,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -7335,6 +7357,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -7422,7 +7445,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = ""; OTHER_LDFLAGS = ""; - PATH = "${PATH}:/opt/homebrew/opt/go@1.19/bin"; + PATH = "${PATH}:/opt/homebrew/opt/go@1.21/bin"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -7435,7 +7458,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = ""; OTHER_LDFLAGS = ""; - PATH = "${PATH}:/opt/homebrew/opt/go@1.19/bin"; + PATH = "${PATH}:/opt/homebrew/opt/go@1.21/bin"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -7746,7 +7769,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Staging; @@ -7790,7 +7813,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = ""; OTHER_LDFLAGS = ""; - PATH = "${PATH}:/opt/homebrew/opt/go@1.19/bin"; + PATH = "${PATH}:/opt/homebrew/opt/go@1.21/bin"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Staging; @@ -7815,6 +7838,7 @@ "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Packet Tunnel Development"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; }; name = Staging; }; @@ -7833,6 +7857,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Staging; @@ -7978,6 +8003,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -8050,6 +8076,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8072,6 +8099,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Staging; @@ -8276,6 +8304,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8326,6 +8355,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8376,6 +8406,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8425,6 +8456,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8452,6 +8484,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -8476,6 +8509,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Staging; @@ -8500,6 +8534,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -8524,6 +8559,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = MockRelease; @@ -8580,7 +8616,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -8621,7 +8657,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.0; OTHER_CFLAGS = ""; OTHER_LDFLAGS = ""; - PATH = "${PATH}:/opt/homebrew/opt/go@1.19/bin"; + PATH = "${PATH}:/opt/homebrew/opt/go@1.21/bin"; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = MockRelease; @@ -8645,6 +8681,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Packet Tunnel Development"; SWIFT_STRICT_CONCURRENCY = minimal; + SWIFT_VERSION = 5.0; }; name = MockRelease; }; @@ -8663,6 +8700,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = MockRelease; @@ -8808,6 +8846,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSION_INFO_PREFIX = ""; }; @@ -8880,6 +8919,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -8902,6 +8942,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = MockRelease; diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift index 03e108968a9c..699022a674a7 100644 --- a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTester.swift @@ -22,7 +22,7 @@ class ProxyConfigurationTester: ProxyConfigurationTesterProtocol { self.transportProvider = transportProvider } - func start(configuration: PersistentProxyConfiguration, completion: @escaping (Error?) -> Void) { + func start(configuration: PersistentProxyConfiguration, completion: @escaping @Sendable (Error?) -> Void) { do { let transport = try transportProvider.makeTransport(with: configuration) let request = REST.APIAvailabilityTestRequest(transport: transport) diff --git a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift index b01d817d61b6..95f6261c6583 100644 --- a/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift +++ b/ios/MullvadVPN/AccessMethodRepository/ProxyConfigurationTesterProtocol.swift @@ -15,7 +15,7 @@ protocol ProxyConfigurationTesterProtocol { /// - Parameters: /// - configuration: a proxy configuration. /// - completion: a completion handler that receives `nil` upon success, otherwise the underlying error. - func start(configuration: PersistentProxyConfiguration, completion: @escaping (Error?) -> Void) + func start(configuration: PersistentProxyConfiguration, completion: @escaping @Sendable (Error?) -> Void) /// Cancel testing proxy configuration. func cancel() diff --git a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift index 00d8b2fe1365..50d052a2b197 100644 --- a/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift +++ b/ios/MullvadVPN/AddressCacheTracker/AddressCacheTracker.swift @@ -12,7 +12,7 @@ import MullvadTypes import Operations import UIKit -final class AddressCacheTracker { +final class AddressCacheTracker: @unchecked Sendable { /// Update interval. private static let updateInterval: Duration = .days(1) @@ -84,7 +84,7 @@ final class AddressCacheTracker { timer = nil } - func updateEndpoints(completionHandler: ((Result) -> Void)? = nil) -> Cancellable { + func updateEndpoints(completionHandler: (@Sendable (Result) -> Void)? = nil) -> Cancellable { let operation = ResultBlockOperation { finish -> Cancellable in guard self.nextScheduleDate() <= Date() else { finish(.success(false)) diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 2f78e904760a..d6620c992ca1 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -18,9 +18,10 @@ import StoreKit import UIKit import UserNotifications -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, StorePaymentManagerDelegate { - private var logger: Logger! +@main +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, StorePaymentManagerDelegate, + @unchecked Sendable { + nonisolated(unsafe) private var logger: Logger! #if targetEnvironment(simulator) private var simulatorTunnelProviderHost: SimulatorTunnelProviderHost? @@ -29,22 +30,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private let operationQueue = AsyncOperationQueue.makeSerial() private(set) var tunnelStore: TunnelStore! - private(set) var tunnelManager: TunnelManager! + nonisolated(unsafe) private(set) var tunnelManager: TunnelManager! private(set) var addressCache: REST.AddressCache! private var proxyFactory: ProxyFactoryProtocol! private(set) var apiProxy: APIQuerying! private(set) var accountsProxy: RESTAccountHandling! - private(set) var devicesProxy: DeviceHandling! + nonisolated(unsafe) private(set) var devicesProxy: DeviceHandling! private(set) var addressCacheTracker: AddressCacheTracker! - private(set) var relayCacheTracker: RelayCacheTracker! - private(set) var storePaymentManager: StorePaymentManager! - private var transportMonitor: TransportMonitor! + nonisolated(unsafe) private(set) var relayCacheTracker: RelayCacheTracker! + nonisolated(unsafe) private(set) var storePaymentManager: StorePaymentManager! + nonisolated(unsafe) private var transportMonitor: TransportMonitor! private var settingsObserver: TunnelBlockObserver! private var migrationManager: MigrationManager! - private(set) var accessMethodRepository = AccessMethodRepository() + nonisolated(unsafe) private(set) var accessMethodRepository = AccessMethodRepository() private(set) var shadowsocksLoader: ShadowsocksLoaderProtocol! private(set) var configuredTransportProvider: ProxyConfigurationTransportProvider! private(set) var ipOverrideRepository = IPOverrideRepository() @@ -277,11 +278,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: BackgroundTask.appRefresh.identifier, using: nil ) { [self] task in + nonisolated(unsafe) let nonisolatedTask = task + let handle = relayCacheTracker.updateRelays { result in - task.setTaskCompleted(success: result.isSuccess) + nonisolatedTask.setTaskCompleted(success: result.isSuccess) } - task.expirationHandler = { + nonisolatedTask.expirationHandler = { handle.cancel() } @@ -300,13 +303,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: BackgroundTask.privateKeyRotation.identifier, using: nil ) { [self] task in + nonisolated(unsafe) let nonisolatedTask = task let handle = tunnelManager.rotatePrivateKey { [self] error in - scheduleKeyRotationTask() + Task { @MainActor in + scheduleKeyRotationTask() - task.setTaskCompleted(success: error == nil) + nonisolatedTask.setTaskCompleted(success: error == nil) + } } - task.expirationHandler = { + nonisolatedTask.expirationHandler = { handle.cancel() } } @@ -323,13 +329,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD forTaskWithIdentifier: BackgroundTask.addressCacheUpdate.identifier, using: nil ) { [self] task in - let handle = addressCacheTracker.updateEndpoints { [self] result in - scheduleAddressCacheUpdateTask() + nonisolated(unsafe) let nonisolatedTask = task - task.setTaskCompleted(success: result.isSuccess) + let handle = addressCacheTracker.updateEndpoints { [self] result in + Task { @MainActor in + scheduleAddressCacheUpdateTask() + nonisolatedTask.setTaskCompleted(success: result.isSuccess) + } } - task.expirationHandler = { + nonisolatedTask.expirationHandler = { handle.cancel() } } @@ -471,48 +480,56 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func getLoadTunnelStoreOperation() -> AsyncBlockOperation { AsyncBlockOperation(dispatchQueue: .main) { [self] finish in - tunnelStore.loadPersistentTunnels { [self] error in - if let error { - logger.error( - error: error, - message: "Failed to load persistent tunnels." - ) + MainActor.assumeIsolated { + tunnelStore.loadPersistentTunnels { [self] error in + if let error { + logger.error( + error: error, + message: "Failed to load persistent tunnels." + ) + } + finish(nil) } - finish(nil) } } } private func getMigrateSettingsOperation(application: UIApplication) -> AsyncBlockOperation { - AsyncBlockOperation(dispatchQueue: .main) { [self] finish in - migrationManager - .migrateSettings(store: SettingsManager.store) { [self] migrationResult in - switch migrationResult { - case .success: - // Tell the tunnel to re-read tunnel configuration after migration. - logger.debug("Successful migration from UI Process") - tunnelManager.reconnectTunnel(selectNewRelay: true) - fallthrough - - case .nothing: - logger.debug("Attempted migration from UI Process, but found nothing to do") - finish(nil) - - case let .failure(error): - logger.error("Failed migration from UI Process: \(error)") - let migrationUIHandler = application.connectedScenes - .first { $0 is SettingsMigrationUIHandler } as? SettingsMigrationUIHandler - - if let migrationUIHandler { - migrationUIHandler.showMigrationError(error) { - finish(error) + AsyncBlockOperation(dispatchQueue: .main, block: { [self] (finish: @escaping @Sendable (Error?) -> Void) in + MainActor.assumeIsolated { + migrationManager + .migrateSettings(store: SettingsManager.store) { [self] migrationResult in + switch migrationResult { + case .success: + // Tell the tunnel to re-read tunnel configuration after migration. + logger.debug("Successful migration from UI Process") + tunnelManager.reconnectTunnel(selectNewRelay: true) + fallthrough + + case .nothing: + logger.debug("Attempted migration from UI Process, but found nothing to do") + finish(nil) + + case let .failure(error): + logger.error("Failed migration from UI Process: \(error)") + MainActor.assumeIsolated { + let migrationUIHandler = application.connectedScenes + .first { $0 is SettingsMigrationUIHandler } as? SettingsMigrationUIHandler + + if let migrationUIHandler { + migrationUIHandler.showMigrationError(error) { + MainActor.assumeIsolated { + finish(error) + } + } + } else { + finish(error) + } } - } else { - finish(error) } } - } - } + } + }) } private func getInitTunnelManagerOperation() -> AsyncBlockOperation { @@ -576,7 +593,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - StorePaymentManagerDelegate - func storePaymentManager(_ manager: StorePaymentManager, didRequestAccountTokenFor payment: SKPayment) -> String? { + nonisolated func storePaymentManager( + _ manager: StorePaymentManager, + didRequestAccountTokenFor payment: SKPayment + ) -> String? { // Since we do not persist the relation between payment and account number between the // app launches, we assume that all successful purchases belong to the active account // number. @@ -585,21 +605,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - UNUserNotificationCenterDelegate - func userNotificationCenter( + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { + nonisolated(unsafe) let nonisolatedResponse = response + nonisolated(unsafe) let nonisolatedCompletionHandler = completionHandler + let blockOperation = AsyncBlockOperation(dispatchQueue: .main) { - NotificationManager.shared.handleSystemNotificationResponse(response) + NotificationManager.shared.handleSystemNotificationResponse(nonisolatedResponse) - completionHandler() + nonisolatedCompletionHandler() } operationQueue.addOperation(blockOperation) } - func userNotificationCenter( + nonisolated func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void diff --git a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift index 5aa1e0c86823..690059e3ab9d 100644 --- a/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift +++ b/ios/MullvadVPN/Classes/AccessbilityIdentifier.swift @@ -221,6 +221,7 @@ extension AccessibilityIdentifier { } extension UIAccessibilityIdentification { + @MainActor func setAccessibilityIdentifier(_ value: AccessibilityIdentifier?) { accessibilityIdentifier = value.map(\.asString) } diff --git a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift index 00defec889b3..d4a34170508a 100644 --- a/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift +++ b/ios/MullvadVPN/Classes/AutomaticKeyboardResponder.swift @@ -9,6 +9,7 @@ import MullvadLogging import UIKit +@MainActor class AutomaticKeyboardResponder { weak var targetView: UIView? private let handler: (UIView, CGFloat) -> Void diff --git a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift index 6e96912c5856..43b1a19dab34 100644 --- a/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift +++ b/ios/MullvadVPN/Classes/ConsolidatedApplicationLog.swift @@ -13,7 +13,7 @@ private let kRedactedPlaceholder = "[REDACTED]" private let kRedactedAccountPlaceholder = "[REDACTED ACCOUNT NUMBER]" private let kRedactedContainerPlaceholder = "[REDACTED CONTAINER PATH]" -class ConsolidatedApplicationLog: TextOutputStreamable { +class ConsolidatedApplicationLog: TextOutputStreamable, @unchecked Sendable { typealias Metadata = KeyValuePairs private let bufferSize: UInt64 @@ -50,7 +50,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { } } - func addLogFiles(fileURLs: [URL], completion: (() -> Void)? = nil) { + func addLogFiles(fileURLs: [URL], completion: (@Sendable () -> Void)? = nil) { logQueue.async(flags: .barrier) { for fileURL in fileURLs { self.addSingleLogFile(fileURL) @@ -61,7 +61,7 @@ class ConsolidatedApplicationLog: TextOutputStreamable { } } - func addError(message: String, error: String, completion: (() -> Void)? = nil) { + func addError(message: String, error: String, completion: (@Sendable () -> Void)? = nil) { let redactedError = redact(string: error) logQueue.async(flags: .barrier) { self.logs.append(LogAttachment(label: message, content: redactedError)) diff --git a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift index 2c405395bc40..8e3a4c8f5d58 100644 --- a/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift +++ b/ios/MullvadVPN/Containers/Root/RootContainerViewController.swift @@ -9,7 +9,7 @@ import Routing import UIKit -enum HeaderBarStyle { +enum HeaderBarStyle: Sendable { case transparent, `default`, unsecured, secured fileprivate func backgroundColor() -> UIColor { @@ -26,7 +26,7 @@ enum HeaderBarStyle { } } -struct HeaderBarPresentation { +struct HeaderBarPresentation: Sendable { let style: HeaderBarStyle let showsDivider: Bool @@ -36,7 +36,8 @@ struct HeaderBarPresentation { } /// A protocol that defines the relationship between the root container and its child controllers -protocol RootContainment { +@MainActor +protocol RootContainment: Sendable { /// Return the preferred header bar style var preferredHeaderBarPresentation: HeaderBarPresentation { get } @@ -60,7 +61,7 @@ extension RootContainment { } } -protocol RootContainerViewControllerDelegate: AnyObject { +protocol RootContainerViewControllerDelegate: AnyObject, Sendable { func rootContainerViewControllerShouldShowAccount( _ controller: RootContainerViewController, animated: Bool diff --git a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift index 310ebbdc8467..50f2a54d4624 100644 --- a/ios/MullvadVPN/Coordinators/AccountCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountCoordinator.swift @@ -9,18 +9,18 @@ import Routing import UIKit -enum AccountDismissReason: Equatable { +enum AccountDismissReason: Equatable, Sendable { case none case userLoggedOut case accountDeletion } -enum AddedMoreCreditOption: Equatable { +enum AddedMoreCreditOption: Equatable, Sendable { case redeemingVoucher case inAppPurchase } -final class AccountCoordinator: Coordinator, Presentable, Presenting { +final class AccountCoordinator: Coordinator, Presentable, Presenting, @unchecked Sendable { private let interactor: AccountInteractor private var accountController: AccountViewController? @@ -29,7 +29,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { navigationController } - var didFinish: ((AccountCoordinator, AccountDismissReason) -> Void)? + var didFinish: (@MainActor (AccountCoordinator, AccountDismissReason) -> Void)? init( navigationController: UINavigationController, @@ -101,6 +101,7 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { ) } + @MainActor private func navigateToDeleteAccount() { let coordinator = AccountDeletionCoordinator( navigationController: CustomNavigationController(), @@ -109,10 +110,12 @@ final class AccountCoordinator: Coordinator, Presentable, Presenting { coordinator.start() coordinator.didCancel = { accountDeletionCoordinator in - accountDeletionCoordinator.dismiss(animated: true) + Task { @MainActor in + accountDeletionCoordinator.dismiss(animated: true) + } } - coordinator.didFinish = { accountDeletionCoordinator in + coordinator.didFinish = { @MainActor accountDeletionCoordinator in accountDeletionCoordinator.dismiss(animated: true) { self.didFinish?(self, .userLoggedOut) } diff --git a/ios/MullvadVPN/Coordinators/AccountDeletionCoordinator.swift b/ios/MullvadVPN/Coordinators/AccountDeletionCoordinator.swift index e1e2058ca2a6..445812f60178 100644 --- a/ios/MullvadVPN/Coordinators/AccountDeletionCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AccountDeletionCoordinator.swift @@ -14,8 +14,8 @@ final class AccountDeletionCoordinator: Coordinator, Presentable { private let navigationController: UINavigationController private let interactor: AccountDeletionInteractor - var didCancel: ((AccountDeletionCoordinator) -> Void)? - var didFinish: ((AccountDeletionCoordinator) -> Void)? + var didCancel: (@MainActor (AccountDeletionCoordinator) -> Void)? + var didFinish: (@MainActor (AccountDeletionCoordinator) -> Void)? var presentedViewController: UIViewController { navigationController @@ -37,7 +37,7 @@ final class AccountDeletionCoordinator: Coordinator, Presentable { } } -extension AccountDeletionCoordinator: AccountDeletionViewControllerDelegate { +extension AccountDeletionCoordinator: @preconcurrency AccountDeletionViewControllerDelegate { func deleteAccountDidSucceed(controller: AccountDeletionViewController) { didFinish?(self) } diff --git a/ios/MullvadVPN/Coordinators/AddCreditSucceededCoordinator.swift b/ios/MullvadVPN/Coordinators/AddCreditSucceededCoordinator.swift index 7d7922adfba1..3a90d4cb82f5 100644 --- a/ios/MullvadVPN/Coordinators/AddCreditSucceededCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/AddCreditSucceededCoordinator.swift @@ -35,7 +35,7 @@ final class AddCreditSucceededCoordinator: Coordinator { } } -extension AddCreditSucceededCoordinator: AddCreditSucceededViewControllerDelegate { +extension AddCreditSucceededCoordinator: @preconcurrency AddCreditSucceededViewControllerDelegate { func header(in controller: AddCreditSucceededViewController) -> String { switch paymentType { case .inAppPurchase: diff --git a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift index a4ff7e3cd0f5..d9794fd234b3 100644 --- a/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ApplicationCoordinator.swift @@ -16,14 +16,15 @@ import UIKit /** Application coordinator managing split view and two navigation contexts. */ -final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewControllerDelegate, - UISplitViewControllerDelegate, ApplicationRouterDelegate, NotificationManagerDelegate { +final class ApplicationCoordinator: Coordinator, Presenting, @preconcurrency RootContainerViewControllerDelegate, + UISplitViewControllerDelegate, @preconcurrency ApplicationRouterDelegate, + @preconcurrency NotificationManagerDelegate { typealias RouteType = AppRoute /** Application router. */ - private(set) var router: ApplicationRouter! + nonisolated(unsafe) private(set) var router: ApplicationRouter! /** Navigation container. @@ -109,7 +110,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo _ router: ApplicationRouter, presentWithContext context: RoutePresentationContext, animated: Bool, - completion: @escaping (Coordinator) -> Void + completion: @escaping @Sendable (Coordinator) -> Void ) { switch context.route { case .account: @@ -150,7 +151,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo func applicationRouter( _ router: ApplicationRouter, dismissWithContext context: RouteDismissalContext, - completion: @escaping () -> Void + completion: @escaping @Sendable () -> Void ) { let dismissedRoute = context.dismissedRoutes.first! @@ -230,7 +231,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo func applicationRouter( _ router: ApplicationRouter, handleSubNavigationWithContext context: RouteSubnavigationContext, - completion: @escaping () -> Void + completion: @escaping @Sendable @MainActor () -> Void ) { switch context.route { case let .settings(subRoute): @@ -385,9 +386,11 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo coordinator.didFinishPayment = { [weak self] _ in guard let self = self else { return } - if shouldDismissOutOfTime() { - router.dismiss(.outOfTime, animated: true) - continueFlow(animated: true) + Task { @MainActor in + if shouldDismissOutOfTime() { + router.dismiss(.outOfTime, animated: true) + continueFlow(animated: true) + } } } @@ -448,10 +451,14 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo coordinator.preferredAccountNumberPublisher = preferredAccountNumberSubject.eraseToAnyPublisher() coordinator.didFinish = { [weak self] _ in - self?.continueFlow(animated: true) + MainActor.assumeIsolated { + self?.continueFlow(animated: true) + } } coordinator.didCreateAccount = { [weak self] in - self?.appPreferences.isShownOnboarding = false + MainActor.assumeIsolated { + self?.appPreferences.isShownOnboarding = false + } } addChild(coordinator) @@ -530,7 +537,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinish = { [weak self] _, reason in - self?.didDismissAccount(reason) + MainActor.assumeIsolated { + self?.didDismissAccount(reason) + } } coordinator.start(animated: animated) @@ -548,7 +557,7 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo private func presentSettings( route: SettingsNavigationRoute?, animated: Bool, - completion: @escaping (Coordinator) -> Void + completion: @escaping @Sendable (Coordinator) -> Void ) { let interactorFactory = SettingsInteractorFactory( storePaymentManager: storePaymentManager, @@ -572,7 +581,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo ) coordinator.didFinish = { [weak self] _ in - self?.router.dismissAll(.settings, animated: true) + Task { @MainActor in + self?.router.dismissAll(.settings, animated: true) + } } coordinator.willNavigate = { [weak self] _, _, to in @@ -661,7 +672,9 @@ final class ApplicationCoordinator: Coordinator, Presenting, RootContainerViewCo guard !accountData.isExpired else { return } let timer = Timer(fire: accountData.expiry, interval: 0, repeats: false, block: { [weak self] _ in - self?.router.present(.outOfTime, animated: true) + Task { @MainActor in + self?.router.present(.outOfTime, animated: true) + } }) RunLoop.main.add(timer, forMode: .common) diff --git a/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift index 50beb1ac5592..7fc203dac32d 100644 --- a/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CreateAccountVoucherCoordinator.swift @@ -50,7 +50,7 @@ public class CreateAccountVoucherCoordinator: Coordinator { } } -extension CreateAccountVoucherCoordinator: RedeemVoucherViewControllerDelegate { +extension CreateAccountVoucherCoordinator: @preconcurrency RedeemVoucherViewControllerDelegate { func redeemVoucherDidSucceed(_ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse) { let coordinator = AddCreditSucceededCoordinator( purchaseType: .redeemingVoucher, diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift index 6ef8d044fd6b..d69a9974941b 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddCustomListCoordinator.swift @@ -71,7 +71,7 @@ class AddCustomListCoordinator: Coordinator, Presentable, Presenting { } } -extension AddCustomListCoordinator: CustomListViewControllerDelegate { +extension AddCustomListCoordinator: @preconcurrency CustomListViewControllerDelegate { func customListDidSave(_ list: CustomList) { didFinish?(self) } diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift index 1634a24e8075..20bdc02500d2 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsCoordinator.swift @@ -51,7 +51,7 @@ class AddLocationsCoordinator: Coordinator, Presentable, Presenting { } } -extension AddLocationsCoordinator: AddLocationsViewControllerDelegate { +extension AddLocationsCoordinator: @preconcurrency AddLocationsViewControllerDelegate { func didBack() { didFinish?(self) } diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift index 4db1587c6bec..64a823015660 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsDataSource.swift @@ -13,7 +13,7 @@ import UIKit class AddLocationsDataSource: UITableViewDiffableDataSource, - LocationDiffableDataSourceProtocol { + LocationDiffableDataSourceProtocol, @unchecked Sendable { private var customListLocationNode: CustomListLocationNode private let nodes: [LocationNode] private let subject: CurrentValueSubject @@ -108,7 +108,7 @@ extension AddLocationsDataSource: UITableViewDelegate { } } -extension AddLocationsDataSource: LocationCellDelegate { +extension AddLocationsDataSource: @preconcurrency LocationCellDelegate { func toggleExpanding(cell: LocationCell) { toggleItems(for: cell) { if let indexPath = self.tableView.indexPath(for: cell), diff --git a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift index 10d8a308b682..053ba3622197 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/AddLocationsViewController.swift @@ -11,7 +11,7 @@ import MullvadSettings import MullvadTypes import UIKit -protocol AddLocationsViewControllerDelegate: AnyObject { +protocol AddLocationsViewControllerDelegate: AnyObject, Sendable { func didBack() } diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift index ddf7b87c11a1..ef3cc36cc360 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListCellConfiguration.swift @@ -10,6 +10,7 @@ import Combine import MullvadTypes import UIKit +@MainActor struct CustomListCellConfiguration { let tableView: UITableView let subject: CurrentValueSubject diff --git a/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift index b5598abf3e05..aed829ce39b4 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/CustomListDataSourceConfiguration.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor class CustomListDataSourceConfiguration: NSObject { let dataSource: UITableViewDiffableDataSource var validationErrors: Set = [] diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift index 2e3c8c9a0c65..bc2892c58fe8 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/EditCustomListCoordinator.swift @@ -139,7 +139,7 @@ class EditCustomListCoordinator: Coordinator, Presentable, Presenting { } } -extension EditCustomListCoordinator: CustomListViewControllerDelegate { +extension EditCustomListCoordinator: @preconcurrency CustomListViewControllerDelegate { func customListDidSave(_ list: CustomList) { didFinish?(self, .save, list) } @@ -156,7 +156,9 @@ extension EditCustomListCoordinator: CustomListViewControllerDelegate { ) coordinator.didFinish = { locationsCoordinator in - locationsCoordinator.removeFromParent() + Task { @MainActor in + locationsCoordinator.removeFromParent() + } } coordinator.start() diff --git a/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift index 9ca615ea693b..a5561c1682d6 100644 --- a/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/CustomLists/EditLocationsCoordinator.swift @@ -12,12 +12,13 @@ import MullvadTypes import Routing import UIKit -class EditLocationsCoordinator: Coordinator, Presentable, Presenting { +@MainActor +class EditLocationsCoordinator: Coordinator, Presentable, Presenting, Sendable { private let navigationController: UINavigationController private let nodes: [LocationNode] private var subject: CurrentValueSubject - var didFinish: ((EditLocationsCoordinator) -> Void)? + nonisolated(unsafe) var didFinish: (@Sendable (EditLocationsCoordinator) -> Void)? var presentedViewController: UIViewController { navigationController @@ -51,7 +52,7 @@ class EditLocationsCoordinator: Coordinator, Presentable, Presenting { } extension EditLocationsCoordinator: AddLocationsViewControllerDelegate { - func didBack() { + nonisolated func didBack() { didFinish?(self) } } diff --git a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift index acdb359560db..8703c46e5d14 100644 --- a/ios/MullvadVPN/Coordinators/LocationCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LocationCoordinator.swift @@ -204,7 +204,7 @@ extension LocationCoordinator: UIAdaptivePresentationControllerDelegate { } } -extension LocationCoordinator: RelayCacheTrackerObserver { +extension LocationCoordinator: @preconcurrency RelayCacheTrackerObserver { func relayCacheTracker( _ tracker: RelayCacheTracker, didUpdateCachedRelays cachedRelays: CachedRelays @@ -219,7 +219,7 @@ extension LocationCoordinator: RelayCacheTrackerObserver { } } -extension LocationCoordinator: LocationViewControllerWrapperDelegate { +extension LocationCoordinator: @preconcurrency LocationViewControllerWrapperDelegate { func didSelectEntryRelays(_ relays: UserSelectedRelays) { var relayConstraints = tunnelManager.settings.relayConstraints relayConstraints.entryLocations = .only(relays) diff --git a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift index a99d693b9df2..4ac34bcbbe5f 100644 --- a/ios/MullvadVPN/Coordinators/LoginCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/LoginCoordinator.swift @@ -13,16 +13,16 @@ import Operations import Routing import UIKit -final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewControllerDelegate { +final class LoginCoordinator: Coordinator, Presenting, @preconcurrency DeviceManagementViewControllerDelegate { private let tunnelManager: TunnelManager private let devicesProxy: DeviceHandling private var loginController: LoginViewController? - private var lastLoginAction: LoginAction? + nonisolated(unsafe) private var lastLoginAction: LoginAction? private var subscriptions = Set() - var didFinish: ((LoginCoordinator) -> Void)? - var didCreateAccount: (() -> Void)? + var didFinish: (@Sendable (LoginCoordinator) -> Void)? + var didCreateAccount: (@Sendable () -> Void)? var preferredAccountNumberPublisher: AnyPublisher? var presentationContext: UIViewController { @@ -84,10 +84,11 @@ final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewContr if case let .useExistingAccount(accountNumber) = action { if let error = error as? REST.Error, error.compareErrorCode(.maxDevicesReached) { return .wait(Promise { resolve in + nonisolated(unsafe) let sendableResolve = resolve self.showDeviceList(for: accountNumber) { error in self.lastLoginAction = action - resolve(error.map { .failure($0) } ?? .success(())) + sendableResolve(error.map { .failure($0) } ?? .success(())) } }) } else { @@ -115,7 +116,7 @@ final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewContr } } - private func showDeviceList(for accountNumber: String, completion: @escaping (Error?) -> Void) { + private func showDeviceList(for accountNumber: String, completion: @escaping @Sendable (Error?) -> Void) { let interactor = DeviceManagementInteractor( accountNumber: accountNumber, devicesProxy: devicesProxy @@ -131,8 +132,10 @@ final class LoginCoordinator: Coordinator, Presenting, DeviceManagementViewContr switch result { case .success: - navigationController.pushViewController(controller, animated: true) { - completion(nil) + Task { @MainActor in + navigationController.pushViewController(controller, animated: true) { + completion(nil) + } } case let .failure(error): diff --git a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift index 5c333c456407..54e928e72600 100644 --- a/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/OutOfTimeCoordinator.swift @@ -9,12 +9,12 @@ import Routing import UIKit -class OutOfTimeCoordinator: Coordinator, Presenting, OutOfTimeViewControllerDelegate, Poppable { +class OutOfTimeCoordinator: Coordinator, Presenting, @preconcurrency OutOfTimeViewControllerDelegate, Poppable { let navigationController: RootContainerViewController let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager - var didFinishPayment: ((OutOfTimeCoordinator) -> Void)? + nonisolated(unsafe) var didFinishPayment: (@Sendable (OutOfTimeCoordinator) -> Void)? var presentedViewController: UIViewController { navigationController diff --git a/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift b/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift index 61fbcd7e9455..6e0e06a08efa 100644 --- a/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/ProfileVoucherCoordinator.swift @@ -44,7 +44,7 @@ final class ProfileVoucherCoordinator: Coordinator, Presentable { } } -extension ProfileVoucherCoordinator: RedeemVoucherViewControllerDelegate { +extension ProfileVoucherCoordinator: @preconcurrency RedeemVoucherViewControllerDelegate { func redeemVoucherDidSucceed( _ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse @@ -59,7 +59,7 @@ extension ProfileVoucherCoordinator: RedeemVoucherViewControllerDelegate { } } -extension ProfileVoucherCoordinator: AddCreditSucceededViewControllerDelegate { +extension ProfileVoucherCoordinator: @preconcurrency AddCreditSucceededViewControllerDelegate { func addCreditSucceededViewControllerDidFinish(in controller: AddCreditSucceededViewController) { didFinish?(self) } diff --git a/ios/MullvadVPN/Coordinators/RelayFilterCoordinator.swift b/ios/MullvadVPN/Coordinators/RelayFilterCoordinator.swift index 39287b3ec2b5..02d65b023a42 100644 --- a/ios/MullvadVPN/Coordinators/RelayFilterCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/RelayFilterCoordinator.swift @@ -11,7 +11,7 @@ import MullvadTypes import Routing import UIKit -class RelayFilterCoordinator: Coordinator, Presentable, RelayCacheTrackerObserver { +class RelayFilterCoordinator: Coordinator, Presentable, @preconcurrency RelayCacheTrackerObserver { private let tunnelManager: TunnelManager private let relayCacheTracker: RelayCacheTracker private var cachedRelays: CachedRelays? diff --git a/ios/MullvadVPN/Coordinators/SafariCoordinator.swift b/ios/MullvadVPN/Coordinators/SafariCoordinator.swift index eca261faeac4..cca28e075c43 100644 --- a/ios/MullvadVPN/Coordinators/SafariCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/SafariCoordinator.swift @@ -10,8 +10,9 @@ import Foundation import Routing import SafariServices -class SafariCoordinator: Coordinator, Presentable, SFSafariViewControllerDelegate { - var didFinish: (() -> Void)? +@MainActor +class SafariCoordinator: Coordinator, Presentable, @preconcurrency SFSafariViewControllerDelegate { + nonisolated(unsafe) var didFinish: (@Sendable () -> Void)? var presentedViewController: UIViewController { safariController diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift index d6ea38a42660..33d08703073d 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Add/AddAccessMethodCoordinator.swift @@ -81,7 +81,7 @@ class AddAccessMethodCoordinator: Coordinator, Presentable, Presenting { } } -extension AddAccessMethodCoordinator: MethodSettingsViewControllerDelegate { +extension AddAccessMethodCoordinator: @preconcurrency MethodSettingsViewControllerDelegate { func accessMethodDidSave(_ accessMethod: PersistentAccessMethod) { dismiss(animated: true) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentView.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentView.swift index c41123906d8e..a3b0009b460c 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentView.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Cells/TextCellContentView.swift @@ -9,7 +9,7 @@ import UIKit /// Content view presenting a label and text field. -class TextCellContentView: UIView, UIContentView, UIGestureRecognizerDelegate { +class TextCellContentView: UIView, UIContentView, UIGestureRecognizerDelegate, Sendable { private var textLabel = UILabel() private var textField = CustomTextField() @@ -175,6 +175,7 @@ extension TextCellContentView: UITextFieldDelegate { } extension TextCellContentConfiguration.TextFieldProperties { + @MainActor func apply(to textField: CustomTextField) { textField.font = font textField.backgroundColor = .clear @@ -197,6 +198,7 @@ extension TextCellContentConfiguration.TextFieldProperties { } extension TextCellContentConfiguration.EditingEvents { + @MainActor func register(in textField: UITextField) { onChange.map { textField.addAction($0, for: .editingChanged) } onBegin.map { textField.addAction($0, for: .editingDidBegin) } @@ -204,6 +206,7 @@ extension TextCellContentConfiguration.EditingEvents { onEndOnExit.map { textField.addAction($0, for: .editingDidEndOnExit) } } + @MainActor func unregister(from textField: UITextField) { onChange.map { textField.removeAction($0, for: .editingChanged) } onBegin.map { textField.removeAction($0, for: .editingDidBegin) } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift index 81d9e9fc02ae..7a32cd4aa128 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/ShadowsocksSectionHandler.swift @@ -10,6 +10,7 @@ import Combine import UIKit /// Type responsible for handling cells in shadowsocks table view section. +@MainActor struct ShadowsocksSectionHandler { private let authenticationInputMaxLength = 2048 diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift index b5c3acfb694b..8ee9eb752f15 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Common/SocksSectionHandler.swift @@ -16,6 +16,7 @@ struct SocksSectionHandler { let tableStyle: UITableView.Style let subject: CurrentValueSubject + @MainActor func configure(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { switch itemIdentifier { case .server: @@ -31,6 +32,7 @@ struct SocksSectionHandler { } } + @MainActor private func configureServer(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { var contentConfiguration = TextCellContentConfiguration() contentConfiguration.text = itemIdentifier.text @@ -42,6 +44,7 @@ struct SocksSectionHandler { cell.contentConfiguration = contentConfiguration } + @MainActor private func configurePort(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { var contentConfiguration = TextCellContentConfiguration() contentConfiguration.text = itemIdentifier.text @@ -55,6 +58,7 @@ struct SocksSectionHandler { cell.contentConfiguration = contentConfiguration } + @MainActor private func configureAuthentication(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { var contentConfiguration = SwitchCellContentConfiguration() contentConfiguration.text = itemIdentifier.text @@ -64,6 +68,7 @@ struct SocksSectionHandler { cell.contentConfiguration = contentConfiguration } + @MainActor private func configureUsername(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { var contentConfiguration = TextCellContentConfiguration() contentConfiguration.text = itemIdentifier.text @@ -76,6 +81,7 @@ struct SocksSectionHandler { cell.contentConfiguration = contentConfiguration } + @MainActor private func configurePassword(_ cell: UITableViewCell, itemIdentifier: SocksItemIdentifier) { var contentConfiguration = TextCellContentConfiguration() contentConfiguration.text = itemIdentifier.text diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/CurrentValueSubject+UIActionBindings.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/CurrentValueSubject+UIActionBindings.swift index 9adb159935b9..1ad22507e42f 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/CurrentValueSubject+UIActionBindings.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/CurrentValueSubject+UIActionBindings.swift @@ -14,7 +14,7 @@ extension CurrentValueSubject { /// /// - Parameter keyPath: the key path to the field that should be updated. /// - Returns: an instance of `UIAction`. - func bindTextAction(to keyPath: WritableKeyPath) -> UIAction { + @MainActor func bindTextAction(to keyPath: WritableKeyPath) -> UIAction { UIAction { action in guard let textField = action.sender as? UITextField else { return } @@ -26,7 +26,7 @@ extension CurrentValueSubject { /// /// - Parameter keyPath: the key path to the field that should be updated. /// - Returns: an instance of `UIAction`. - func bindSwitchAction(to keyPath: WritableKeyPath) -> UIAction { + @MainActor func bindSwitchAction(to keyPath: WritableKeyPath) -> UIAction { UIAction { action in guard let toggle = action.sender as? UISwitch else { return } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift index d219a7bf93bc..3f69ed090aa4 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodCoordinator.swift @@ -57,7 +57,7 @@ class EditAccessMethodCoordinator: Coordinator, Presenting { } } -extension EditAccessMethodCoordinator: EditAccessMethodViewControllerDelegate { +extension EditAccessMethodCoordinator: @preconcurrency EditAccessMethodViewControllerDelegate { func controllerShouldShowMethodSettings(_ controller: EditAccessMethodViewController) { methodSettingsSubject = getViewModelSubjectFromStore() @@ -127,7 +127,7 @@ extension EditAccessMethodCoordinator: EditAccessMethodViewControllerDelegate { } } -extension EditAccessMethodCoordinator: MethodSettingsViewControllerDelegate { +extension EditAccessMethodCoordinator: @preconcurrency MethodSettingsViewControllerDelegate { func accessMethodDidSave(_ accessMethod: PersistentAccessMethod) { editAccessMethodSubject.value = accessMethod.toViewModel() navigationController.popViewController(animated: true) diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift index 420fd71eab66..c369a96212cb 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/EditAccessMethodInteractor.swift @@ -6,7 +6,7 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // -import Combine +@preconcurrency import Combine import Foundation import MullvadSettings @@ -25,7 +25,7 @@ struct EditAccessMethodInteractor: EditAccessMethodInteractorProtocol { repository.delete(id: subject.value.id) } - func startProxyConfigurationTest(_ completion: ((Bool) -> Void)?) { + func startProxyConfigurationTest(_ completion: (@Sendable (Bool) -> Void)?) { guard let config = try? subject.value.intoPersistentProxyConfiguration() else { return } let subject = subject diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift index 5b2b48a59301..5793c079c3eb 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsCellConfiguration.swift @@ -10,6 +10,7 @@ import Combine import MullvadTypes import UIKit +@MainActor class MethodSettingsCellConfiguration { private let subject: CurrentValueSubject private let tableView: UITableView @@ -25,7 +26,7 @@ class MethodSettingsCellConfiguration { self.subject = subject } - func dequeueCell( + @MainActor func dequeueCell( at indexPath: IndexPath, for itemIdentifier: MethodSettingsItemIdentifier, contentValidationErrors: [AccessMethodFieldValidationError] diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsDataSourceConfiguration.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsDataSourceConfiguration.swift index 7e88cfc1c55e..092a3d76fe0a 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsDataSourceConfiguration.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsDataSourceConfiguration.swift @@ -20,6 +20,7 @@ class MethodSettingsDataSourceConfiguration { self.dataSource = dataSource } + @MainActor func updateDataSource( previousValue: AccessMethodViewModel?, newValue: AccessMethodViewModel, @@ -100,6 +101,7 @@ class MethodSettingsDataSourceConfiguration { dataSource?.apply(snapshot, animatingDifferences: animated, completion: completion) } + @MainActor func updateDataSourceWithContentValidationErrors(viewModel: AccessMethodViewModel) { guard var snapshot = dataSource?.snapshot() else { return diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift index 7828a37ba817..58763a255196 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/MethodSettings/MethodSettingsViewController.swift @@ -298,7 +298,9 @@ class MethodSettingsViewController: UITableViewController { saveBarButton.isEnabled = false interactor.startProxyConfigurationTest { [weak self] _ in - self?.onTestCompleted() + Task { @MainActor in + self?.onTestCompleted() + } } } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationInteractorProtocol.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationInteractorProtocol.swift index cd7d24d2ae96..80a1076fc760 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationInteractorProtocol.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Edit/ProxyConfigurationInteractorProtocol.swift @@ -18,7 +18,7 @@ protocol ProxyConfigurationInteractorProtocol { /// the UI accordingly. /// /// - Parameter completion: completion handler receiving `true` if the test succeeded, otherwise `false`. - func startProxyConfigurationTest(_ completion: ((Bool) -> Void)?) + func startProxyConfigurationTest(_ completion: (@Sendable (Bool) -> Void)?) /// Cancel currently running configuration test. /// The interactor is expected to reset the testing status to the initial. diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift index f5383c6d19ee..346c14158a7f 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/List/ListAccessMethodCoordinator.swift @@ -130,7 +130,7 @@ class ListAccessMethodCoordinator: Coordinator, Presenting, SettingsChildCoordin } } -extension ListAccessMethodCoordinator: ListAccessMethodViewControllerDelegate { +extension ListAccessMethodCoordinator: @preconcurrency ListAccessMethodViewControllerDelegate { func controllerShouldShowAbout(_ controller: ListAccessMethodViewController) { about() } diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/AccessMethodProtocolPicker.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/AccessMethodProtocolPicker.swift index e11246635ced..f6c64905b195 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/AccessMethodProtocolPicker.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/AccessMethodProtocolPicker.swift @@ -18,6 +18,7 @@ struct AccessMethodProtocolPicker { /// - Parameters: /// - currentValue: current selection. /// - completion: a completion handler. + @MainActor func present(currentValue: AccessMethodKind, completion: @escaping (AccessMethodKind) -> Void) { let navigationController = navigationController diff --git a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift index c858aa472625..97721651c8c4 100644 --- a/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift +++ b/ios/MullvadVPN/Coordinators/Settings/APIAccess/Pickers/ShadowsocksCipherPicker.swift @@ -10,6 +10,7 @@ import MullvadSettings import UIKit /// Type implementing the shadowsocks cipher picker. +@MainActor struct ShadowsocksCipherPicker { /// The navigation controller used for presenting the picker. let navigationController: UINavigationController diff --git a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift index b2f9fe51645f..a5cd3d484ca9 100644 --- a/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/IPOverride/IPOverrideCoordinator.swift @@ -40,7 +40,7 @@ class IPOverrideCoordinator: Coordinator, Presenting, SettingsChildCoordinator { } } -extension IPOverrideCoordinator: IPOverrideViewControllerDelegate { +extension IPOverrideCoordinator: @preconcurrency IPOverrideViewControllerDelegate { func presentImportTextController() { let viewController = IPOverrideTextViewController(interactor: interactor) let customNavigationController = CustomNavigationController(rootViewController: viewController) diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift index f8c501ff11f6..f82aa035c2bc 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsCoordinator.swift @@ -38,14 +38,18 @@ enum SettingsNavigationRoute: Equatable { } /// Top-level settings coordinator. +@MainActor final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsViewControllerDelegate, - UINavigationControllerDelegate { + UINavigationControllerDelegate, Sendable { private let logger = Logger(label: "SettingsNavigationCoordinator") private var currentRoute: SettingsNavigationRoute? - private var modalRoute: SettingsNavigationRoute? + nonisolated(unsafe) private var modalRoute: SettingsNavigationRoute? private let interactorFactory: SettingsInteractorFactory private var viewControllerFactory: SettingsViewControllerFactory? + private let accessMethodRepository: AccessMethodRepositoryProtocol + private let proxyConfigurationTester: ProxyConfigurationTesterProtocol + private let ipOverrideRepository: IPOverrideRepository let navigationController: UINavigationController @@ -61,7 +65,7 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV ) -> Void)? /// Event handler invoked when coordinator and its view hierarchy should be dismissed. - var didFinish: ((SettingsCoordinator) -> Void)? + nonisolated(unsafe) var didFinish: (@Sendable (SettingsCoordinator) -> Void)? /// Designated initializer. /// - Parameters: @@ -76,6 +80,9 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV ) { self.navigationController = navigationController self.interactorFactory = interactorFactory + self.accessMethodRepository = accessMethodRepository + self.proxyConfigurationTester = proxyConfigurationTester + self.ipOverrideRepository = ipOverrideRepository super.init() @@ -109,7 +116,11 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV /// - route: the route to present. /// - animated: whether transition should be animated. /// - completion: a completion handler, typically called immediately for horizontal navigation and - func navigate(to route: SettingsNavigationRoute, animated: Bool, completion: (() -> Void)? = nil) { + func navigate( + to route: SettingsNavigationRoute, + animated: Bool, + completion: (@Sendable @MainActor () -> Void)? = nil + ) { switch route { case .root: popToRoot(animated: animated) @@ -182,15 +193,17 @@ final class SettingsCoordinator: Coordinator, Presentable, Presenting, SettingsV // MARK: - SettingsViewControllerDelegate - func settingsViewControllerDidFinish(_ controller: SettingsViewController) { + nonisolated func settingsViewControllerDidFinish(_ controller: SettingsViewController) { didFinish?(self) } - func settingsViewController( + nonisolated func settingsViewController( _ controller: SettingsViewController, didRequestRoutePresentation route: SettingsNavigationRoute ) { - navigate(to: route, animated: true) + Task { + await navigate(to: route, animated: true) + } } // MARK: - Route handling diff --git a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift index f93ebba2dc23..c9228aa36273 100644 --- a/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift +++ b/ios/MullvadVPN/Coordinators/Settings/SettingsViewControllerFactory.swift @@ -11,6 +11,7 @@ import Routing import SwiftUI import UIKit +@MainActor struct SettingsViewControllerFactory { /// The result of creating a child representing a route. enum MakeChildResult { diff --git a/ios/MullvadVPN/Coordinators/SetupAccountCompletedCoordinator.swift b/ios/MullvadVPN/Coordinators/SetupAccountCompletedCoordinator.swift index 93c5532edefa..6101e4be4021 100644 --- a/ios/MullvadVPN/Coordinators/SetupAccountCompletedCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/SetupAccountCompletedCoordinator.swift @@ -34,7 +34,7 @@ class SetupAccountCompletedCoordinator: Coordinator, Presenting { } } -extension SetupAccountCompletedCoordinator: SetupAccountCompletedControllerDelegate { +extension SetupAccountCompletedCoordinator: @preconcurrency SetupAccountCompletedControllerDelegate { func didRequestToSeePrivacy(controller: SetupAccountCompletedController) { presentChild(SafariCoordinator(url: ApplicationConfiguration.privacyGuidesURL), animated: true) } diff --git a/ios/MullvadVPN/Coordinators/VPNSettingsCoordinator.swift b/ios/MullvadVPN/Coordinators/VPNSettingsCoordinator.swift index 8d2db23e57c2..ba9c2584222c 100644 --- a/ios/MullvadVPN/Coordinators/VPNSettingsCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/VPNSettingsCoordinator.swift @@ -42,7 +42,7 @@ class VPNSettingsCoordinator: Coordinator, Presenting, SettingsChildCoordinator } } -extension VPNSettingsCoordinator: VPNSettingsViewControllerDelegate { +extension VPNSettingsCoordinator: @preconcurrency VPNSettingsViewControllerDelegate { func showIPOverrides() { let coordinator = IPOverrideCoordinator( navigationController: navigationController, diff --git a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift index 7ae0f7876e84..0f21f39f1c03 100644 --- a/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift +++ b/ios/MullvadVPN/Coordinators/WelcomeCoordinator.swift @@ -80,7 +80,7 @@ final class WelcomeCoordinator: Coordinator, Poppable, Presenting { } } -extension WelcomeCoordinator: WelcomeViewControllerDelegate { +extension WelcomeCoordinator: @preconcurrency WelcomeViewControllerDelegate { func didRequestToShowInfo(controller: WelcomeViewController) { let message = NSLocalizedString( "WELCOME_DEVICE_CONCEPT_TEXT_DIALOG", diff --git a/ios/MullvadVPN/Extensions/UITextField+Appearance.swift b/ios/MullvadVPN/Extensions/UITextField+Appearance.swift index 16bfb0cf5eb0..7d15c073fb5f 100644 --- a/ios/MullvadVPN/Extensions/UITextField+Appearance.swift +++ b/ios/MullvadVPN/Extensions/UITextField+Appearance.swift @@ -33,6 +33,7 @@ extension UITextField { ) } + @MainActor func apply(to searchBar: UISearchBar) { searchBar.setImage( UIImage(named: "IconCloseSml")?.withTintColor(leftViewTintColor), @@ -43,6 +44,7 @@ extension UITextField { apply(to: searchBar.searchTextField) } + @MainActor func apply(to textField: UITextField) { textField.leftView?.tintColor = leftViewTintColor textField.tintColor = textColor diff --git a/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift index d1b05242bd66..b44e35793b61 100644 --- a/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift +++ b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift @@ -19,8 +19,8 @@ protocol AutoLayoutAnchorsProtocol { var trailingAnchor: NSLayoutXAxisAnchor { get } } -extension UIView: AutoLayoutAnchorsProtocol {} -extension UILayoutGuide: AutoLayoutAnchorsProtocol {} +extension UIView: @preconcurrency AutoLayoutAnchorsProtocol {} +extension UILayoutGuide: @preconcurrency AutoLayoutAnchorsProtocol {} extension UIView { /** @@ -139,6 +139,7 @@ extension UIView { /** Add subviews using AutoLayout and configure constraints. */ + @MainActor func addConstrainedSubviews( _ subviews: [UIView], @AutoLayoutBuilder builder: () -> [NSLayoutConstraint] @@ -201,6 +202,7 @@ struct PinnableEdges { lhs.rectEdge == rhs.rectEdge } + @MainActor func makeConstraint( firstView: AutoLayoutAnchorsProtocol, secondView: AutoLayoutAnchorsProtocol @@ -256,6 +258,7 @@ struct PinnableEdges { /** Returns new constraints pinning edges of the corresponding views. */ + @MainActor func makeConstraints( firstView: AutoLayoutAnchorsProtocol, secondView: AutoLayoutAnchorsProtocol diff --git a/ios/MullvadVPN/Extensions/View+Size.swift b/ios/MullvadVPN/Extensions/View+Size.swift index 34be86795b99..37109237537a 100644 --- a/ios/MullvadVPN/Extensions/View+Size.swift +++ b/ios/MullvadVPN/Extensions/View+Size.swift @@ -24,8 +24,8 @@ extension View { } } -private struct ViewSizeKey: PreferenceKey { - static var defaultValue: CGSize = .zero +private struct ViewSizeKey: PreferenceKey, Sendable { + nonisolated(unsafe) static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift index 6c43b9d7c1fe..cbe95c1ce629 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpiryInAppNotificationProvider.swift @@ -10,7 +10,8 @@ import Foundation import MullvadSettings import MullvadTypes -final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppNotificationProvider { +final class AccountExpiryInAppNotificationProvider: NotificationProvider, InAppNotificationProvider, + @unchecked Sendable { private var accountExpiry = AccountExpiry() private var tunnelObserver: TunnelBlockObserver? private var timer: DispatchSourceTimer? diff --git a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift index 16987abc287f..e0e69f747d1a 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/AccountExpirySystemNotificationProvider.swift @@ -10,7 +10,8 @@ import Foundation import MullvadSettings import UserNotifications -final class AccountExpirySystemNotificationProvider: NotificationProvider, SystemNotificationProvider { +final class AccountExpirySystemNotificationProvider: NotificationProvider, SystemNotificationProvider, + @unchecked Sendable { private var accountExpiry = AccountExpiry() private var tunnelObserver: TunnelBlockObserver? private var accountHasExpired = false diff --git a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift index b9b1a9c8e6e2..ade1b0eb201a 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/RegisteredDeviceInAppNotificationProvider.swift @@ -12,7 +12,7 @@ import UIKit.UIColor import UIKit.UIFont final class RegisteredDeviceInAppNotificationProvider: NotificationProvider, - InAppNotificationProvider { + InAppNotificationProvider, @unchecked Sendable { // MARK: - private properties private let tunnelManager: TunnelManager diff --git a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift index 3e98da16c759..8a0a3b88cee3 100644 --- a/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/Notification Providers/TunnelStatusNotificationProvider.swift @@ -9,7 +9,7 @@ import Foundation import PacketTunnelCore -final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider { +final class TunnelStatusNotificationProvider: NotificationProvider, InAppNotificationProvider, @unchecked Sendable { private var isWaitingForConnectivity = false private var noNetwork = false private var packetTunnelError: BlockedStateReason? diff --git a/ios/MullvadVPN/Notifications/NotificationManager.swift b/ios/MullvadVPN/Notifications/NotificationManager.swift index 3f722a628028..378a9bdc1bf2 100644 --- a/ios/MullvadVPN/Notifications/NotificationManager.swift +++ b/ios/MullvadVPN/Notifications/NotificationManager.swift @@ -48,7 +48,7 @@ final class NotificationManager: NotificationProviderDelegate { } } - static let shared = NotificationManager() + nonisolated(unsafe) static let shared = NotificationManager() private init() {} diff --git a/ios/MullvadVPN/Notifications/NotificationProvider.swift b/ios/MullvadVPN/Notifications/NotificationProvider.swift index ded3aa6b60ab..8e82313010b0 100644 --- a/ios/MullvadVPN/Notifications/NotificationProvider.swift +++ b/ios/MullvadVPN/Notifications/NotificationProvider.swift @@ -16,7 +16,7 @@ protocol NotificationProviderDelegate: AnyObject { } /// Base class for all notification providers. -class NotificationProvider: NotificationProviderProtocol { +class NotificationProvider: NotificationProviderProtocol, @unchecked Sendable { weak var delegate: NotificationProviderDelegate? /** @@ -51,7 +51,7 @@ class NotificationProvider: NotificationProviderProtocol { } } - private func dispatchOnMain(_ block: @escaping () -> Void) { + private func dispatchOnMain(_ block: @escaping @Sendable () -> Void) { if Thread.isMainThread { block() } else { diff --git a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift index fbe02adb2cf3..f2985c7739ae 100644 --- a/ios/MullvadVPN/Operations/ProductsRequestOperation.swift +++ b/ios/MullvadVPN/Operations/ProductsRequestOperation.swift @@ -11,7 +11,7 @@ import Operations import StoreKit final class ProductsRequestOperation: ResultOperation, - SKProductsRequestDelegate { + SKProductsRequestDelegate, @unchecked Sendable { private let productIdentifiers: Set private let maxRetryCount = 10 diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index 8b00f9f26a95..fa1b6cf6e602 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -13,10 +13,10 @@ import MullvadTypes import Operations import UIKit -protocol RelayCacheTrackerProtocol { +protocol RelayCacheTrackerProtocol: Sendable { func startPeriodicUpdates() func stopPeriodicUpdates() - func updateRelays(completionHandler: ((Result) -> Void)?) -> Cancellable + func updateRelays(completionHandler: (@Sendable (Result) -> Void)?) -> Cancellable func getCachedRelays() throws -> CachedRelays func getNextUpdateDate() -> Date func addObserver(_ observer: RelayCacheTrackerObserver) @@ -24,12 +24,12 @@ protocol RelayCacheTrackerProtocol { func refreshCachedRelays() throws } -final class RelayCacheTracker: RelayCacheTrackerProtocol { +final class RelayCacheTracker: RelayCacheTrackerProtocol, @unchecked Sendable { /// Relay update interval. static let relayUpdateInterval: Duration = .hours(1) /// Tracker log. - private let logger = Logger(label: "RelayCacheTracker") + nonisolated(unsafe) private let logger = Logger(label: "RelayCacheTracker") /// Relay cache. private let cache: RelayCacheProtocol @@ -164,7 +164,7 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { timerSource = nil } - func updateRelays(completionHandler: ((Result) -> Void)? = nil) + func updateRelays(completionHandler: (@Sendable (Result) -> Void)? = nil) -> Cancellable { let operation = ResultBlockOperation { finish in let cachedRelays = try? self.getCachedRelays() diff --git a/ios/MullvadVPN/SceneDelegate.swift b/ios/MullvadVPN/SceneDelegate.swift index a79e02745a69..ff5334d8535d 100644 --- a/ios/MullvadVPN/SceneDelegate.swift +++ b/ios/MullvadVPN/SceneDelegate.swift @@ -13,7 +13,7 @@ import MullvadTypes import Operations import UIKit -class SceneDelegate: UIResponder, UIWindowSceneDelegate, SettingsMigrationUIHandler { +class SceneDelegate: UIResponder, UIWindowSceneDelegate, @preconcurrency SettingsMigrationUIHandler { private let logger = Logger(label: "SceneDelegate") var window: UIWindow? diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift index a1324df09fe7..a2eee7be0d97 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelInfo.swift @@ -11,7 +11,8 @@ import Foundation import NetworkExtension -final class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol { +final class SimulatorTunnelProviderSession: SimulatorVPNConnection, VPNTunnelProviderSessionProtocol, + @unchecked Sendable { func sendProviderMessage(_ messageData: Data, responseHandler: ((Data?) -> Void)?) throws { SimulatorTunnelProvider.shared.handleAppMessage( messageData, diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift index 64b8b55b179d..1d8766bbda95 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProvider.swift @@ -27,11 +27,11 @@ class SimulatorTunnelProviderDelegate { } } - func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + func startTunnel(options: [String: NSObject]?, completionHandler: @escaping @Sendable (Error?) -> Void) { completionHandler(nil) } - func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping @Sendable () -> Void) { completionHandler() } @@ -40,11 +40,11 @@ class SimulatorTunnelProviderDelegate { } } -final class SimulatorTunnelProvider { +final class SimulatorTunnelProvider: Sendable { static let shared = SimulatorTunnelProvider() private let lock = NSLock() - private var _delegate: SimulatorTunnelProviderDelegate? + nonisolated(unsafe) private var _delegate: SimulatorTunnelProviderDelegate? var delegate: SimulatorTunnelProviderDelegate! { get { diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift index 3dd21b1351a7..29f9d3c29e48 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderHost.swift @@ -8,7 +8,7 @@ #if targetEnvironment(simulator) -import Foundation +@preconcurrency import Foundation import MullvadLogging import MullvadREST import MullvadSettings @@ -16,7 +16,7 @@ import MullvadTypes import NetworkExtension import PacketTunnelCore -final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { +final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate, @unchecked Sendable { private var observedState: ObservedState = .disconnected private var selectedRelays: SelectedRelays? private let urlRequestProxy: URLRequestProxy @@ -35,7 +35,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { override func startTunnel( options: [String: NSObject]?, - completionHandler: @escaping (Error?) -> Void + completionHandler: @escaping @Sendable (Error?) -> Void ) { dispatchQueue.async { [weak self] in guard let self else { @@ -75,7 +75,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } } - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping @Sendable () -> Void) { dispatchQueue.async { [weak self] in self?.selectedRelays = nil self?.observedState = .disconnected @@ -85,7 +85,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - dispatchQueue.async { + dispatchQueue.sync { do { let message = try TunnelProviderMessage(messageData: messageData) @@ -101,7 +101,11 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { } } - private func handleProviderMessage(_ message: TunnelProviderMessage, completionHandler: ((Data?) -> Void)?) { + private func handleProviderMessage( + _ message: TunnelProviderMessage, + completionHandler: ((Data?) -> Void)? + ) { + nonisolated(unsafe) let handler = completionHandler switch message { case .getTunnelStatus: var reply: Data? @@ -114,7 +118,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { ) } - completionHandler?(reply) + handler?(reply) case let .reconnectTunnel(nextRelay): reasserting = true @@ -146,7 +150,7 @@ final class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { message: "Failed to encode ProxyURLResponse." ) } - completionHandler?(reply) + handler?(reply) } case let .cancelURLRequest(listId): diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift index c8bc1fe7fe93..cc83c239d032 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorTunnelProviderManager.swift @@ -13,7 +13,7 @@ import NetworkExtension final class SimulatorTunnelProviderManager: NSObject, VPNTunnelProviderManagerProtocol { static let tunnelsLock = NSRecursiveLock() - fileprivate static var tunnels = [SimulatorTunnelInfo]() + nonisolated(unsafe) fileprivate static var tunnels = [SimulatorTunnelInfo]() private let lock = NSLock() private var tunnelInfo: SimulatorTunnelInfo diff --git a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift index 5ca0b16f1eb9..28ceadfaf0e4 100644 --- a/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift +++ b/ios/MullvadVPN/SimulatorTunnelProvider/SimulatorVPNConnection.swift @@ -12,7 +12,7 @@ import Foundation import MullvadREST import NetworkExtension -class SimulatorVPNConnection: NSObject, VPNConnectionProtocol { +class SimulatorVPNConnection: NSObject, VPNConnectionProtocol, @unchecked Sendable { // Protocol configuration is automatically synced by `SimulatorTunnelInfo` var protocolConfiguration = NEVPNProtocol() diff --git a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift index acb32083cd21..5da2e911a49a 100644 --- a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift +++ b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift @@ -13,7 +13,8 @@ import MullvadTypes import Operations import StoreKit -class SendStoreReceiptOperation: ResultOperation, SKRequestDelegate { +class SendStoreReceiptOperation: ResultOperation, SKRequestDelegate, + @unchecked Sendable { private let apiProxy: APIQuerying private let accountNumber: String diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift index ce3fd61915ae..cde43da250a4 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift @@ -9,7 +9,7 @@ import Foundation final class StorePaymentBlockObserver: StorePaymentObserver { - typealias BlockHandler = (StorePaymentManager, StorePaymentEvent) -> Void + typealias BlockHandler = @Sendable (StorePaymentManager, StorePaymentEvent) -> Void private let blockHandler: BlockHandler diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift index 7d017e9dd776..ca7a9226ead9 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift @@ -8,10 +8,10 @@ import Foundation import MullvadREST -import StoreKit +@preconcurrency import StoreKit /// The payment event received by observers implementing ``StorePaymentObserver``. -enum StorePaymentEvent { +enum StorePaymentEvent: @unchecked Sendable { /// The payment is successfully completed. case finished(StorePaymentCompletion) @@ -42,7 +42,7 @@ struct StorePaymentCompletion { } /// Failed payment metadata. -struct StorePaymentFailure { +struct StorePaymentFailure: @unchecked Sendable { /// Transaction object, if available. /// May not be available due to account validation failure. let transaction: SKPaymentTransaction? diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index 8086e4216194..b4c06adad955 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -10,13 +10,13 @@ import MullvadLogging import MullvadREST import MullvadTypes import Operations -import StoreKit +@preconcurrency import StoreKit import UIKit /// Manager responsible for handling AppStore payments and passing StoreKit receipts to the backend. /// /// - Warning: only interact with this object on the main queue. -final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { +final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchecked Sendable { private enum OperationCategory { static let sendStoreReceipt = "StorePaymentManager.sendStoreReceipt" static let productsRequest = "StorePaymentManager.productsRequest" @@ -114,7 +114,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { /// - Returns: the request cancellation token func requestProducts( with productIdentifiers: Set, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let productIdentifiers = productIdentifiers.productIdentifiersSet let operation = ProductsRequestOperation( @@ -172,7 +172,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { /// - Returns: the request cancellation token. func restorePurchases( for accountNumber: String, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { logger.debug("Restore purchases.") @@ -222,7 +222,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { /// - completionHandler: completion handler invoked on main queue. The completion block Receives `nil` upon success, otherwise an error. private func validateAccount( accountNumber: String, - completionHandler: @escaping (StorePaymentManagerError?) -> Void + completionHandler: @escaping @Sendable (StorePaymentManagerError?) -> Void ) { let accountOperation = ResultBlockOperation(dispatchQueue: .main) { finish in self.accountsProxy.getAccountData(accountNumber: accountNumber).execute( @@ -255,7 +255,7 @@ final class StorePaymentManager: NSObject, SKPaymentTransactionObserver { private func sendStoreReceipt( accountNumber: String, forceRefresh: Bool, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let operation = SendStoreReceiptOperation( apiProxy: apiProxy, diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift index 8d2481474999..fd6ee850e466 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift @@ -9,7 +9,7 @@ import Foundation import StoreKit -protocol StorePaymentManagerDelegate: AnyObject { +protocol StorePaymentManagerDelegate: AnyObject, Sendable { /// Return the account number associated with the payment. /// Usually called for unfinished transactions coming back after the app was restarted. func storePaymentManager(_ manager: StorePaymentManager, didRequestAccountTokenFor payment: SKPayment) -> String? diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift index 0d6e3584fc99..f504f5062a53 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift @@ -8,7 +8,7 @@ import Foundation -protocol StorePaymentObserver: AnyObject { +protocol StorePaymentObserver: AnyObject, Sendable { func storePaymentManager( _ manager: StorePaymentManager, didReceiveEvent event: StorePaymentEvent diff --git a/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift b/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift index 8d380bdc4480..57153e0616d7 100644 --- a/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift +++ b/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift @@ -12,7 +12,7 @@ import MullvadLogging /// Transaction log responsible for storing and querying processed transactions. /// /// This class is thread safe. -final class StoreTransactionLog { +final class StoreTransactionLog: @unchecked Sendable { private let logger = Logger(label: "StoreTransactionLog") private var transactionIdentifiers: Set = [] private let stateLock = NSLock() diff --git a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift index 713fc4f54ddd..375a052bc008 100644 --- a/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift +++ b/ios/MullvadVPN/TransportMonitor/PacketTunnelTransport.swift @@ -25,7 +25,7 @@ struct PacketTunnelTransport: RESTTransport { func sendRequest( _ request: URLRequest, - completion: @escaping (Data?, URLResponse?, Error?) -> Void + completion: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void ) -> Cancellable { let proxyRequest = ProxyURLRequest( id: UUID(), diff --git a/ios/MullvadVPN/TunnelManager/BackgroundTaskProvider.swift b/ios/MullvadVPN/TunnelManager/BackgroundTaskProvider.swift index de826634e8a3..86529dbf7639 100644 --- a/ios/MullvadVPN/TunnelManager/BackgroundTaskProvider.swift +++ b/ios/MullvadVPN/TunnelManager/BackgroundTaskProvider.swift @@ -12,7 +12,7 @@ import Foundation import UIKit @available(iOSApplicationExtension, unavailable) -public protocol BackgroundTaskProviding { +public protocol BackgroundTaskProviding: Sendable { var backgroundTimeRemaining: TimeInterval { get } #if compiler(>=6) nonisolated @@ -31,9 +31,9 @@ public protocol BackgroundTaskProviding { } @available(iOSApplicationExtension, unavailable) -public class BackgroundTaskProvider: BackgroundTaskProviding { - public var backgroundTimeRemaining: TimeInterval - weak var application: UIApplication! +public final class BackgroundTaskProvider: BackgroundTaskProviding { + nonisolated(unsafe) public var backgroundTimeRemaining: TimeInterval + nonisolated(unsafe) weak var application: UIApplication! public init(backgroundTimeRemaining: TimeInterval, application: UIApplication) { self.backgroundTimeRemaining = backgroundTimeRemaining diff --git a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift index 54f14355a73e..d43a92fcc63a 100644 --- a/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift +++ b/ios/MullvadVPN/TunnelManager/LoadTunnelConfigurationOperation.swift @@ -12,7 +12,7 @@ import MullvadSettings import MullvadTypes import Operations -class LoadTunnelConfigurationOperation: ResultOperation { +class LoadTunnelConfigurationOperation: ResultOperation, @unchecked Sendable { private let logger = Logger(label: "LoadTunnelConfigurationOperation") private let interactor: TunnelInteractor diff --git a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift index 674c59d1860b..fc55fafa9bbd 100644 --- a/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift +++ b/ios/MullvadVPN/TunnelManager/MapConnectionStatusOperation.swift @@ -14,7 +14,7 @@ import NetworkExtension import Operations import PacketTunnelCore -class MapConnectionStatusOperation: AsyncOperation { +class MapConnectionStatusOperation: AsyncOperation, @unchecked Sendable { private let interactor: TunnelInteractor private let connectionStatus: NEVPNStatus private var request: Cancellable? @@ -159,7 +159,7 @@ class MapConnectionStatusOperation: AsyncOperation { private func fetchTunnelStatus( tunnel: any TunnelProtocol, - mapToState: @escaping (ObservedState) -> TunnelState? + mapToState: @escaping @Sendable (ObservedState) -> TunnelState? ) { request = tunnel.getTunnelStatus { [weak self] result in guard let self else { return } diff --git a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift index 99d77a1204a0..2b9c19ce60ba 100644 --- a/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift +++ b/ios/MullvadVPN/TunnelManager/RedeemVoucherOperation.swift @@ -13,7 +13,7 @@ import MullvadSettings import MullvadTypes import Operations -class RedeemVoucherOperation: ResultOperation { +class RedeemVoucherOperation: ResultOperation, @unchecked Sendable { private let logger = Logger(label: "RedeemVoucherOperation") private let interactor: TunnelInteractor diff --git a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift index 0176f02b39e7..7369b937d50a 100644 --- a/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift +++ b/ios/MullvadVPN/TunnelManager/RotateKeyOperation.swift @@ -14,7 +14,7 @@ import MullvadTypes import Operations import WireGuardKitTypes -class RotateKeyOperation: ResultOperation { +class RotateKeyOperation: ResultOperation, @unchecked Sendable { private let logger = Logger(label: "RotateKeyOperation") private let interactor: TunnelInteractor private let devicesProxy: DeviceHandling @@ -35,7 +35,7 @@ class RotateKeyOperation: ResultOperation { } // Create key rotation. - var keyRotation = WgKeyRotation(data: deviceData) + nonisolated(unsafe) var keyRotation = WgKeyRotation(data: deviceData) // Check if key rotation can take place. guard keyRotation.shouldRotate else { diff --git a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift index 47b8562f7fbd..21485ae2ec5a 100644 --- a/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SendTunnelProviderMessageOperation.swift @@ -20,7 +20,7 @@ private let connectingStateWaitDelay: Duration = .seconds(5) /// Default timeout in seconds. private let defaultTimeout: Duration = .seconds(5) -final class SendTunnelProviderMessageOperation: ResultOperation { +final class SendTunnelProviderMessageOperation: ResultOperation, @unchecked Sendable { typealias DecoderHandler = (Data?) throws -> Output private let backgroundTaskProvider: BackgroundTaskProviding diff --git a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift index db6e1cc81436..72fa47168313 100644 --- a/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift +++ b/ios/MullvadVPN/TunnelManager/SetAccountOperation.swift @@ -12,7 +12,7 @@ import MullvadREST import MullvadSettings import MullvadTypes import Operations -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes enum SetAccountAction { /// Set new account. @@ -37,7 +37,7 @@ enum SetAccountAction { } } -class SetAccountOperation: ResultOperation { +class SetAccountOperation: ResultOperation, @unchecked Sendable { private let interactor: TunnelInteractor private let accountsProxy: RESTAccountHandling private let devicesProxy: DeviceHandling @@ -108,7 +108,7 @@ class SetAccountOperation: ResultOperation { Does nothing if device is already logged out. */ - private func startLogoutFlow(completion: @escaping () -> Void) { + private func startLogoutFlow(completion: @escaping @Sendable () -> Void) { switch interactor.deviceState { case let .loggedIn(accountData, deviceData): deleteDevice(accountNumber: accountData.number, deviceIdentifier: deviceData.identifier) { [self] _ in @@ -129,7 +129,7 @@ class SetAccountOperation: ResultOperation { 1. Create new account via API. 2. Call `continueLoginFlow()` passing the result of account creation request. */ - private func startNewAccountFlow(completion: @escaping (Result) -> Void) { + private func startNewAccountFlow(completion: @escaping @Sendable (Result) -> Void) { createAccount { [self] result in continueLoginFlow(result, completion: completion) } @@ -143,7 +143,7 @@ class SetAccountOperation: ResultOperation { */ private func startExistingAccountFlow( accountNumber: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { getAccount(accountNumber: accountNumber) { [self] result in continueLoginFlow(result, completion: completion) @@ -158,7 +158,7 @@ class SetAccountOperation: ResultOperation { */ private func startDeleteAccountFlow( accountNumber: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { deleteAccount(accountNumber: accountNumber) { [self] result in if result.isSuccess { @@ -179,7 +179,7 @@ class SetAccountOperation: ResultOperation { */ private func continueLoginFlow( _ result: Result, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { do { let accountData = try result.get() @@ -234,7 +234,7 @@ class SetAccountOperation: ResultOperation { } /// Create new account and produce `StoredAccountData` upon success. - private func createAccount(completion: @escaping (Result) -> Void) { + private func createAccount(completion: @escaping @Sendable (Result) -> Void) { logger.debug("Create new account...") let task = accountsProxy.createAccount(retryStrategy: .default) { [self] result in @@ -261,7 +261,10 @@ class SetAccountOperation: ResultOperation { } /// Get account data from the API and produce `StoredAccountData` upon success. - private func getAccount(accountNumber: String, completion: @escaping (Result) -> Void) { + private func getAccount( + accountNumber: String, + completion: @escaping @Sendable (Result) -> Void + ) { logger.debug("Request account data...") let task = accountsProxy.getAccountData(accountNumber: accountNumber).execute( @@ -290,7 +293,7 @@ class SetAccountOperation: ResultOperation { } /// Delete account. - private func deleteAccount(accountNumber: String, completion: @escaping (Result) -> Void) { + private func deleteAccount(accountNumber: String, completion: @escaping @Sendable (Result) -> Void) { logger.debug("Delete account...") let task = accountsProxy.deleteAccount( @@ -312,7 +315,11 @@ class SetAccountOperation: ResultOperation { } /// Delete device from API. - private func deleteDevice(accountNumber: String, deviceIdentifier: String, completion: @escaping (Error?) -> Void) { + private func deleteDevice( + accountNumber: String, + deviceIdentifier: String, + completion: @escaping @Sendable (Error?) -> Void + ) { logger.debug("Delete current device...") let task = devicesProxy.deleteDevice( @@ -346,7 +353,7 @@ class SetAccountOperation: ResultOperation { 2. Reset device staate to logged out and persist it. 3. Remove VPN configuration and release an instance of `Tunnel` object. */ - private func unsetDeviceState(completion: @escaping () -> Void) { + private func unsetDeviceState(completion: @escaping @Sendable () -> Void) { // Tell the caller to unsubscribe from VPN status notifications. interactor.prepareForVPNConfigurationDeletion() @@ -379,7 +386,10 @@ class SetAccountOperation: ResultOperation { } /// Create new private key and create new device via API. - private func createDevice(accountNumber: String, completion: @escaping (Result) -> Void) { + private func createDevice( + accountNumber: String, + completion: @escaping @Sendable (Result) -> Void + ) { let privateKey = PrivateKey() let request = REST.CreateDeviceRequest(publicKey: privateKey.publicKey, hijackDNS: false) @@ -417,7 +427,7 @@ class SetAccountOperation: ResultOperation { private func findDevice( accountNumber: String, publicKey: PublicKey, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let task = devicesProxy.getDevices(accountNumber: accountNumber, retryStrategy: .default) { [self] result in dispatchQueue.async { [self] in diff --git a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift index c6b712061ccc..923b33ad361b 100644 --- a/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StartTunnelOperation.swift @@ -13,7 +13,7 @@ import NetworkExtension import Operations import PacketTunnelCore -class StartTunnelOperation: ResultOperation { +class StartTunnelOperation: ResultOperation, @unchecked Sendable { typealias EncodeErrorHandler = (Error) -> Void private let interactor: TunnelInteractor @@ -58,7 +58,7 @@ class StartTunnelOperation: ResultOperation { } } - private func makeTunnelProviderAndStartTunnel(completionHandler: @escaping (Error?) -> Void) { + private func makeTunnelProviderAndStartTunnel(completionHandler: @escaping @Sendable (Error?) -> Void) { makeTunnelProvider { result in self.dispatchQueue.async { do { @@ -100,7 +100,10 @@ class StartTunnelOperation: ResultOperation { try tunnel.start(options: tunnelOptions.rawOptions()) } - private func makeTunnelProvider(completionHandler: @escaping (Result) -> Void) { + private func makeTunnelProvider( + completionHandler: @escaping @Sendable (Result) + -> Void + ) { let persistentTunnels = interactor.getPersistentTunnels() let tunnel = persistentTunnels.first ?? interactor.createNewTunnel() let configuration = Self.makeTunnelConfiguration() diff --git a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift index 1e8362c2ca7b..ba41debe306a 100644 --- a/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift +++ b/ios/MullvadVPN/TunnelManager/StopTunnelOperation.swift @@ -9,7 +9,7 @@ import Foundation import Operations -class StopTunnelOperation: ResultOperation { +class StopTunnelOperation: ResultOperation, @unchecked Sendable { private let interactor: TunnelInteractor init( diff --git a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift index 280420d08684..40f52b1afe1c 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel+Messaging.swift @@ -25,7 +25,7 @@ extension TunnelProtocol { /// Request packet tunnel process to reconnect the tunnel with the given relays. func reconnectTunnel( to nextRelays: NextRelays, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, @@ -43,7 +43,7 @@ extension TunnelProtocol { /// Request status from packet tunnel process. func getTunnelStatus( - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let decoderHandler: (Data?) throws -> ObservedState = { data in if let data { @@ -69,7 +69,7 @@ extension TunnelProtocol { /// Send HTTP request via packet tunnel process bypassing VPN. func sendRequest( _ proxyRequest: ProxyURLRequest, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let decoderHandler: (Data?) throws -> ProxyURLResponse = { data in if let data { @@ -111,7 +111,7 @@ extension TunnelProtocol { /// Notify tunnel about private key rotation. func notifyKeyRotation( - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { let operation = SendTunnelProviderMessageOperation( dispatchQueue: dispatchQueue, diff --git a/ios/MullvadVPN/TunnelManager/Tunnel.swift b/ios/MullvadVPN/TunnelManager/Tunnel.swift index 9489998ef68b..6324cbc01b4b 100644 --- a/ios/MullvadVPN/TunnelManager/Tunnel.swift +++ b/ios/MullvadVPN/TunnelManager/Tunnel.swift @@ -21,7 +21,7 @@ protocol TunnelStatusObserver { func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) } -protocol TunnelProtocol: AnyObject { +protocol TunnelProtocol: AnyObject, Sendable { associatedtype TunnelManagerProtocol: VPNTunnelProviderManagerProtocol var status: NEVPNStatus { get } var isOnDemandEnabled: Bool { get set } @@ -49,7 +49,7 @@ protocol TunnelProtocol: AnyObject { } /// Tunnel wrapper class. -final class Tunnel: TunnelProtocol, Equatable { +final class Tunnel: TunnelProtocol, Equatable, @unchecked Sendable { /// Unique identifier assigned to instance at the time of creation. let identifier = UUID() diff --git a/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift index 050fb1fdf26c..abe9462db7b8 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelBlockObserver.swift @@ -9,7 +9,7 @@ import Foundation import MullvadSettings -final class TunnelBlockObserver: TunnelObserver { +final class TunnelBlockObserver: TunnelObserver, @unchecked Sendable { typealias DidLoadConfigurationHandler = (TunnelManager) -> Void typealias DidUpdateTunnelStatusHandler = (TunnelManager, TunnelStatus) -> Void typealias DidUpdateDeviceStateHandler = ( diff --git a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift index cb268ef33038..e1f0740dca0d 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelInteractor.swift @@ -25,7 +25,7 @@ protocol TunnelInteractor { // MARK: - Tunnel status var tunnelStatus: TunnelStatus { get } - @discardableResult func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus + @discardableResult func updateTunnelStatus(_ block: @Sendable (inout TunnelStatus) -> Void) -> TunnelStatus // MARK: - Configuration diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index 4703fe3cda85..428dcc4fcc8d 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -28,8 +28,8 @@ private let establishedTunnelStatusPollInterval: Duration = .seconds(5) /// A class that provides a convenient interface for VPN tunnels configuration, manipulation and /// monitoring. -final class TunnelManager: StorePaymentObserver { - private enum OperationCategory: String { +final class TunnelManager: StorePaymentObserver, @unchecked Sendable { + private enum OperationCategory: String, Sendable { case manageTunnel case deviceStateUpdate case settingsUpdate @@ -180,7 +180,7 @@ final class TunnelManager: StorePaymentObserver { // MARK: - Public methods - func loadConfiguration(completionHandler: @escaping () -> Void) { + func loadConfiguration(completionHandler: @escaping @Sendable () -> Void) { let loadTunnelOperation = LoadTunnelConfigurationOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self) @@ -218,12 +218,13 @@ final class TunnelManager: StorePaymentObserver { } func startTunnel(completionHandler: ((Error?) -> Void)? = nil) { + nonisolated(unsafe) let nonisolatedCompletionHandler = completionHandler + let operation = StartTunnelOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self), completionHandler: { [weak self] result in guard let self else { return } - DispatchQueue.main.async { if let error = result.error { self.logger.error( @@ -238,7 +239,7 @@ final class TunnelManager: StorePaymentObserver { } } - completionHandler?(result.error) + nonisolatedCompletionHandler?(result.error) } } ) @@ -254,6 +255,7 @@ final class TunnelManager: StorePaymentObserver { } func stopTunnel(completionHandler: ((Error?) -> Void)? = nil) { + nonisolated(unsafe) let nonisolatedCompletionHandler = completionHandler let operation = StopTunnelOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self) @@ -274,7 +276,7 @@ final class TunnelManager: StorePaymentObserver { } } - completionHandler?(result.error) + nonisolatedCompletionHandler?(result.error) } } @@ -288,7 +290,7 @@ final class TunnelManager: StorePaymentObserver { operationQueue.addOperation(operation) } - func reconnectTunnel(selectNewRelay: Bool, completionHandler: ((Error?) -> Void)? = nil) { + func reconnectTunnel(selectNewRelay: Bool, completionHandler: (@Sendable (Error?) -> Void)? = nil) { let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { finish -> Cancellable in do { guard let tunnel = self.tunnel else { @@ -335,7 +337,7 @@ final class TunnelManager: StorePaymentObserver { private func setAccount( action: SetAccountAction, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) { let operation = SetAccountOperation( dispatchQueue: internalQueue, @@ -394,7 +396,7 @@ final class TunnelManager: StorePaymentObserver { _ = try? await setAccount(action: .unset) } - func updateAccountData(_ completionHandler: ((Error?) -> Void)? = nil) { + func updateAccountData(_ completionHandler: (@Sendable (Error?) -> Void)? = nil) { let operation = UpdateAccountDataOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self), @@ -423,7 +425,7 @@ final class TunnelManager: StorePaymentObserver { func redeemVoucher( _ voucherCode: String, - completion: ((Result) -> Void)? = nil + completion: (@Sendable (Result) -> Void)? = nil ) -> Cancellable { let operation = RedeemVoucherOperation( dispatchQueue: internalQueue, @@ -453,7 +455,7 @@ final class TunnelManager: StorePaymentObserver { _ = try await setAccount(action: .delete(accountNumber)) } - func updateDeviceData(_ completionHandler: ((Error?) -> Void)? = nil) { + func updateDeviceData(_ completionHandler: (@Sendable (Error?) -> Void)? = nil) { let operation = UpdateDeviceDataOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self), @@ -480,7 +482,7 @@ final class TunnelManager: StorePaymentObserver { operationQueue.addOperation(operation) } - func rotatePrivateKey(completionHandler: @escaping (Error?) -> Void) -> Cancellable { + func rotatePrivateKey(completionHandler: @escaping @Sendable (Error?) -> Void) -> Cancellable { let operation = RotateKeyOperation( dispatchQueue: internalQueue, interactor: TunnelInteractorProxy(self), @@ -518,7 +520,7 @@ final class TunnelManager: StorePaymentObserver { return operation } - func updateSettings(_ updates: [TunnelSettingsUpdate], completionHandler: (() -> Void)? = nil) { + func updateSettings(_ updates: [TunnelSettingsUpdate], completionHandler: (@Sendable () -> Void)? = nil) { let taskName = "Set " + updates.map(\.subjectName).joined(separator: ", ") scheduleSettingsUpdate( taskName: taskName, @@ -659,7 +661,7 @@ final class TunnelManager: StorePaymentObserver { } } - fileprivate func setTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus { + fileprivate func setTunnelStatus(_ block: @Sendable (inout TunnelStatus) -> Void) -> TunnelStatus { nslock.lock() defer { nslock.unlock() } @@ -676,12 +678,12 @@ final class TunnelManager: StorePaymentObserver { // Packet tunnel may have attempted or rotated the key. // In that case we have to reload device state from Keychain as it's likely was modified by packet tunnel. - let newPacketTunnelKeyRotation = newTunnelStatus.observedState.connectionState?.lastKeyRotation + let newPacketTunnelKeyRotation = _tunnelStatus.observedState.connectionState?.lastKeyRotation if lastPacketTunnelKeyRotation != newPacketTunnelKeyRotation { lastPacketTunnelKeyRotation = newPacketTunnelKeyRotation refreshDeviceState() } - switch newTunnelStatus.state { + switch _tunnelStatus.state { case .connecting, .reconnecting, .negotiatingEphemeralPeer: // Start polling tunnel status to keep the relay information up to date // while the tunnel process is trying to connect. @@ -709,7 +711,7 @@ final class TunnelManager: StorePaymentObserver { DispatchQueue.main.async { self.observerList.notify { observer in - observer.tunnelManager(self, didUpdateTunnelStatus: newTunnelStatus) + observer.tunnelManager(self, didUpdateTunnelStatus: self._tunnelStatus) } } @@ -933,8 +935,8 @@ final class TunnelManager: StorePaymentObserver { private func scheduleSettingsUpdate( taskName: String, - modificationBlock: @escaping (inout LatestTunnelSettings) -> Void, - completionHandler: (() -> Void)? + modificationBlock: @escaping @Sendable (inout LatestTunnelSettings) -> Void, + completionHandler: (@Sendable () -> Void)? ) { let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { let currentSettings = self._tunnelSettings @@ -972,8 +974,8 @@ final class TunnelManager: StorePaymentObserver { private func scheduleDeviceStateUpdate( taskName: String, reconnectTunnel: Bool = true, - modificationBlock: @escaping (inout DeviceState) -> Void, - completionHandler: (() -> Void)? = nil + modificationBlock: @escaping @Sendable (inout DeviceState) -> Void, + completionHandler: (@Sendable () -> Void)? = nil ) { let operation = AsyncBlockOperation(dispatchQueue: internalQueue) { var deviceState = self.deviceState @@ -1071,7 +1073,7 @@ final class TunnelManager: StorePaymentObserver { } } - private func unsetTunnelConfiguration(completion: @escaping () -> Void) { + private func unsetTunnelConfiguration(completion: @escaping @Sendable () -> Void) { // Tell the caller to unsubscribe from VPN status notifications. prepareForVPNConfigurationDeletion() @@ -1224,7 +1226,7 @@ private struct TunnelInteractorProxy: TunnelInteractor { tunnelManager.tunnelStatus } - func updateTunnelStatus(_ block: (inout TunnelStatus) -> Void) -> TunnelStatus { + func updateTunnelStatus(_ block: @Sendable (inout TunnelStatus) -> Void) -> TunnelStatus { tunnelManager.setTunnelStatus(block) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift index 0ae9fd5f636d..5b41a809cc8b 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelObserver.swift @@ -9,7 +9,7 @@ import Foundation import MullvadSettings -protocol TunnelObserver: AnyObject { +protocol TunnelObserver: AnyObject, Sendable { func tunnelManagerDidLoadConfiguration(_ manager: TunnelManager) func tunnelManager(_ manager: TunnelManager, didUpdateTunnelStatus tunnelStatus: TunnelStatus) func tunnelManager( diff --git a/ios/MullvadVPN/TunnelManager/TunnelState.swift b/ios/MullvadVPN/TunnelManager/TunnelState.swift index ea55431d5a3e..4bca099da233 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelState.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelState.swift @@ -10,10 +10,10 @@ import Foundation import MullvadREST import MullvadTypes import PacketTunnelCore -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /// A struct describing the tunnel status. -struct TunnelStatus: Equatable, CustomStringConvertible { +struct TunnelStatus: Equatable, CustomStringConvertible, Sendable { /// Tunnel status returned by tunnel process. var observedState: ObservedState = .disconnected @@ -38,7 +38,7 @@ struct TunnelStatus: Equatable, CustomStringConvertible { } /// An enum that describes the tunnel state. -enum TunnelState: Equatable, CustomStringConvertible { +enum TunnelState: Equatable, CustomStringConvertible, Sendable { enum WaitingForConnectionReason { /// Tunnel connection is down. case noConnection diff --git a/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift b/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift index 135e79220c7a..37ce71a65e6a 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelStatusBlockObserver.swift @@ -9,7 +9,7 @@ import Foundation import NetworkExtension -final class TunnelStatusBlockObserver: TunnelStatusObserver { +final class TunnelStatusBlockObserver: TunnelStatusObserver, @unchecked Sendable { typealias Handler = (any TunnelProtocol, NEVPNStatus) -> Void private weak var tunnel: (any TunnelProtocol)? @@ -27,7 +27,7 @@ final class TunnelStatusBlockObserver: TunnelStatusObserver { } func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) { - let block = { + let block: @Sendable () -> Void = { self.handler(tunnel, status) } diff --git a/ios/MullvadVPN/TunnelManager/TunnelStore.swift b/ios/MullvadVPN/TunnelManager/TunnelStore.swift index fa1fb0f3d712..0fcb75dcbef3 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelStore.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelStore.swift @@ -12,14 +12,14 @@ import MullvadTypes import NetworkExtension import UIKit -protocol TunnelStoreProtocol { +protocol TunnelStoreProtocol: Sendable { associatedtype TunnelType: TunnelProtocol, Equatable func getPersistentTunnels() -> [TunnelType] func createNewTunnel() -> TunnelType } /// Wrapper around system VPN tunnels. -final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver { +final class TunnelStore: TunnelStoreProtocol, TunnelStatusObserver, @unchecked Sendable { typealias TunnelType = Tunnel private let logger = Logger(label: "TunnelStore") private let lock = NSLock() diff --git a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift index 3992f17c9031..9c947732394b 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateAccountDataOperation.swift @@ -13,7 +13,7 @@ import MullvadSettings import MullvadTypes import Operations -class UpdateAccountDataOperation: ResultOperation { +class UpdateAccountDataOperation: ResultOperation, @unchecked Sendable { private let logger = Logger(label: "UpdateAccountDataOperation") private let interactor: TunnelInteractor private let accountsProxy: RESTAccountHandling diff --git a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift index 341af9080653..6d720987ac29 100644 --- a/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift +++ b/ios/MullvadVPN/TunnelManager/UpdateDeviceDataOperation.swift @@ -14,7 +14,7 @@ import MullvadTypes import Operations import WireGuardKitTypes -class UpdateDeviceDataOperation: ResultOperation { +class UpdateDeviceDataOperation: ResultOperation, @unchecked Sendable { private let interactor: TunnelInteractor private let devicesProxy: DeviceHandling @@ -42,7 +42,7 @@ class UpdateDeviceDataOperation: ResultOperation { identifier: deviceData.identifier, retryStrategy: .default, completion: { [weak self] result in - self?.dispatchQueue.async { + self?.dispatchQueue.async { [weak self] in self?.didReceiveDeviceResponse(result: result) } } diff --git a/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift b/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift index 89474e5afd76..f21dc7dedb7f 100644 --- a/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift +++ b/ios/MullvadVPN/TunnelManager/WgKeyRotation.swift @@ -9,13 +9,13 @@ import Foundation import MullvadSettings import MullvadTypes -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /** Implements manipulations related to marking the beginning and the completion of key rotation, private key creation and other tasks relevant to handling the state of key rotation. */ -struct WgKeyRotation { +struct WgKeyRotation: Sendable { /// Private key rotation interval counted from the time when the key was successfully pushed /// to the backend. public static let rotationInterval: Duration = .days(30) diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index f5cc3a77a52b..059a9ac08e9b 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -13,16 +13,16 @@ import MullvadTypes import Operations import StoreKit -final class AccountInteractor { +final class AccountInteractor: Sendable { private let storePaymentManager: StorePaymentManager let tunnelManager: TunnelManager let accountsProxy: RESTAccountHandling - var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)? - var didReceiveDeviceState: ((DeviceState) -> Void)? + nonisolated(unsafe) var didReceivePaymentEvent: (@Sendable (StorePaymentEvent) -> Void)? + nonisolated(unsafe) var didReceiveDeviceState: (@Sendable (DeviceState) -> Void)? - private var tunnelObserver: TunnelObserver? - private var paymentObserver: StorePaymentObserver? + nonisolated(unsafe) private var tunnelObserver: TunnelObserver? + nonisolated(unsafe) private var paymentObserver: StorePaymentObserver? init( storePaymentManager: StorePaymentManager, @@ -63,7 +63,7 @@ final class AccountInteractor { func restorePurchases( for accountNumber: String, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { storePaymentManager.restorePurchases( for: accountNumber, @@ -73,7 +73,7 @@ final class AccountInteractor { func requestProducts( with productIdentifiers: Set, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { storePaymentManager.requestProducts( with: productIdentifiers, diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index f39a228e6681..b3959bc14d72 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -14,7 +14,7 @@ import Operations import StoreKit import UIKit -enum AccountViewControllerAction { +enum AccountViewControllerAction: Sendable { case deviceInfo case finish case logOut @@ -23,7 +23,7 @@ enum AccountViewControllerAction { case restorePurchasesInfo } -class AccountViewController: UIViewController { +class AccountViewController: UIViewController, @unchecked Sendable { typealias ActionHandler = (AccountViewControllerAction) -> Void private let interactor: AccountInteractor @@ -91,11 +91,15 @@ class AccountViewController: UIViewController { } interactor.didReceiveDeviceState = { [weak self] deviceState in - self?.updateView(from: deviceState) + Task { @MainActor in + self?.updateView(from: deviceState) + } } interactor.didReceivePaymentEvent = { [weak self] event in - self?.didReceivePaymentEvent(event) + Task { @MainActor in + self?.didReceivePaymentEvent(event) + } } configUI() addActions() @@ -155,10 +159,13 @@ class AccountViewController: UIViewController { let productState: ProductState = completion.value?.products.first .map { .received($0) } ?? .failed - self?.setProductState(productState, animated: true) + MainActor.assumeIsolated { + self?.setProductState(productState, animated: true) + } } } + @MainActor private func setPaymentState(_ newState: PaymentState, animated: Bool) { paymentState = newState @@ -279,18 +286,20 @@ class AccountViewController: UIViewController { _ = interactor.restorePurchases(for: accountData.number) { [weak self] completion in guard let self else { return } - switch completion { - case let .success(response): - errorPresenter.showAlertForResponse(response, context: .restoration) + Task { @MainActor in + switch completion { + case let .success(response): + errorPresenter.showAlertForResponse(response, context: .restoration) - case let .failure(error as StorePaymentManagerError): - errorPresenter.showAlertForError(error, context: .restoration) + case let .failure(error as StorePaymentManagerError): + errorPresenter.showAlertForError(error, context: .restoration) - default: - break - } + default: + break + } - setPaymentState(.none, animated: true) + setPaymentState(.none, animated: true) + } } } } diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift index 0192f3fdd306..32b9218c6755 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift @@ -12,10 +12,10 @@ import Routing struct PaymentAlertPresenter { let alertContext: any Presenting - func showAlertForError( + @MainActor func showAlertForError( _ error: StorePaymentManagerError, context: REST.CreateApplePaymentResponse.Context, - completion: (() -> Void)? = nil + completion: (@Sendable () -> Void)? = nil ) { let presentation = AlertPresentation( id: "payment-error-alert", @@ -36,10 +36,11 @@ struct PaymentAlertPresenter { presenter.showAlert(presentation: presentation, animated: true) } + @MainActor func showAlertForResponse( _ response: REST.CreateApplePaymentResponse, context: REST.CreateApplePaymentResponse.Context, - completion: (() -> Void)? = nil + completion: (@Sendable () -> Void)? = nil ) { guard case .noTimeAdded = response else { completion?() diff --git a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionInteractor.swift b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionInteractor.swift index f0cf80ab8709..2b4ef5fa2910 100644 --- a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionInteractor.swift +++ b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionInteractor.swift @@ -26,7 +26,7 @@ enum AccountDeletionError: LocalizedError { } } -class AccountDeletionInteractor { +final class AccountDeletionInteractor: Sendable { private let tunnelManager: TunnelManager var viewModel: AccountDeletionViewModel { AccountDeletionViewModel( diff --git a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionViewController.swift b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionViewController.swift index 6479134b1426..0ca4f7cbcde8 100644 --- a/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionViewController.swift +++ b/ios/MullvadVPN/View controllers/AccountDeletion/AccountDeletionViewController.swift @@ -14,6 +14,7 @@ protocol AccountDeletionViewControllerDelegate: AnyObject { func deleteAccountDidCancel(controller: AccountDeletionViewController) } +@MainActor class AccountDeletionViewController: UIViewController { private lazy var contentView: AccountDeletionContentView = { let view = AccountDeletionContentView() @@ -22,7 +23,7 @@ class AccountDeletionViewController: UIViewController { }() weak var delegate: AccountDeletionViewControllerDelegate? - var interactor: AccountDeletionInteractor + let interactor: AccountDeletionInteractor init(interactor: AccountDeletionInteractor) { self.interactor = interactor @@ -75,7 +76,7 @@ class AccountDeletionViewController: UIViewController { } } -extension AccountDeletionViewController: AccountDeletionContentViewDelegate { +extension AccountDeletionViewController: @preconcurrency AccountDeletionContentViewDelegate { func didTapCancelButton(contentView: AccountDeletionContentView, button: AppButton) { contentView.isEditing = false delegate?.deleteAccountDidCancel(controller: self) diff --git a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift index 854076784fb1..8cea541cd166 100644 --- a/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Alert/AlertPresenter.swift @@ -8,6 +8,7 @@ import Routing +@MainActor struct AlertPresenter { weak var context: (any Presenting)? diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift index a7f4cb91eaeb..116814028d30 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Completed/SetupAccountCompletedController.swift @@ -8,7 +8,7 @@ import UIKit -protocol SetupAccountCompletedControllerDelegate: AnyObject { +protocol SetupAccountCompletedControllerDelegate: AnyObject, Sendable { func didRequestToSeePrivacy(controller: SetupAccountCompletedController) func didRequestToStartTheApp(controller: SetupAccountCompletedController) } @@ -51,7 +51,7 @@ class SetupAccountCompletedController: UIViewController, RootContainment { } } -extension SetupAccountCompletedController: SetupAccountCompletedContentViewDelegate { +extension SetupAccountCompletedController: @preconcurrency SetupAccountCompletedContentViewDelegate { func didTapPrivacyButton(view: SetupAccountCompletedContentView, button: AppButton) { delegate?.didRequestToSeePrivacy(controller: self) } diff --git a/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift index 567df8fc891c..e4a0c209e6cd 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift @@ -14,7 +14,7 @@ protocol InAppPurchaseViewControllerDelegate: AnyObject { func didEndPayment() } -class InAppPurchaseInteractor { +class InAppPurchaseInteractor: @unchecked Sendable { let storePaymentManager: StorePaymentManager var didFinishPayment: ((InAppPurchaseInteractor, StorePaymentEvent) -> Void)? weak var viewControllerDelegate: InAppPurchaseViewControllerDelegate? diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift index db3145ac688a..17c9de2d2b22 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeContentView.swift @@ -8,18 +8,18 @@ import UIKit -protocol WelcomeContentViewDelegate: AnyObject { +protocol WelcomeContentViewDelegate: AnyObject, Sendable { func didTapPurchaseButton(welcomeContentView: WelcomeContentView, button: AppButton) func didTapRedeemVoucherButton(welcomeContentView: WelcomeContentView, button: AppButton) func didTapInfoButton(welcomeContentView: WelcomeContentView, button: UIButton) } -struct WelcomeViewModel { +struct WelcomeViewModel: Sendable { let deviceName: String let accountNumber: String } -final class WelcomeContentView: UIView { +final class WelcomeContentView: UIView, Sendable { private let titleLabel: UILabel = { let label = UILabel() label.font = .preferredFont(forTextStyle: .largeTitle, weight: .bold) diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift index 80be852f656d..11bddcf5aa4e 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeInteractor.swift @@ -11,7 +11,7 @@ import MullvadLogging import MullvadTypes import StoreKit -final class WelcomeInteractor { +final class WelcomeInteractor: @unchecked Sendable { private let storePaymentManager: StorePaymentManager private let tunnelManager: TunnelManager diff --git a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift index 9de0ce833442..909c22b8240b 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/Welcome/WelcomeViewController.swift @@ -83,7 +83,7 @@ class WelcomeViewController: UIViewController, RootContainment { } } -extension WelcomeViewController: WelcomeContentViewDelegate { +extension WelcomeViewController: @preconcurrency WelcomeContentViewDelegate { func didTapInfoButton(welcomeContentView: WelcomeContentView, button: UIButton) { delegate?.didRequestToShowInfo(controller: self) } @@ -103,7 +103,7 @@ extension WelcomeViewController: WelcomeContentViewDelegate { } } -extension WelcomeViewController: InAppPurchaseViewControllerDelegate { +extension WelcomeViewController: @preconcurrency InAppPurchaseViewControllerDelegate { func didBeginPayment() { contentView.isPurchasing = true } diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift index 46386165c15f..14bd8cd0fb2a 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementContentView.swift @@ -97,7 +97,7 @@ class DeviceManagementContentView: UIView { return stackView }() - var handleDeviceDeletion: ((DeviceViewModel, @escaping () -> Void) -> Void)? + var handleDeviceDeletion: (@Sendable (DeviceViewModel, @escaping @Sendable () -> Void) -> Void)? private var currentDeviceModels = [DeviceViewModel]() @@ -118,11 +118,11 @@ class DeviceManagementContentView: UIView { } private func addViews() { - [scrollView, buttonStackView].forEach(addSubview) + try? [scrollView, buttonStackView].forEach(addSubview) scrollView.addSubview(scrollContentView) - [statusImageView, titleLabel, messageLabel, deviceStackView] + try? [statusImageView, titleLabel, messageLabel, deviceStackView] .forEach(scrollContentView.addSubview) } @@ -262,7 +262,9 @@ class DeviceManagementContentView: UIView { view.showsActivityIndicator = true self?.handleDeviceDeletion?(view.viewModel) { - view.showsActivityIndicator = false + Task { @MainActor in + view.showsActivityIndicator = false + } } } diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift index 581b5e0d0a11..a42590c3426f 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementInteractor.swift @@ -11,7 +11,7 @@ import MullvadREST import MullvadTypes import Operations -class DeviceManagementInteractor { +class DeviceManagementInteractor: @unchecked Sendable { private let devicesProxy: DeviceHandling private let accountNumber: String @@ -21,7 +21,7 @@ class DeviceManagementInteractor { } @discardableResult - func getDevices(_ completionHandler: @escaping (Result<[Device], Error>) -> Void) -> Cancellable { + func getDevices(_ completionHandler: @escaping @Sendable (Result<[Device], Error>) -> Void) -> Cancellable { devicesProxy.getDevices( accountNumber: accountNumber, retryStrategy: .default, @@ -30,7 +30,10 @@ class DeviceManagementInteractor { } @discardableResult - func deleteDevice(_ identifier: String, completionHandler: @escaping (Result) -> Void) -> Cancellable { + func deleteDevice( + _ identifier: String, + completionHandler: @escaping @Sendable (Result) -> Void + ) -> Cancellable { devicesProxy.deleteDevice( accountNumber: accountNumber, identifier: identifier, diff --git a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift index b36c48b2e74e..546d6bd044e9 100644 --- a/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift +++ b/ios/MullvadVPN/View controllers/DeviceList/DeviceManagementViewController.swift @@ -6,13 +6,13 @@ // Copyright © 2022 Mullvad VPN AB. All rights reserved. // -import MullvadLogging +@preconcurrency import MullvadLogging import MullvadREST import MullvadTypes import Operations import UIKit -protocol DeviceManagementViewControllerDelegate: AnyObject { +protocol DeviceManagementViewControllerDelegate: AnyObject, Sendable { func deviceManagementViewControllerDidFinish(_ controller: DeviceManagementViewController) func deviceManagementViewControllerDidCancel(_ controller: DeviceManagementViewController) } @@ -38,8 +38,8 @@ class DeviceManagementViewController: UIViewController, RootContainment { return contentView }() - private let logger = Logger(label: "DeviceManagementViewController") - private let interactor: DeviceManagementInteractor + nonisolated(unsafe) private let logger = Logger(label: "DeviceManagementViewController") + let interactor: DeviceManagementInteractor private let alertPresenter: AlertPresenter init(interactor: DeviceManagementInteractor, alertPresenter: AlertPresenter) { @@ -73,7 +73,9 @@ class DeviceManagementViewController: UIViewController, RootContainment { ) contentView.handleDeviceDeletion = { [weak self] viewModel, finish in - self?.handleDeviceDeletion(viewModel, completionHandler: finish) + Task { @MainActor in + self?.handleDeviceDeletion(viewModel, completionHandler: finish) + } } NSLayoutConstraint.activate([ @@ -86,7 +88,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { func fetchDevices( animateUpdates: Bool, - completionHandler: ((Result) -> Void)? = nil + completionHandler: (@Sendable (Result) -> Void)? = nil ) { interactor.getDevices { [weak self] result in guard let self = self else { return } @@ -101,7 +103,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { // MARK: - Private - private func setDevices(_ devices: [Device], animated: Bool) { + nonisolated private func setDevices(_ devices: [Device], animated: Bool) { let viewModels = devices.map { restDevice -> DeviceViewModel in DeviceViewModel( id: restDevice.id, @@ -114,13 +116,15 @@ class DeviceManagementViewController: UIViewController, RootContainment { ) } - contentView.canContinue = viewModels.count < ApplicationConfiguration.maxAllowedDevices - contentView.setDeviceViewModels(viewModels, animated: animated) + Task { @MainActor in + contentView.canContinue = viewModels.count < ApplicationConfiguration.maxAllowedDevices + contentView.setDeviceViewModels(viewModels, animated: animated) + } } private func handleDeviceDeletion( _ device: DeviceViewModel, - completionHandler: @escaping () -> Void + completionHandler: @escaping @Sendable () -> Void ) { showLogoutConfirmation(deviceName: device.name) { [weak self] shouldDelete in guard let self else { return } @@ -134,15 +138,17 @@ class DeviceManagementViewController: UIViewController, RootContainment { guard let self = self else { return } if let error { - self.showErrorAlert( - title: NSLocalizedString( - "LOGOUT_DEVICE_ERROR_ALERT_TITLE", - tableName: "DeviceManagement", - value: "Failed to log out device", - comment: "" - ), - error: error - ) + Task { @MainActor in + self.showErrorAlert( + title: NSLocalizedString( + "LOGOUT_DEVICE_ERROR_ALERT_TITLE", + tableName: "DeviceManagement", + value: "Failed to log out device", + comment: "" + ), + error: error + ) + } } completionHandler() @@ -236,14 +242,16 @@ class DeviceManagementViewController: UIViewController, RootContainment { alertPresenter.showAlert(presentation: presentation, animated: true) } - private func deleteDevice(identifier: String, completionHandler: @escaping (Error?) -> Void) { + private func deleteDevice(identifier: String, completionHandler: @escaping @Sendable (Error?) -> Void) { interactor.deleteDevice(identifier) { [weak self] completion in guard let self = self else { return } switch completion { case .success: - fetchDevices(animateUpdates: true) { completion in - completionHandler(completion.error) + Task { @MainActor in + fetchDevices(animateUpdates: true) { completion in + completionHandler(completion.error) + } } case let .failure(error): @@ -271,7 +279,7 @@ class DeviceManagementViewController: UIViewController, RootContainment { } } -struct DeviceViewModel { +struct DeviceViewModel: Sendable { let id: String let name: String let creationDate: String diff --git a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift index a6d230007706..cd7564f35653 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginInteractor.swift @@ -7,15 +7,15 @@ // import Foundation -import MullvadLogging +@preconcurrency import MullvadLogging import MullvadSettings -final class LoginInteractor { +final class LoginInteractor: @unchecked Sendable { private let tunnelManager: TunnelManager private let logger = Logger(label: "LoginInteractor") private var tunnelObserver: TunnelObserver? - var didCreateAccount: (() -> Void)? - var suggestPreferredAccountNumber: ((String) -> Void)? + var didCreateAccount: (@Sendable () -> Void)? + var suggestPreferredAccountNumber: (@Sendable (String) -> Void)? init(tunnelManager: TunnelManager) { self.tunnelManager = tunnelManager diff --git a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift index 81d00f824820..fb162fedd698 100644 --- a/ios/MullvadVPN/View controllers/Login/LoginViewController.swift +++ b/ios/MullvadVPN/View controllers/Login/LoginViewController.swift @@ -141,7 +141,9 @@ class LoginViewController: UIViewController, RootContainment { } interactor.suggestPreferredAccountNumber = { [weak self] value in - self?.contentView.accountInputGroup.setAccount(value) + Task { @MainActor in + self?.contentView.accountInputGroup.setAccount(value) + } } contentView.accountInputGroup.setOnReturnKey { [weak self] _ in diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift index 8524ff13b0ce..6beb0bd9c178 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeInteractor.swift @@ -12,23 +12,23 @@ import MullvadREST import MullvadSettings import MullvadTypes import Operations -import StoreKit +@preconcurrency import StoreKit -final class OutOfTimeInteractor { +final class OutOfTimeInteractor: Sendable { private let storePaymentManager: StorePaymentManager private let tunnelManager: TunnelManager - private var tunnelObserver: TunnelObserver? - private var paymentObserver: StorePaymentObserver? + nonisolated(unsafe) private var tunnelObserver: TunnelObserver? + nonisolated(unsafe) private var paymentObserver: StorePaymentObserver? - private let logger = Logger(label: "OutOfTimeInteractor") + nonisolated(unsafe) private let logger = Logger(label: "OutOfTimeInteractor") private let accountUpdateTimerInterval: Duration = .minutes(1) - private var accountUpdateTimer: DispatchSourceTimer? + nonisolated(unsafe) private var accountUpdateTimer: DispatchSourceTimer? - var didReceivePaymentEvent: ((StorePaymentEvent) -> Void)? - var didReceiveTunnelStatus: ((TunnelStatus) -> Void)? - var didAddMoreCredit: (() -> Void)? + nonisolated(unsafe) var didReceivePaymentEvent: (@Sendable (StorePaymentEvent) -> Void)? + nonisolated(unsafe) var didReceiveTunnelStatus: (@Sendable (TunnelStatus) -> Void)? + nonisolated(unsafe) var didAddMoreCredit: (@Sendable () -> Void)? init(storePaymentManager: StorePaymentManager, tunnelManager: TunnelManager) { self.storePaymentManager = storePaymentManager @@ -76,7 +76,7 @@ final class OutOfTimeInteractor { func restorePurchases( for accountNumber: String, - completionHandler: @escaping (Result< + completionHandler: @escaping @Sendable (Result< REST.CreateApplePaymentResponse, Error >) -> Void @@ -89,7 +89,7 @@ final class OutOfTimeInteractor { func requestProducts( with productIdentifiers: Set, - completionHandler: @escaping (Result) -> Void + completionHandler: @escaping @Sendable (Result) -> Void ) -> Cancellable { storePaymentManager.requestProducts( with: productIdentifiers, diff --git a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift index a85b0de35c55..a735fb4ecad1 100644 --- a/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift +++ b/ios/MullvadVPN/View controllers/OutOfTime/OutOfTimeViewController.swift @@ -9,14 +9,15 @@ import Foundation import MullvadREST import Operations -import StoreKit +@preconcurrency import StoreKit import UIKit -protocol OutOfTimeViewControllerDelegate: AnyObject { +protocol OutOfTimeViewControllerDelegate: AnyObject, Sendable { func outOfTimeViewControllerDidBeginPayment(_ controller: OutOfTimeViewController) func outOfTimeViewControllerDidEndPayment(_ controller: OutOfTimeViewController) } +@MainActor class OutOfTimeViewController: UIViewController, RootContainment { weak var delegate: OutOfTimeViewControllerDelegate? @@ -42,7 +43,7 @@ class OutOfTimeViewController: UIViewController, RootContainment { .lightContent } - var preferredHeaderBarPresentation: HeaderBarPresentation { + nonisolated(unsafe) var preferredHeaderBarPresentation: HeaderBarPresentation { let tunnelState = interactor.tunnelStatus.state return HeaderBarPresentation( @@ -95,12 +96,16 @@ class OutOfTimeViewController: UIViewController, RootContainment { ) interactor.didReceivePaymentEvent = { [weak self] event in - self?.didReceivePaymentEvent(event) + Task { @MainActor in + self?.didReceivePaymentEvent(event) + } } interactor.didReceiveTunnelStatus = { [weak self] _ in - self?.setNeedsHeaderBarStyleAppearanceUpdate() - self?.applyViewState() + Task { @MainActor in + self?.setNeedsHeaderBarStyleAppearanceUpdate() + self?.applyViewState() + } } if StorePaymentManager.canMakePayments { @@ -131,7 +136,9 @@ class OutOfTimeViewController: UIViewController, RootContainment { let productState: ProductState = completion.value?.products.first .map { .received($0) } ?? .failed - self?.productState = productState + Task { @MainActor in + self?.productState = productState + } } } @@ -214,7 +221,9 @@ class OutOfTimeViewController: UIViewController, RootContainment { default: errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) { - self.paymentState = .none + MainActor.assumeIsolated { + self.paymentState = .none + } } } } @@ -249,17 +258,27 @@ class OutOfTimeViewController: UIViewController, RootContainment { switch result { case let .success(response): - errorPresenter.showAlertForResponse(response, context: .restoration) { - self.paymentState = .none + Task { @MainActor in + errorPresenter.showAlertForResponse(response, context: .restoration) { + MainActor.assumeIsolated { + self.paymentState = .none + } + } } case let .failure(error as StorePaymentManagerError): - errorPresenter.showAlertForError(error, context: .restoration) { - self.paymentState = .none + Task { @MainActor in + errorPresenter.showAlertForError(error, context: .restoration) { + MainActor.assumeIsolated { + self.paymentState = .none + } + } } default: - paymentState = .none + Task { @MainActor in + paymentState = .none + } } } } diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift index 616ea7d081fb..a63d97e2edfa 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportInteractor.swift @@ -11,7 +11,7 @@ import MullvadREST import MullvadTypes import Operations -final class ProblemReportInteractor { +final class ProblemReportInteractor: @unchecked Sendable { private let apiProxy: APIQuerying private let tunnelManager: TunnelManager private let consolidatedLog: ConsolidatedApplicationLog @@ -28,7 +28,7 @@ final class ProblemReportInteractor { ) } - func fetchReportString(completion: @escaping (String) -> Void) { + func fetchReportString(completion: @escaping @Sendable (String) -> Void) { consolidatedLog.addLogFiles(fileURLs: ApplicationTarget.allCases.flatMap { ApplicationConfiguration.logFileURLs(for: $0, in: ApplicationConfiguration.containerURL) }) { [weak self] in @@ -40,7 +40,7 @@ final class ProblemReportInteractor { func sendReport( email: String, message: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let logString = self.consolidatedLog.string @@ -67,7 +67,7 @@ final class ProblemReportInteractor { email: String, message: String, logString: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) { let metadataDict = self.consolidatedLog.metadata.reduce(into: [:]) { output, entry in output[entry.key.rawValue] = entry.value diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift index 56befb66fd0f..1103a76e71f6 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportReviewViewController.swift @@ -97,9 +97,11 @@ class ProblemReportReviewViewController: UIViewController { spinnerView.startAnimating() interactor.fetchReportString { [weak self] reportString in guard let self else { return } - textView.text = reportString - spinnerView.stopAnimating() - spinnerContainerView.isHidden = true + Task { @MainActor in + textView.text = reportString + spinnerView.stopAnimating() + spinnerContainerView.isHidden = true + } } } @@ -107,14 +109,16 @@ class ProblemReportReviewViewController: UIViewController { private func share() { interactor.fetchReportString { [weak self] reportString in guard let self,!reportString.isEmpty else { return } - let activityController = UIActivityViewController( - activityItems: [reportString], - applicationActivities: nil - ) + Task { @MainActor in + let activityController = UIActivityViewController( + activityItems: [reportString], + applicationActivities: nil + ) - activityController.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem + activityController.popoverPresentationController?.barButtonItem = navigationItem.leftBarButtonItem - present(activityController, animated: true) + present(activityController, animated: true) + } } } #endif diff --git a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift index 1cbba022e481..a7ad2d73a76b 100644 --- a/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift +++ b/ios/MullvadVPN/View controllers/ProblemReport/ProblemReportViewController.swift @@ -262,7 +262,9 @@ final class ProblemReportViewController: UIViewController, UITextFieldDelegate { email: viewModel.email, message: viewModel.message ) { completion in - self.didSendProblemReport(viewModel: viewModel, completion: completion) + Task { @MainActor in + self.didSendProblemReport(viewModel: viewModel, completion: completion) + } } } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift index 88011370057a..12fd0271d180 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherInteractor.swift @@ -10,7 +10,7 @@ import Foundation import MullvadREST import MullvadTypes -final class RedeemVoucherInteractor { +final class RedeemVoucherInteractor: @unchecked Sendable { private let tunnelManager: TunnelManager private let accountsProxy: RESTAccountHandling private let shouldVerifyVoucherAsAccount: Bool @@ -33,7 +33,7 @@ final class RedeemVoucherInteractor { func redeemVoucher( code: String, - completion: @escaping ((Result) -> Void) + completion: @escaping (@Sendable (Result) -> Void) ) { tasks.append(tunnelManager.redeemVoucher(code) { [weak self] result in guard let self else { return } diff --git a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift index e1fdc52f16ed..870426c0045b 100644 --- a/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift +++ b/ios/MullvadVPN/View controllers/RedeemVoucher/RedeemVoucherViewController.swift @@ -10,7 +10,7 @@ import MullvadREST import MullvadTypes import UIKit -protocol RedeemVoucherViewControllerDelegate: AnyObject { +protocol RedeemVoucherViewControllerDelegate: AnyObject, Sendable { func redeemVoucherDidSucceed( _ controller: RedeemVoucherViewController, with response: REST.SubmitVoucherResponse @@ -18,9 +18,10 @@ protocol RedeemVoucherViewControllerDelegate: AnyObject { func redeemVoucherDidCancel(_ controller: RedeemVoucherViewController) } +@MainActor class RedeemVoucherViewController: UIViewController, UINavigationControllerDelegate, RootContainment { private let contentView: RedeemVoucherContentView - private var interactor: RedeemVoucherInteractor + nonisolated(unsafe) private var interactor: RedeemVoucherInteractor weak var delegate: RedeemVoucherViewControllerDelegate? @@ -115,12 +116,14 @@ class RedeemVoucherViewController: UIViewController, UINavigationControllerDeleg contentView.isEditing = false interactor.redeemVoucher(code: code, completion: { [weak self] result in guard let self else { return } - switch result { - case let .success(value): - contentView.state = .success - delegate?.redeemVoucherDidSucceed(self, with: value) - case let .failure(error): - contentView.state = .failure(error) + MainActor.assumeIsolated { + switch result { + case let .success(value): + contentView.state = .success + delegate?.redeemVoucherDidSucceed(self, with: value) + case let .failure(error): + contentView.state = .failure(error) + } } }) } diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift index ca3e26cc7584..a82fd2cfecfe 100644 --- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift +++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterCellFactory.swift @@ -8,7 +8,8 @@ import UIKit -struct RelayFilterCellFactory: CellFactoryProtocol { +@MainActor +struct RelayFilterCellFactory: @preconcurrency CellFactoryProtocol { let tableView: UITableView func makeCell(for item: RelayFilterDataSource.Item, indexPath: IndexPath) -> UITableViewCell { diff --git a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift index 53634790bfec..5ff3a3aed674 100644 --- a/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift +++ b/ios/MullvadVPN/View controllers/RelayFilter/RelayFilterView.swift @@ -164,9 +164,11 @@ class RelayFilterView: UIView { .old, ]) { [weak self] _, change in guard let self, let newSize = change.newValue else { return } - let height = newSize.height == .zero ? 8 : newSize.height - collectionViewHeightConstraint.constant = height > 80 ? 80 : height - layoutIfNeeded() // Update the layout + Task { @MainActor in + let height = newSize.height == .zero ? 8 : newSize.height + collectionViewHeightConstraint.constant = height > 80 ? 80 : height + layoutIfNeeded() // Update the layout + } } } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift index ec7f5b75e298..76b537bf6cf5 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationCellViewModel.swift @@ -8,7 +8,7 @@ import MullvadTypes -struct LocationCellViewModel: Hashable { +struct LocationCellViewModel: Hashable, Sendable { let section: LocationSection let node: LocationNode var indentationLevel = 0 diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift index c841d1ffda0a..674e174cfa74 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationDataSource.swift @@ -15,19 +15,18 @@ import UIKit final class LocationDataSource: UITableViewDiffableDataSource, LocationDiffableDataSourceProtocol { - private var currentSearchString = "" - private var dataSources: [LocationDataSourceProtocol] = [] + nonisolated(unsafe) private var currentSearchString = "" + nonisolated(unsafe) private var dataSources: [LocationDataSourceProtocol] = [] // The selected location. - private var selectedLocation: LocationCellViewModel? + nonisolated(unsafe) private var selectedLocation: LocationCellViewModel? // When multihop is enabled, this is the "inverted" selected location, ie. entry // if in exit mode and exit if in entry mode. - private var excludedLocation: LocationCellViewModel? + nonisolated(unsafe) private var excludedLocation: LocationCellViewModel? let tableView: UITableView let sections: [LocationSection] - let serialUpdateQueue = DispatchQueue(label: "LocationDataSource.UpdateQueue") - var didSelectRelayLocations: ((UserSelectedRelays) -> Void)? - var didTapEditCustomLists: (() -> Void)? + var didSelectRelayLocations: (@Sendable (UserSelectedRelays) -> Void)? + var didTapEditCustomLists: (@Sendable () -> Void)? init( tableView: UITableView, @@ -56,22 +55,22 @@ final class LocationDataSource: } func setRelays(_ relaysWithLocation: LocationRelays, selectedRelays: RelaySelection) { - serialUpdateQueue.async { [weak self] in - guard let self = self, - let allLocationsDataSource = dataSources - .first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, - let customListsDataSource = dataSources - .first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource else { return } + Task { @MainActor in + guard let allLocationsDataSource = dataSources + .first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, + let customListsDataSource = dataSources + .first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource else { return } allLocationsDataSource.reload(relaysWithLocation) customListsDataSource.reload(allLocationNodes: allLocationsDataSource.nodes) - setSelectedRelays(selectedRelays) - filterRelays(by: currentSearchString) + Task { @MainActor in + setSelectedRelays(selectedRelays) + filterRelays(by: currentSearchString) + } } } func filterRelays(by searchString: String) { - serialUpdateQueue.async(flags: .barrier) { [weak self] in - guard let self = self else { return } + Task { @MainActor in currentSearchString = searchString let list = sections.enumerated().map { index, section in @@ -103,12 +102,11 @@ final class LocationDataSource: /// Refreshes the custom list section and keeps all modifications intact (selection and expanded states). func refreshCustomLists() { - serialUpdateQueue.async { [weak self] in - guard let self = self, - let allLocationsDataSource = - dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, - let customListsDataSource = - dataSources.first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource + Task { @MainActor in + guard let allLocationsDataSource = + dataSources.first(where: { $0 is AllLocationDataSource }) as? AllLocationDataSource, + let customListsDataSource = + dataSources.first(where: { $0 is CustomListsDataSource }) as? CustomListsDataSource else { return } @@ -120,8 +118,8 @@ final class LocationDataSource: } func setSelectedRelays(_ selectedRelays: RelaySelection) { - serialUpdateQueue.async(flags: .barrier) { [weak self] in - guard let self, let _selectedLocation = mapSelection(from: selectedRelays.selected) else { return } + Task { @MainActor in + guard let _selectedLocation = mapSelection(from: selectedRelays.selected) else { return } selectedLocation = _selectedLocation excludedLocation = mapSelection(from: selectedRelays.excluded) excludedLocation?.excludedRelayTitle = selectedRelays.excludedTitle @@ -141,9 +139,7 @@ final class LocationDataSource: } private func indexPathForSelectedRelay() -> IndexPath? { - serialUpdateQueue.sync { - selectedLocation.flatMap { indexPath(for: $0) } - } + selectedLocation.flatMap { indexPath(for: $0) } } private func mapSelection(from selectedRelays: UserSelectedRelays?) -> LocationCellViewModel? { @@ -330,7 +326,7 @@ extension LocationDataSource: UITableViewDelegate { } } -extension LocationDataSource: LocationCellDelegate { +extension LocationDataSource: @preconcurrency LocationCellDelegate { func toggleExpanding(cell: LocationCell) { guard let indexPath = tableView.indexPath(for: cell), let item = itemIdentifier(for: indexPath) else { return } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift index 8a43493f0b8d..6c81c941b3a0 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationNode.swift @@ -9,7 +9,7 @@ import MullvadSettings import MullvadTypes -class LocationNode { +class LocationNode: @unchecked Sendable { let name: String var code: String var locations: [RelayLocation] @@ -134,13 +134,13 @@ extension LocationNode: Comparable { } /// Proxy class for building and/or searching node trees. -class RootLocationNode: LocationNode { +class RootLocationNode: LocationNode, @unchecked Sendable { init(name: String = "", code: String = "", children: [LocationNode] = []) { super.init(name: name, code: code, children: children) } } -class CustomListLocationNode: LocationNode { +class CustomListLocationNode: LocationNode, @unchecked Sendable { let customList: CustomList init( diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationRelays.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationRelays.swift index a3bc127593e8..baa8f60ec636 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationRelays.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationRelays.swift @@ -8,7 +8,7 @@ import MullvadREST -struct LocationRelays { +struct LocationRelays: Sendable { var relays: [REST.ServerRelay] var locations: [String: REST.ServerLocation] } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift index 87dc2dbb53e9..074675de48c3 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationSection.swift @@ -7,7 +7,7 @@ // import Foundation -enum LocationSection: String, Hashable, CustomStringConvertible, CaseIterable, CellIdentifierProtocol { +enum LocationSection: String, Hashable, CustomStringConvertible, CaseIterable, CellIdentifierProtocol, Sendable { case customLists case allLocations diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift index 70e11012fe80..3bd2671f1290 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewController.swift @@ -136,16 +136,20 @@ final class LocationViewController: UIViewController { ) dataSource?.didSelectRelayLocations = { [weak self] relays in - self?.delegate?.didSelectRelays(relays: relays) + Task { @MainActor in + self?.delegate?.didSelectRelays(relays: relays) + } } dataSource?.didTapEditCustomLists = { [weak self] in guard let self else { return } - if let relaysWithLocation { - let allLocationDataSource = AllLocationDataSource() - allLocationDataSource.reload(relaysWithLocation) - delegate?.navigateToCustomLists(nodes: allLocationDataSource.nodes) + Task { @MainActor in + if let relaysWithLocation { + let allLocationDataSource = AllLocationDataSource() + allLocationDataSource.reload(relaysWithLocation) + delegate?.navigateToCustomLists(nodes: allLocationDataSource.nodes) + } } } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift index 3aba8a46e566..1bcd44713cfc 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/LocationViewControllerWrapper.swift @@ -280,7 +280,7 @@ final class LocationViewControllerWrapper: UIViewController { } } -extension LocationViewControllerWrapper: LocationViewControllerDelegate { +extension LocationViewControllerWrapper: @preconcurrency LocationViewControllerDelegate { func navigateToCustomLists(nodes: [LocationNode]) { delegate?.navigateToCustomLists(nodes: nodes) } diff --git a/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift b/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift index 85b185a574c2..8cffa380b738 100644 --- a/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift +++ b/ios/MullvadVPN/View controllers/SelectLocation/RelaySelection.swift @@ -8,7 +8,7 @@ import MullvadTypes -struct RelaySelection { +struct RelaySelection: Sendable { var selected: UserSelectedRelays? var excluded: UserSelectedRelays? var excludedTitle: String? diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift index d9b5b42bb116..dc365734e8bb 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsCellFactory.swift @@ -13,7 +13,8 @@ protocol SettingsCellEventHandler { func showInfo(for button: SettingsInfoButtonItem) } -final class SettingsCellFactory: CellFactoryProtocol { +@MainActor +final class SettingsCellFactory: @preconcurrency CellFactoryProtocol, Sendable { let tableView: UITableView var delegate: SettingsCellEventHandler? var viewModel: SettingsViewModel diff --git a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift index 7d46b67750be..b769690d26b8 100644 --- a/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/Settings/SettingsDataSource.swift @@ -175,7 +175,7 @@ final class SettingsDataSource: UITableViewDiffableDataSource Void) -> Void ) { + nonisolated(unsafe) let nonisolatedBlock = block let operation = AsyncBlockOperation(dispatchQueue: .main) { finish in - block { + nonisolatedBlock { finish(nil) } } diff --git a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift index 47b75fd7d5ef..2acd57bbe24a 100644 --- a/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift +++ b/ios/MullvadVPN/View controllers/Tunnel/TunnelViewControllerInteractor.swift @@ -10,7 +10,7 @@ import Foundation import MullvadSettings import MullvadTypes -final class TunnelViewControllerInteractor { +final class TunnelViewControllerInteractor: @unchecked Sendable { private let tunnelManager: TunnelManager private let outgoingConnectionService: OutgoingConnectionServiceHandling private var tunnelObserver: TunnelObserver? diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift index 6566211edd54..7b8342029e3d 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSCellFactory.swift @@ -16,7 +16,8 @@ protocol CustomDNSCellEventHandler { func showInfo(for button: VPNSettingsInfoButtonItem) } -final class CustomDNSCellFactory: CellFactoryProtocol { +@MainActor +final class CustomDNSCellFactory: @preconcurrency CellFactoryProtocol { let tableView: UITableView var viewModel: VPNSettingsViewModel var delegate: CustomDNSCellEventHandler? diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift index 42d3ac8f456d..133de6af2452 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSDataSource.swift @@ -630,7 +630,7 @@ final class CustomDNSDataSource: UITableViewDiffableDataSource< } } -extension CustomDNSDataSource: CustomDNSCellEventHandler { +extension CustomDNSDataSource: @preconcurrency CustomDNSCellEventHandler { func didChangeState(for preference: Item, isOn: Bool) { switch preference { case .blockAll: diff --git a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift index efae49ad8c52..a66fa474cc6b 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/CustomDNSViewController.swift @@ -96,7 +96,7 @@ class CustomDNSViewController: UITableViewController { } } -extension CustomDNSViewController: DNSSettingsDataSourceDelegate { +extension CustomDNSViewController: @preconcurrency DNSSettingsDataSourceDelegate { func didChangeViewModel(_ viewModel: VPNSettingsViewModel) { interactor.updateSettings([.dnsSettings(viewModel.asDNSSettings())]) } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift index 3f0b75c9e196..265c0ae5b305 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsCellFactory.swift @@ -18,7 +18,8 @@ protocol VPNSettingsCellEventHandler { func switchMultihop(_ state: MultihopState) } -final class VPNSettingsCellFactory: CellFactoryProtocol { +@MainActor +final class VPNSettingsCellFactory: @preconcurrency CellFactoryProtocol { let tableView: UITableView var viewModel: VPNSettingsViewModel var delegate: VPNSettingsCellEventHandler? diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift index 22d8a4a8da0a..6d57966ca2ad 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsDataSource.swift @@ -600,7 +600,7 @@ final class VPNSettingsDataSource: UITableViewDiffableDataSource< } } -extension VPNSettingsDataSource: VPNSettingsCellEventHandler { +extension VPNSettingsDataSource: @preconcurrency VPNSettingsCellEventHandler { func showInfo(for button: VPNSettingsInfoButtonItem) { delegate?.showInfo(for: button) } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInteractor.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInteractor.swift index 88fddf83b248..ccae325641c6 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInteractor.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsInteractor.swift @@ -38,11 +38,11 @@ final class VPNSettingsInteractor { tunnelManager.addObserver(tunnelObserver) } - func updateSettings(_ changes: [TunnelSettingsUpdate], completion: (() -> Void)? = nil) { + func updateSettings(_ changes: [TunnelSettingsUpdate], completion: (@Sendable () -> Void)? = nil) { tunnelManager.updateSettings(changes, completionHandler: completion) } - func setPort(_ port: UInt16?, completion: (() -> Void)? = nil) { + func setPort(_ port: UInt16?, completion: (@Sendable () -> Void)? = nil) { var relayConstraints = tunnelManager.settings.relayConstraints if let port { diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index a9756aecf712..a21e31236b74 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -73,7 +73,7 @@ class VPNSettingsViewController: UITableViewController { } } -extension VPNSettingsViewController: VPNSettingsDataSourceDelegate { +extension VPNSettingsViewController: @preconcurrency VPNSettingsDataSourceDelegate { func humanReadablePortRepresentation() -> String { let ranges = interactor.cachedRelays?.relays.wireguard.portRanges ?? [] return ranges diff --git a/ios/MullvadVPN/Views/CustomButton.swift b/ios/MullvadVPN/Views/CustomButton.swift index 8c99ddb2cc6c..6f6deb0e0aba 100644 --- a/ios/MullvadVPN/Views/CustomButton.swift +++ b/ios/MullvadVPN/Views/CustomButton.swift @@ -24,7 +24,7 @@ extension UIControl.State { } /// A custom `UIButton` subclass that implements additional layouts for the image -class CustomButton: UIButton { +class CustomButton: UIButton, Sendable { var imageAlignment: NSDirectionalRectEdge = .leading { didSet { self.configuration?.imagePlacement = imageAlignment diff --git a/ios/MullvadVPN/Views/CustomTextView.swift b/ios/MullvadVPN/Views/CustomTextView.swift index c9f43ed6d7cb..9f3106d3ab80 100644 --- a/ios/MullvadVPN/Views/CustomTextView.swift +++ b/ios/MullvadVPN/Views/CustomTextView.swift @@ -84,7 +84,7 @@ class CustomTextView: UITextView { } } - private var notificationObserver: Any? + nonisolated(unsafe) private var notificationObserver: Any? override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) @@ -125,7 +125,9 @@ class CustomTextView: UITextView { object: textStorage, queue: OperationQueue.main ) { [weak self] _ in - self?.updatePlaceholderVisibility() + MainActor.assumeIsolated { + self?.updatePlaceholderVisibility() + } } updatePlaceholderVisibility() diff --git a/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift index 2b0a5e07acc7..24daafbc6d63 100644 --- a/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift +++ b/ios/MullvadVPN/Views/SpinnerActivityIndicatorView.swift @@ -8,11 +8,13 @@ import UIKit +@MainActor class SpinnerActivityIndicatorView: UIView { private static let rotationAnimationKey = "rotation" private static let animationDuration = 0.6 - enum Style { + @MainActor + enum Style: Sendable { case small, medium, large, custom var intrinsicSize: CGSize { @@ -57,7 +59,9 @@ class SpinnerActivityIndicatorView: UIView { } deinit { - unregisterSceneActivationObserver() + MainActor.assumeIsolated { + unregisterSceneActivationObserver() + } } override func didMoveToWindow() { @@ -110,7 +114,9 @@ class SpinnerActivityIndicatorView: UIView { forName: UIScene.willEnterForegroundNotification, object: window?.windowScene, queue: .main, using: { [weak self] _ in - self?.restartAnimationIfNeeded() + MainActor.assumeIsolated { + self?.restartAnimationIfNeeded() + } } ) } diff --git a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift index c8cfc4308f68..b0302f0e39c3 100644 --- a/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift +++ b/ios/MullvadVPNTests/MullvadREST/Shadowsocks/ShadowsocksLoaderTests.swift @@ -10,7 +10,7 @@ @testable import MullvadSettings @testable import MullvadTypes -import XCTest +@preconcurrency import XCTest class ShadowsocksLoaderTests: XCTestCase { private let sampleRelays = ServerRelaysResponseStubs.sampleRelays @@ -91,7 +91,7 @@ class ShadowsocksLoaderTests: XCTestCase { } } -class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { +class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol, @unchecked Sendable { var entryBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) var exitBridgeResult: Result = .failure(ShadowsocksRelaySelectorStubError()) private let relays: REST.ServerRelaysResponse @@ -114,7 +114,7 @@ class ShadowsocksRelaySelectorStub: ShadowsocksRelaySelectorProtocol { } } -class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol { +class ShadowsocksConfigurationCacheStub: ShadowsocksConfigurationCacheProtocol, @unchecked Sendable { private(set) var cachedConfiguration: ShadowsocksConfiguration? func read() throws -> ShadowsocksConfiguration { diff --git a/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift b/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift index 1f19b6429c21..8c44a0ed7a7c 100644 --- a/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift +++ b/ios/MullvadVPNTests/MullvadSettings/InMemorySettingsStore.swift @@ -13,7 +13,7 @@ protocol Instantiable { init() } -class InMemorySettingsStore: SettingsStore where ThrownError: Instantiable { +class InMemorySettingsStore: SettingsStore, @unchecked Sendable where ThrownError: Instantiable { private var settings = [SettingsKey: Data]() func read(key: SettingsKey) throws -> Data { diff --git a/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift b/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift index 523c02bebf28..3146848827af 100644 --- a/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/Log/ConsolidatedApplicationLogTests.swift @@ -8,8 +8,8 @@ import XCTest -class ConsolidatedApplicationLogTests: XCTestCase { - var consolidatedLog: ConsolidatedApplicationLog! +final class ConsolidatedApplicationLogTests: XCTestCase, @unchecked Sendable { + nonisolated(unsafe) var consolidatedLog: ConsolidatedApplicationLog! let mockRedactStrings = ["sensitive", "secret"] let mockSecurityGroupIdentifiers = ["group1", "group2"] var createdMockFiles: [URL] = [] @@ -35,7 +35,7 @@ class ConsolidatedApplicationLogTests: XCTestCase { createdMockFiles = [] } - func testAddLogFiles() { + func testAddLogFiles() async { var string = "" let expectation = self.expectation(description: "Log files added") let mockFile = createMockFile(content: content, fileName: "\(generateRandomName()).txt") @@ -44,7 +44,7 @@ class ConsolidatedApplicationLogTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 1) + await fulfillment(of: [expectation], timeout: 1) consolidatedLog.write(to: &string) XCTAssertTrue( consolidatedLog.string.contains(string), @@ -52,7 +52,7 @@ class ConsolidatedApplicationLogTests: XCTestCase { ) } - func testAddError() { + func testAddError() async { let expectation = self.expectation(description: "Error added to log") let errorMessage = "Test error" let errorDetails = "A sensitive error occurred" @@ -61,14 +61,14 @@ class ConsolidatedApplicationLogTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 1) + await fulfillment(of: [expectation], timeout: 1) XCTAssertTrue( consolidatedLog.string.contains(errorMessage), "Log should include the error message." ) } - func testStringOutput() { + func testStringOutput() async { let expectation = self.expectation(description: "Log files added") let mockFile = createMockFile(content: content, fileName: "\(generateRandomName()).txt") consolidatedLog.addLogFiles(fileURLs: [mockFile]) { @@ -76,7 +76,7 @@ class ConsolidatedApplicationLogTests: XCTestCase { let output = self.consolidatedLog.string XCTAssertFalse(output.isEmpty, "Output string should include redacted log content.") } - waitForExpectations(timeout: 1) + await fulfillment(of: [expectation], timeout: 1) } // MARK: - Private functions diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift index 6edd2059cc3e..07f3a413cf62 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnel.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes import NetworkExtension -class MockTunnel: TunnelProtocol { +class MockTunnel: TunnelProtocol, @unchecked Sendable { typealias TunnelManagerProtocol = SimulatorTunnelProviderManager var status: NEVPNStatus diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift index 552607a279f5..607c93e65282 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/MockTunnelInteractor.swift @@ -12,7 +12,7 @@ import MullvadSettings import MullvadTypes // this is still very minimal, and will be fleshed out as needed. -class MockTunnelInteractor: TunnelInteractor { +final class MockTunnelInteractor: TunnelInteractor, @unchecked Sendable { var isConfigurationLoaded: Bool var settings: LatestTunnelSettings diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/StartTunnelOperationTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/StartTunnelOperationTests.swift index 7b2b0615362c..08b2946d9967 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/StartTunnelOperationTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/StartTunnelOperationTests.swift @@ -69,7 +69,7 @@ class StartTunnelOperationTests: XCTestCase { func testSetsReconnectIfDisconnecting() { let interactor = makeInteractor(deviceState: loggedInDeviceState, tunnelState: .disconnecting(.nothing)) - var tunnelStatus = TunnelStatus() + nonisolated(unsafe) var tunnelStatus = TunnelStatus() interactor.onUpdateTunnelStatus = { status in tunnelStatus = status } let expectation = expectation(description: "Tunnel status set to reconnect") diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift index f8946271514c..f6967306abf7 100644 --- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift +++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelStore+Stubs.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes import NetworkExtension -struct TunnelStoreStub: TunnelStoreProtocol { +struct TunnelStoreStub: TunnelStoreProtocol, Sendable { typealias TunnelType = TunnelStub let backgroundTaskProvider: any BackgroundTaskProviding func getPersistentTunnels() -> [TunnelType] { @@ -26,7 +26,7 @@ class DummyTunnelStatusObserver: TunnelStatusObserver { func tunnel(_ tunnel: any TunnelProtocol, didReceiveStatus status: NEVPNStatus) {} } -final class TunnelStub: TunnelProtocol, Equatable { +final class TunnelStub: TunnelProtocol, Equatable, @unchecked Sendable { typealias TunnelManagerProtocol = SimulatorTunnelProviderManager static func == (lhs: TunnelStub, rhs: TunnelStub) -> Bool { diff --git a/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/CustomListRepositoryTests.swift b/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/CustomListRepositoryTests.swift index 5e08158ebb38..cd1ec200fb99 100644 --- a/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/CustomListRepositoryTests.swift +++ b/ios/MullvadVPNTests/MullvadVPN/View controllers/SelectLocation/CustomListRepositoryTests.swift @@ -11,7 +11,7 @@ import Network import XCTest class CustomListRepositoryTests: XCTestCase { - static let store = InMemorySettingsStore() + nonisolated(unsafe) static let store = InMemorySettingsStore() private var repository = CustomListRepository() override class func setUp() { diff --git a/ios/Operations/AsyncBlockOperation.swift b/ios/Operations/AsyncBlockOperation.swift index 062fc528f5c0..69e52aff0eaf 100644 --- a/ios/Operations/AsyncBlockOperation.swift +++ b/ios/Operations/AsyncBlockOperation.swift @@ -10,11 +10,14 @@ import Foundation import protocol MullvadTypes.Cancellable /// Asynchronous block operation -public class AsyncBlockOperation: AsyncOperation { - private var executor: ((@escaping (Error?) -> Void) -> Cancellable?)? +public class AsyncBlockOperation: AsyncOperation, @unchecked Sendable { + private var executor: ((@escaping @Sendable (Error?) -> Void) -> Cancellable?)? private var cancellableTask: Cancellable? - public init(dispatchQueue: DispatchQueue? = nil, block: @escaping (@escaping (Error?) -> Void) -> Void) { + public init( + dispatchQueue: DispatchQueue? = nil, + block: @escaping @Sendable (@escaping @Sendable (Error?) -> Void) -> Void + ) { super.init(dispatchQueue: dispatchQueue) executor = { finish in block(finish) @@ -22,7 +25,7 @@ public class AsyncBlockOperation: AsyncOperation { } } - public init(dispatchQueue: DispatchQueue? = nil, block: @escaping () -> Void) { + public init(dispatchQueue: DispatchQueue? = nil, block: @escaping @Sendable () -> Void) { super.init(dispatchQueue: dispatchQueue) executor = { finish in block() @@ -33,7 +36,7 @@ public class AsyncBlockOperation: AsyncOperation { public init( dispatchQueue: DispatchQueue? = nil, - cancellableTask: @escaping (@escaping (Error?) -> Void) -> Cancellable + cancellableTask: @escaping @Sendable (@escaping @Sendable (Error?) -> Void) -> Cancellable ) { super.init(dispatchQueue: dispatchQueue) executor = { cancellableTask($0) } diff --git a/ios/Operations/AsyncOperation.swift b/ios/Operations/AsyncOperation.swift index 034f17f199cf..771bf334fa8b 100644 --- a/ios/Operations/AsyncOperation.swift +++ b/ios/Operations/AsyncOperation.swift @@ -39,7 +39,7 @@ import Foundation } /// A base implementation of an asynchronous operation -open class AsyncOperation: Operation { +open class AsyncOperation: Operation, @unchecked Sendable { /// Mutex lock used for guarding critical sections of operation lifecycle. private let operationLock = NSRecursiveLock() @@ -199,7 +199,7 @@ open class AsyncOperation: Operation { state = .evaluatingConditions - var results = [Bool](repeating: false, count: _conditions.count) + nonisolated(unsafe) var results = [Bool](repeating: false, count: _conditions.count) let group = DispatchGroup() for (index, condition) in _conditions.enumerated() { diff --git a/ios/Operations/AsyncOperationQueue.swift b/ios/Operations/AsyncOperationQueue.swift index 57c4451138a7..cf4c421756dd 100644 --- a/ios/Operations/AsyncOperationQueue.swift +++ b/ios/Operations/AsyncOperationQueue.swift @@ -8,7 +8,7 @@ import Foundation -public final class AsyncOperationQueue: OperationQueue { +public final class AsyncOperationQueue: OperationQueue, @unchecked Sendable { override public func addOperation(_ operation: Operation) { if let operation = operation as? AsyncOperation { let categories = operation.conditions @@ -50,7 +50,7 @@ public final class AsyncOperationQueue: OperationQueue { } } -private final class ExclusivityManager { +private final class ExclusivityManager: @unchecked Sendable { static let shared = ExclusivityManager() private var operationsByCategory = [String: [Operation]]() diff --git a/ios/Operations/BackgroundObserver.swift b/ios/Operations/BackgroundObserver.swift index 5165780a15c9..5f5add27bc9b 100644 --- a/ios/Operations/BackgroundObserver.swift +++ b/ios/Operations/BackgroundObserver.swift @@ -26,7 +26,13 @@ public final class BackgroundObserver: OperationObserver { } public func didAttach(to operation: Operation) { + #if swift(>=6) + let expirationHandler = cancelUponExpiration + ? { @MainActor in operation.cancel() } as? @MainActor @Sendable () -> Void + : nil + #else let expirationHandler = cancelUponExpiration ? { operation.cancel() } : nil + #endif taskIdentifier = backgroundTaskProvider.beginBackgroundTask( withName: name, diff --git a/ios/Operations/GroupOperation.swift b/ios/Operations/GroupOperation.swift index 1f474e7b2887..5d8015ec8e84 100644 --- a/ios/Operations/GroupOperation.swift +++ b/ios/Operations/GroupOperation.swift @@ -8,7 +8,7 @@ import Foundation -public final class GroupOperation: AsyncOperation { +public final class GroupOperation: AsyncOperation, @unchecked Sendable { private let operationQueue = AsyncOperationQueue() private let children: [Operation] diff --git a/ios/Operations/OutputOperation.swift b/ios/Operations/OutputOperation.swift index 82274b145044..1a9e1acb877f 100644 --- a/ios/Operations/OutputOperation.swift +++ b/ios/Operations/OutputOperation.swift @@ -9,7 +9,7 @@ import Foundation public protocol OutputOperation: Operation { - associatedtype Output + associatedtype Output: Sendable var output: Output? { get } } diff --git a/ios/Operations/ResultBlockOperation.swift b/ios/Operations/ResultBlockOperation.swift index 867dee45cf2e..cb4f5fff647e 100644 --- a/ios/Operations/ResultBlockOperation.swift +++ b/ios/Operations/ResultBlockOperation.swift @@ -9,24 +9,24 @@ import Foundation import protocol MullvadTypes.Cancellable -public final class ResultBlockOperation: ResultOperation { - private var executor: ((@escaping (Result) -> Void) -> Cancellable?)? +public final class ResultBlockOperation: ResultOperation, @unchecked Sendable { + private var executor: ((@escaping @Sendable (Result) -> Void) -> Cancellable?)? private var cancellableTask: Cancellable? public init( dispatchQueue: DispatchQueue? = nil, - executionBlock: @escaping (_ finish: @escaping (Result) -> Void) -> Void + executionBlock: @escaping @Sendable (_ finish: @escaping (Result) -> Void) -> Void ) { super.init(dispatchQueue: dispatchQueue) - executor = { finish in + executor = { @Sendable finish in executionBlock(finish) return nil } } - public init(dispatchQueue: DispatchQueue? = nil, executionBlock: @escaping () throws -> Success) { + public init(dispatchQueue: DispatchQueue? = nil, executionBlock: @escaping @Sendable () throws -> Success) { super.init(dispatchQueue: dispatchQueue) - executor = { finish in + executor = { @Sendable finish in finish(Result { try executionBlock() }) return nil } @@ -34,7 +34,7 @@ public final class ResultBlockOperation: ResultOperation { public init( dispatchQueue: DispatchQueue? = nil, - cancellableTask: @escaping (_ finish: @escaping (Result) -> Void) -> Cancellable + cancellableTask: @escaping (_ finish: @escaping @Sendable (Result) -> Void) -> Cancellable ) { super.init(dispatchQueue: dispatchQueue) executor = { cancellableTask($0) } diff --git a/ios/Operations/ResultOperation.swift b/ios/Operations/ResultOperation.swift index e377ecf9f520..2debb4381af7 100644 --- a/ios/Operations/ResultOperation.swift +++ b/ios/Operations/ResultOperation.swift @@ -9,8 +9,8 @@ import Foundation /// Base class for operations producing result. -open class ResultOperation: AsyncOperation, OutputOperation, @unchecked Sendable { - public typealias CompletionHandler = (Result) -> Void +open class ResultOperation: AsyncOperation, OutputOperation, @unchecked Sendable { + public typealias CompletionHandler = @Sendable (Result) -> Void private let nslock = NSLock() private var _output: Success? @@ -118,7 +118,7 @@ open class ResultOperation: AsyncOperation, OutputOperation, @unchecked let completionQueue = _completionQueue nslock.unlock() - let block = { + let block: @Sendable () -> Void = { // Call completion handler. completionHandler?(result) diff --git a/ios/OperationsTests/AsyncBlockOperationTests.swift b/ios/OperationsTests/AsyncBlockOperationTests.swift index c19e5e8a3fec..7b87810649d0 100644 --- a/ios/OperationsTests/AsyncBlockOperationTests.swift +++ b/ios/OperationsTests/AsyncBlockOperationTests.swift @@ -11,6 +11,7 @@ import MullvadTypes import Operations import XCTest +@MainActor final class AsyncBlockOperationTests: XCTestCase { let operationQueue = AsyncOperationQueue() diff --git a/ios/OperationsTests/AsyncResultBlockOperationTests.swift b/ios/OperationsTests/AsyncResultBlockOperationTests.swift index 0cccf93241de..3c7bb06c4ce8 100644 --- a/ios/OperationsTests/AsyncResultBlockOperationTests.swift +++ b/ios/OperationsTests/AsyncResultBlockOperationTests.swift @@ -11,6 +11,7 @@ import MullvadTypes import Operations import XCTest +@MainActor final class AsyncResultBlockOperationTests: XCTestCase { let operationQueue = AsyncOperationQueue() diff --git a/ios/OperationsTests/OperationConditionTests.swift b/ios/OperationsTests/OperationConditionTests.swift index 26abaca2e9d0..02be4f3888e1 100644 --- a/ios/OperationsTests/OperationConditionTests.swift +++ b/ios/OperationsTests/OperationConditionTests.swift @@ -10,6 +10,7 @@ import Operations import XCTest +@MainActor class OperationConditionTests: XCTestCase { func testTrueCondition() { let expectConditionEvaluation = expectation(description: "Expect condition evaluation") diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift index 6c4e00b156e3..1fbdff9a7fe1 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckOperation.swift @@ -27,7 +27,7 @@ import WireGuardKitTypes Other times, packet tunnel runs this operation with `rotateImmediatelyOnKeyMismatch` set to `false`, in which case it respects the 24 hour interval between key rotation retry attempts. */ -final class DeviceCheckOperation: ResultOperation { +final class DeviceCheckOperation: ResultOperation, @unchecked Sendable { private let logger = Logger(label: "DeviceCheckOperation") private let remoteService: DeviceCheckRemoteServiceProtocol @@ -127,8 +127,8 @@ final class DeviceCheckOperation: ResultOperation { accountNumber: String, deviceIdentifier: String, completion: @escaping (Result, Result) -> Void ) { - var accountResult: Result = .failure(OperationError.cancelled) - var deviceResult: Result = .failure(OperationError.cancelled) + nonisolated(unsafe) var accountResult: Result = .failure(OperationError.cancelled) + nonisolated(unsafe) var deviceResult: Result = .failure(OperationError.cancelled) let dispatchGroup = DispatchGroup() diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift index 2bae95fe3c4b..1668680631c2 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteService.swift @@ -23,7 +23,7 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol { func getAccountData( accountNumber: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> Cancellable { accountsProxy.getAccountData(accountNumber: accountNumber).execute(completionHandler: completion) } @@ -31,7 +31,7 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol { func getDevice( accountNumber: String, identifier: String, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> Cancellable { devicesProxy.getDevice( accountNumber: accountNumber, @@ -45,7 +45,7 @@ struct DeviceCheckRemoteService: DeviceCheckRemoteServiceProtocol { accountNumber: String, identifier: String, publicKey: PublicKey, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> Cancellable { devicesProxy.rotateDeviceKey( accountNumber: accountNumber, diff --git a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift index 0cab26654f87..05030dba894f 100644 --- a/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift +++ b/ios/PacketTunnel/DeviceCheck/DeviceCheckRemoteServiceProtocol.swift @@ -12,14 +12,18 @@ import WireGuardKitTypes /// A protocol that formalizes remote service dependency used by `DeviceCheckOperation`. protocol DeviceCheckRemoteServiceProtocol { - func getAccountData(accountNumber: String, completion: @escaping (Result) -> Void) + func getAccountData(accountNumber: String, completion: @escaping @Sendable (Result) -> Void) -> Cancellable - func getDevice(accountNumber: String, identifier: String, completion: @escaping (Result) -> Void) + func getDevice( + accountNumber: String, + identifier: String, + completion: @escaping @Sendable (Result) -> Void + ) -> Cancellable func rotateDeviceKey( accountNumber: String, identifier: String, publicKey: PublicKey, - completion: @escaping (Result) -> Void + completion: @escaping @Sendable (Result) -> Void ) -> Cancellable } diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift index 56953553dde2..91c650ab4478 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelPathObserver.swift @@ -10,7 +10,7 @@ import Combine import NetworkExtension import PacketTunnelCore -final class PacketTunnelPathObserver: DefaultPathObserverProtocol { +final class PacketTunnelPathObserver: DefaultPathObserverProtocol, @unchecked Sendable { private weak var packetTunnelProvider: NEPacketTunnelProvider? private let stateLock = NSLock() private var pathUpdatePublisher: AnyCancellable? @@ -25,7 +25,7 @@ final class PacketTunnelPathObserver: DefaultPathObserverProtocol { return packetTunnelProvider?.defaultPath } - func start(_ body: @escaping (NetworkPath) -> Void) { + func start(_ body: @escaping @Sendable (NetworkPath) -> Void) { stateLock.withLock { pathUpdatePublisher?.cancel() diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift index 84ff437b2010..7df9d9090ca0 100644 --- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift @@ -16,7 +16,7 @@ import NetworkExtension import PacketTunnelCore import WireGuardKitTypes -class PacketTunnelProvider: NEPacketTunnelProvider { +class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private let internalQueue = DispatchQueue(label: "PacketTunnel-internalQueue") private let providerLogger: Logger @@ -174,7 +174,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } private func performSettingsMigration() { - var hasNotMigrated = true + nonisolated(unsafe) var hasNotMigrated = true repeat { migrationManager.migrateSettings( store: SettingsManager.store, diff --git a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift index 4bfd9b809103..a77fbc8bcbec 100644 --- a/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift +++ b/ios/PacketTunnel/WireGuardAdapter/WgAdapter.swift @@ -7,13 +7,13 @@ // import Foundation -import MullvadLogging +@preconcurrency import MullvadLogging import MullvadTypes import NetworkExtension import PacketTunnelCore -import WireGuardKit +@preconcurrency import WireGuardKit -struct WgAdapter: TunnelAdapterProtocol { +struct WgAdapter: TunnelAdapterProtocol, @unchecked Sendable { let logger = Logger(label: "WgAdapter") let adapter: WireGuardAdapter diff --git a/ios/PacketTunnelCore/Actor/AnyTask.swift b/ios/PacketTunnelCore/Actor/AnyTask.swift index 43c44b064414..40d723b40202 100644 --- a/ios/PacketTunnelCore/Actor/AnyTask.swift +++ b/ios/PacketTunnelCore/Actor/AnyTask.swift @@ -9,7 +9,7 @@ import Foundation /// A type-erased `Task`. -public protocol AnyTask { +public protocol AnyTask: Sendable { /// Cancel task. func cancel() } diff --git a/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift b/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift index c80e28f88017..46ac15d0a237 100644 --- a/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift +++ b/ios/PacketTunnelCore/Actor/AutoCancellingTask.swift @@ -13,7 +13,7 @@ import Foundation It behaves identical to `Combine.AnyCancellable`. */ -public final class AutoCancellingTask { +public final class AutoCancellingTask: Sendable { private let task: AnyTask init(_ task: AnyTask) { diff --git a/ios/PacketTunnelCore/Actor/EphemeralPeerNegotiationState.swift b/ios/PacketTunnelCore/Actor/EphemeralPeerNegotiationState.swift index 339e458ce266..1a06a82f278f 100644 --- a/ios/PacketTunnelCore/Actor/EphemeralPeerNegotiationState.swift +++ b/ios/PacketTunnelCore/Actor/EphemeralPeerNegotiationState.swift @@ -7,9 +7,9 @@ // import MullvadREST -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes -public enum EphemeralPeerNegotiationState: Equatable { +public enum EphemeralPeerNegotiationState: Equatable, Sendable { case single(EphemeralPeerRelayConfiguration) case multi(entry: EphemeralPeerRelayConfiguration, exit: EphemeralPeerRelayConfiguration) @@ -25,7 +25,7 @@ public enum EphemeralPeerNegotiationState: Equatable { } } -public struct EphemeralPeerRelayConfiguration: Equatable, CustomDebugStringConvertible { +public struct EphemeralPeerRelayConfiguration: Equatable, CustomDebugStringConvertible, Sendable { public let relay: SelectedRelay public let configuration: EphemeralPeerConfiguration @@ -39,7 +39,7 @@ public struct EphemeralPeerRelayConfiguration: Equatable, CustomDebugStringConve } } -public struct EphemeralPeerConfiguration: Equatable, CustomDebugStringConvertible { +public struct EphemeralPeerConfiguration: Equatable, CustomDebugStringConvertible, Sendable { public let privateKey: PrivateKey public let preSharedKey: PreSharedKey? public let allowedIPs: [IPAddressRange] diff --git a/ios/PacketTunnelCore/Actor/ObservedState.swift b/ios/PacketTunnelCore/Actor/ObservedState.swift index 8b3779284e85..16c4b08f9d0a 100644 --- a/ios/PacketTunnelCore/Actor/ObservedState.swift +++ b/ios/PacketTunnelCore/Actor/ObservedState.swift @@ -11,10 +11,10 @@ import Foundation import MullvadREST import MullvadTypes import Network -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /// A serializable representation of internal state. -public enum ObservedState: Equatable, Codable { +public enum ObservedState: Equatable, Codable, Sendable { case initial case connecting(ObservedConnectionState) case reconnecting(ObservedConnectionState) @@ -26,7 +26,7 @@ public enum ObservedState: Equatable, Codable { } /// A serializable representation of internal connection state. -public struct ObservedConnectionState: Equatable, Codable { +public struct ObservedConnectionState: Equatable, Codable, Sendable { public var selectedRelays: SelectedRelays public var relayConstraints: RelayConstraints public var networkReachability: NetworkReachability @@ -65,7 +65,7 @@ public struct ObservedConnectionState: Equatable, Codable { } /// A serializable representation of internal blocked state. -public struct ObservedBlockedState: Equatable, Codable { +public struct ObservedBlockedState: Equatable, Codable, Sendable { public var reason: BlockedStateReason public var relayConstraints: RelayConstraints? diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift index 3610c3ff5064..d189e45bfac1 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor+Extensions.swift @@ -6,6 +6,7 @@ // Copyright © 2023 Mullvad VPN AB. All rights reserved. // +@preconcurrency import Combine import Foundation extension PacketTunnelActor { diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift index 74f8a5a0e262..f42438dd0e53 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActor.swift @@ -7,7 +7,7 @@ // import Foundation -import MullvadLogging +@preconcurrency import MullvadLogging import MullvadREST import MullvadRustRuntime import MullvadSettings @@ -38,11 +38,11 @@ public actor PacketTunnelActor { @Published internal(set) public var observedState: ObservedState = .initial - nonisolated let logger = Logger(label: "PacketTunnelActor") + nonisolated(unsafe) let logger = Logger(label: "PacketTunnelActor") let timings: PacketTunnelActorTimings let tunnelAdapter: TunnelAdapterProtocol - nonisolated let tunnelMonitor: TunnelMonitorProtocol + let tunnelMonitor: TunnelMonitorProtocol let defaultPathObserver: DefaultPathObserverProtocol let blockedStateErrorMapper: BlockedStateErrorMapperProtocol public let relaySelector: RelaySelectorProtocol diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift index b677986d041b..38dcd0ba4206 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorCommand.swift @@ -11,7 +11,7 @@ import WireGuardKitTypes extension PacketTunnelActor { /// Describes events that the state machine handles. These can be user commands or non-user-initiated events - enum Event { + enum Event: Sendable { /// Start tunnel. case start(StartOptions) diff --git a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift index 3382baf2092a..82820577dd17 100644 --- a/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift +++ b/ios/PacketTunnelCore/Actor/PacketTunnelActorReducer.swift @@ -11,7 +11,7 @@ import WireGuardKitTypes extension PacketTunnelActor { /// A structure encoding an effect; each event will yield zero or more of those, which can then be sequentially executed. - enum Effect: Equatable { + enum Effect: Equatable, Sendable { case startDefaultPathObserver case stopDefaultPathObserver case startTunnelMonitor @@ -53,7 +53,7 @@ extension PacketTunnelActor { } } - struct Reducer { + struct Reducer: Sendable { static func reduce(_ state: inout State, _ event: Event) -> [Effect] { switch event { case let .start(options): diff --git a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift index a1107efc2a19..49f501ee9046 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/SettingsReaderProtocol.swift @@ -10,7 +10,7 @@ import Foundation import MullvadSettings import MullvadTypes import Network -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /// A type that implements a reader that can return settings required by `PacketTunnelActor` in order to configure the tunnel. public protocol SettingsReaderProtocol { @@ -24,7 +24,7 @@ public protocol SettingsReaderProtocol { } /// Struct holding settings necessary to configure packet tunnel adapter. -public struct Settings: Equatable { +public struct Settings: Equatable, Sendable { /// Private key used by device. public var privateKey: PrivateKey @@ -89,7 +89,7 @@ extension Settings { } /// Enum describing selected DNS servers option. -public enum SelectedDNSServers: Equatable { +public enum SelectedDNSServers: Equatable, Sendable { /// Custom DNS servers. case custom([IPAddress]) /// Mullvad server acting as a blocking DNS proxy. diff --git a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift index f99ccb0e8231..630e1c6515ad 100644 --- a/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift +++ b/ios/PacketTunnelCore/Actor/Protocols/TunnelAdapterProtocol.swift @@ -10,10 +10,10 @@ import Foundation import MullvadTypes import Network -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /// Protocol describing interface for any kind of adapter implementing a VPN tunnel. -public protocol TunnelAdapterProtocol { +public protocol TunnelAdapterProtocol: Sendable { /// Start tunnel adapter or update active configuration. func start(configuration: TunnelAdapterConfiguration, daita: DaitaConfiguration?) async throws diff --git a/ios/PacketTunnelCore/Actor/StartOptions.swift b/ios/PacketTunnelCore/Actor/StartOptions.swift index 9af92fe34ce2..25e38e93b3e3 100644 --- a/ios/PacketTunnelCore/Actor/StartOptions.swift +++ b/ios/PacketTunnelCore/Actor/StartOptions.swift @@ -10,7 +10,7 @@ import Foundation import MullvadREST /// Packet tunnel start options parsed from dictionary passed to packet tunnel with a call to `startTunnel()`. -public struct StartOptions { +public struct StartOptions: Sendable { /// The system that triggered the launch of packet tunnel. public var launchSource: LaunchSource @@ -36,7 +36,7 @@ public struct StartOptions { } /// The source facility that triggered a launch of packet tunnel extension. -public enum LaunchSource: String, CustomStringConvertible { +public enum LaunchSource: String, CustomStringConvertible, Sendable { /// Launched by the main bundle app using network extension framework. case app diff --git a/ios/PacketTunnelCore/Actor/State.swift b/ios/PacketTunnelCore/Actor/State.swift index 10a28b5a2486..bd90beff9466 100644 --- a/ios/PacketTunnelCore/Actor/State.swift +++ b/ios/PacketTunnelCore/Actor/State.swift @@ -10,7 +10,7 @@ import Foundation import MullvadREST import MullvadRustRuntime import MullvadTypes -import WireGuardKitTypes +@preconcurrency import WireGuardKitTypes /** Tunnel actor state with metadata describing the current phase of packet tunnel lifecycle. @@ -87,7 +87,7 @@ enum State: Equatable { } /// Enum describing network availability. -public enum NetworkReachability: Equatable, Codable { +public enum NetworkReachability: Equatable, Codable, Sendable { case undetermined, reachable, unreachable } @@ -100,7 +100,7 @@ protocol StateAssociatedData { extension State { /// Policy describing what WG key to use for tunnel communication. - enum KeyPolicy { + enum KeyPolicy: Sendable { /// Use current key stored in device data. case useCurrent @@ -156,7 +156,7 @@ extension State { } /// Data associated with error state. - struct BlockingData: StateAssociatedData { + struct BlockingData: StateAssociatedData, Sendable { /// Reason why block state was entered. public var reason: BlockedStateReason @@ -188,7 +188,7 @@ extension State { } /// Reason why packet tunnel entered error state. -public enum BlockedStateReason: String, Codable, Equatable { +public enum BlockedStateReason: String, Codable, Equatable, Sendable { /// Device is locked. case deviceLocked @@ -241,7 +241,7 @@ extension State.BlockingData { } /// Describes which relay the tunnel should connect to next. -public enum NextRelays: Equatable, Codable { +public enum NextRelays: Equatable, Codable, Sendable { /// Select next relays randomly. case random @@ -253,7 +253,7 @@ public enum NextRelays: Equatable, Codable { } /// Describes the reason for reconnection request. -public enum ActorReconnectReason: Equatable { +public enum ActorReconnectReason: Equatable, Sendable { /// Initiated by user. case userInitiated diff --git a/ios/PacketTunnelCore/Actor/Task+Duration.swift b/ios/PacketTunnelCore/Actor/Task+Duration.swift index 46e90bd196df..91f2ac39ba04 100644 --- a/ios/PacketTunnelCore/Actor/Task+Duration.swift +++ b/ios/PacketTunnelCore/Actor/Task+Duration.swift @@ -36,7 +36,7 @@ extension Task where Success == Never, Failure == Never { */ @available(iOS, introduced: 15.0, obsoleted: 16.0, message: "Replace with Task.sleep(for:tolerance:clock:).") static func sleepUsingContinuousClock(for duration: Duration) async throws { - let timer = DispatchSource.makeTimerSource() + nonisolated(unsafe) let timer = DispatchSource.makeTimerSource() try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in diff --git a/ios/PacketTunnelCore/Actor/Timings.swift b/ios/PacketTunnelCore/Actor/Timings.swift index 5a62f7b05858..947bbaf4a874 100644 --- a/ios/PacketTunnelCore/Actor/Timings.swift +++ b/ios/PacketTunnelCore/Actor/Timings.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes /// Struct holding all timings used by tunnel actor. -public struct PacketTunnelActorTimings { +public struct PacketTunnelActorTimings: Sendable { /// Periodicity at which actor will attempt to restart when an error occurred on system boot when filesystem is locked until device is unlocked or tunnel adapter error. public var bootRecoveryPeriodicity: Duration diff --git a/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift index d4d192fb743e..b330ff93d05a 100644 --- a/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift +++ b/ios/PacketTunnelCore/TunnelMonitor/DefaultPathObserverProtocol.swift @@ -10,19 +10,19 @@ import Foundation import NetworkExtension /// A type providing default path access and observation. -public protocol DefaultPathObserverProtocol { +public protocol DefaultPathObserverProtocol: Sendable { /// Returns current default path or `nil` if unknown yet. var defaultPath: NetworkPath? { get } /// Start observing changes to `defaultPath`. /// This call must be idempotent. Multiple calls to start should replace the existing handler block. - func start(_ body: @escaping (NetworkPath) -> Void) + func start(_ body: @escaping @Sendable (NetworkPath) -> Void) /// Stop observing changes to `defaultPath`. func stop() } /// A type that represents a network path. -public protocol NetworkPath { +public protocol NetworkPath: Sendable { var status: NetworkExtension.NWPathStatus { get } } diff --git a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift index 0a47d01fb716..63677eb9ee4e 100644 --- a/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift +++ b/ios/PacketTunnelCore/TunnelMonitor/TunnelMonitorProtocol.swift @@ -10,7 +10,7 @@ import Foundation import Network /// Tunnel monitor event. -public enum TunnelMonitorEvent { +public enum TunnelMonitorEvent: Sendable { /// Dispatched after receiving the first ping response case connectionEstablished @@ -20,7 +20,7 @@ public enum TunnelMonitorEvent { } /// A type that can provide tunnel monitoring. -public protocol TunnelMonitorProtocol: AnyObject { +public protocol TunnelMonitorProtocol: AnyObject, Sendable { /// Event handler that starts receiving events after the call to `start(probeAddress:)`. var onEvent: ((TunnelMonitorEvent) -> Void)? { get set } diff --git a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift index 1e13bf9b83f7..65c7e8e6e2b3 100644 --- a/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift +++ b/ios/PacketTunnelCore/URLRequestProxy/ProxyURLResponse.swift @@ -9,7 +9,7 @@ import Foundation /// Struct describing serializable URLResponse data. -public struct ProxyURLResponse: Codable { +public struct ProxyURLResponse: Codable, Sendable { public let data: Data? public let response: HTTPURLResponseWrapper? public let error: URLErrorWrapper? @@ -21,7 +21,7 @@ public struct ProxyURLResponse: Codable { } } -public struct URLErrorWrapper: Codable { +public struct URLErrorWrapper: Codable, Sendable { public let code: Int? public let localizedDescription: String @@ -37,7 +37,7 @@ public struct URLErrorWrapper: Codable { } } -public struct HTTPURLResponseWrapper: Codable { +public struct HTTPURLResponseWrapper: Codable, Sendable { public let url: URL? public let statusCode: Int public let headerFields: [String: String]? diff --git a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift index 8bde84f7816a..6a036ee6b08f 100644 --- a/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift +++ b/ios/PacketTunnelCoreTests/Mocks/DefaultPathObserverFake.swift @@ -15,7 +15,7 @@ struct NetworkPathStub: NetworkPath { } /// Default path observer fake that uses in-memory storage to keep current path and provides a method to simulate path change from tests. -class DefaultPathObserverFake: DefaultPathObserverProtocol { +class DefaultPathObserverFake: DefaultPathObserverProtocol, @unchecked Sendable { var defaultPath: NetworkPath? { return stateLock.withLock { innerPath } } diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift index 3c8e9552e711..c8afc4acd5c7 100644 --- a/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelAdapterDummy.swift @@ -11,7 +11,7 @@ import PacketTunnelCore @testable import WireGuardKitTypes /// Dummy tunnel adapter that does nothing and reports no errors. -class TunnelAdapterDummy: TunnelAdapterProtocol { +class TunnelAdapterDummy: TunnelAdapterProtocol, @unchecked Sendable { func startMultihop( entryConfiguration: TunnelAdapterConfiguration?, exitConfiguration: TunnelAdapterConfiguration, diff --git a/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift b/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift index 60609fee36e7..26abbf05f8df 100644 --- a/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift +++ b/ios/PacketTunnelCoreTests/Mocks/TunnelMonitorStub.swift @@ -11,7 +11,7 @@ import Network import PacketTunnelCore /// Tunnel monitor stub that can be configured with block handler to simulate a specific behavior. -class TunnelMonitorStub: TunnelMonitorProtocol { +class TunnelMonitorStub: TunnelMonitorProtocol, @unchecked Sendable { enum Command { case start, stop } diff --git a/ios/Routing/Coordinator.swift b/ios/Routing/Coordinator.swift index 46e343e64877..8ee1ff8c5c91 100644 --- a/ios/Routing/Coordinator.swift +++ b/ios/Routing/Coordinator.swift @@ -15,7 +15,8 @@ import UIKit Coordinators help to abstract the navigation and business logic from view controllers making them more manageable and reusable. */ -open class Coordinator: NSObject { +@MainActor +open class Coordinator: NSObject, Sendable { /// Private trace log. private lazy var logger = Logger(label: "\(Self.self)") @@ -82,7 +83,7 @@ open class Coordinator: NSObject { /** Protocol describing coordinators that can be presented using modal presentation. */ -public protocol Presentable: Coordinator { +public protocol Presentable: Coordinator, Sendable { /** View controller that is presented modally. It's expected it to be the topmost view controller managed by coordinator. @@ -177,7 +178,7 @@ extension Presentable { Automatically removes itself from parent. */ - public func dismiss(animated: Bool, completion: (() -> Void)? = nil) { + public func dismiss(animated: Bool, completion: (@MainActor () -> Void)? = nil) { removeFromParent() presentedViewController.dismiss(animated: animated, completion: completion) @@ -186,7 +187,7 @@ extension Presentable { /** Add block based observer triggered if coordinator is dismissed via user interaction. */ - public func onInteractiveDismissal(_ handler: @escaping (Coordinator) -> Void) { + public func onInteractiveDismissal(_ handler: @escaping @Sendable (Coordinator) -> Void) { interactiveDismissalObservers.append(handler) } } diff --git a/ios/Routing/ModalPresentationConfiguration.swift b/ios/Routing/ModalPresentationConfiguration.swift index d50297fefc39..15fccafdea4e 100644 --- a/ios/Routing/ModalPresentationConfiguration.swift +++ b/ios/Routing/ModalPresentationConfiguration.swift @@ -11,6 +11,7 @@ import UIKit /** A struct holding modal presentation configuration. */ +@MainActor public struct ModalPresentationConfiguration { var preferredContentSize: CGSize? var modalPresentationStyle: UIModalPresentationStyle? diff --git a/ios/Routing/PresentationControllerDismissalInterceptor.swift b/ios/Routing/PresentationControllerDismissalInterceptor.swift index 90d92a87038b..e56e6f15a1ab 100644 --- a/ios/Routing/PresentationControllerDismissalInterceptor.swift +++ b/ios/Routing/PresentationControllerDismissalInterceptor.swift @@ -15,7 +15,7 @@ import UIKit final class PresentationControllerDismissalInterceptor: NSObject, UIAdaptivePresentationControllerDelegate { private let dismissHandler: (UIPresentationController) -> Void - private let forwardingTarget: UIAdaptivePresentationControllerDelegate? + nonisolated(unsafe) private let forwardingTarget: UIAdaptivePresentationControllerDelegate? private let protocolSelectors: [Selector] init( diff --git a/ios/Routing/Router/AppRouteProtocol.swift b/ios/Routing/Router/AppRouteProtocol.swift index 85fbd5302888..b15776ae2789 100644 --- a/ios/Routing/Router/AppRouteProtocol.swift +++ b/ios/Routing/Router/AppRouteProtocol.swift @@ -11,7 +11,7 @@ import Foundation /** Formal protocol describing a group of routes. */ -public protocol AppRouteGroupProtocol: Comparable, Equatable, Hashable { +public protocol AppRouteGroupProtocol: Comparable, Equatable, Hashable, Sendable { /** Returns `true` if group is presented modally, otherwise `false` if group is a part of root view controller. @@ -44,7 +44,7 @@ extension AppRouteGroupProtocol { /** Formal protocol describing a single route. */ -public protocol AppRouteProtocol: Equatable, Hashable { +public protocol AppRouteProtocol: Equatable, Hashable, Sendable { associatedtype RouteGroupType: AppRouteGroupProtocol /** diff --git a/ios/Routing/Router/ApplicationRouter.swift b/ios/Routing/Router/ApplicationRouter.swift index d5658751431f..c55daf24001e 100644 --- a/ios/Routing/Router/ApplicationRouter.swift +++ b/ios/Routing/Router/ApplicationRouter.swift @@ -13,8 +13,9 @@ import UIKit /** Main application router. */ -public final class ApplicationRouter { - private let logger = Logger(label: "ApplicationRouter") +@MainActor +public final class ApplicationRouter: Sendable { + nonisolated(unsafe) private let logger = Logger(label: "ApplicationRouter") private(set) var modalStack: [RouteType.RouteGroupType] = [] private(set) var presentedRoutes: [RouteType.RouteGroupType: [PresentedRoute]] = [:] @@ -95,7 +96,7 @@ public final class ApplicationRouter { _ route: RouteType, animated: Bool, metadata: Any?, - completion: @escaping (PendingPresentationResult) -> Void + completion: @escaping @Sendable @MainActor (PendingPresentationResult) -> Void ) { /** Pass sub-route for routes supporting sub-navigation. @@ -160,15 +161,19 @@ public final class ApplicationRouter { /* Synchronize router when modal controllers are removed by swipe. */ - if let presentable = coordinator as? Presentable { - presentable.onInteractiveDismissal { [weak self] coordinator in - self?.handleInteractiveDismissal(route: route, coordinator: coordinator) + MainActor.assumeIsolated { + if let presentable = coordinator as? Presentable { + presentable.onInteractiveDismissal { [weak self] coordinator in + MainActor.assumeIsolated { + self?.handleInteractiveDismissal(route: route, coordinator: coordinator) + } + } } - } - self.addPresentedRoute(PresentedRoute(route: route, coordinator: coordinator)) + self.addPresentedRoute(PresentedRoute(route: route, coordinator: coordinator)) - completion(.success) + completion(.success) + } } } else { completion(.drop) @@ -178,7 +183,7 @@ public final class ApplicationRouter { private func dismissGroup( _ dismissGroup: RouteType.RouteGroupType, animated: Bool, - completion: @escaping (PendingDismissalResult) -> Void + completion: @escaping @Sendable (PendingDismissalResult) -> Void ) { /** Check if routes corresponding to the group requested for dismissal are present. @@ -224,7 +229,7 @@ public final class ApplicationRouter { private func dismissRoute( _ dismissRoute: RouteType, animated: Bool, - completion: @escaping (PendingDismissalResult) -> Void + completion: @escaping @Sendable (PendingDismissalResult) -> Void ) { var routes = presentedRoutes[dismissRoute.routeGroup] ?? [] @@ -304,19 +309,21 @@ public final class ApplicationRouter { case let .dismiss(dismissMatch): handleDismissal(dismissMatch, animated: pendingRoute.animated) { result in - switch result { - case .success, .drop: - self.finishPendingRoute(pendingRoute) - - case .blockedByModalAbove: - /** - If router cannot dismiss modal because there is one above, - try walking down the queue and see if there is a dismissal request that could - resolve that. - */ - self.processPendingRoutes( - skipRouteGroups: skipRouteGroups.union([dismissMatch.routeGroup]) - ) + MainActor.assumeIsolated { + switch result { + case .success, .drop: + self.finishPendingRoute(pendingRoute) + + case .blockedByModalAbove: + /** + If router cannot dismiss modal because there is one above, + try walking down the queue and see if there is a dismissal request that could + resolve that. + */ + self.processPendingRoutes( + skipRouteGroups: skipRouteGroups.union([dismissMatch.routeGroup]) + ) + } } } } @@ -325,7 +332,7 @@ public final class ApplicationRouter { private func handleDismissal( _ dismissMatch: DismissMatch, animated: Bool, - completion: @escaping (PendingDismissalResult) -> Void + completion: @escaping @Sendable (PendingDismissalResult) -> Void ) { switch dismissMatch { case let .singleRoute(route): diff --git a/ios/Routing/Router/ApplicationRouterDelegate.swift b/ios/Routing/Router/ApplicationRouterDelegate.swift index a98870d30383..2200066a1fe6 100644 --- a/ios/Routing/Router/ApplicationRouterDelegate.swift +++ b/ios/Routing/Router/ApplicationRouterDelegate.swift @@ -11,7 +11,7 @@ import Foundation /** Application router delegate */ -public protocol ApplicationRouterDelegate: AnyObject { +public protocol ApplicationRouterDelegate: AnyObject, Sendable { associatedtype RouteType: AppRouteProtocol /** @@ -21,7 +21,7 @@ public protocol ApplicationRouterDelegate: AnyObject { _ router: ApplicationRouter, presentWithContext context: RoutePresentationContext, animated: Bool, - completion: @escaping (Coordinator) -> Void + completion: @escaping @Sendable (Coordinator) -> Void ) /** @@ -30,7 +30,7 @@ public protocol ApplicationRouterDelegate: AnyObject { func applicationRouter( _ router: ApplicationRouter, dismissWithContext context: RouteDismissalContext, - completion: @escaping () -> Void + completion: @escaping @Sendable () -> Void ) /** @@ -57,6 +57,6 @@ public protocol ApplicationRouterDelegate: AnyObject { func applicationRouter( _ router: ApplicationRouter, handleSubNavigationWithContext context: RouteSubnavigationContext, - completion: @escaping () -> Void + completion: @escaping @Sendable @MainActor () -> Void ) } diff --git a/ios/Routing/Router/ApplicationRouterTypes.swift b/ios/Routing/Router/ApplicationRouterTypes.swift index 7be142adaf5d..1a661dfd2aa4 100644 --- a/ios/Routing/Router/ApplicationRouterTypes.swift +++ b/ios/Routing/Router/ApplicationRouterTypes.swift @@ -11,10 +11,10 @@ import Foundation /** Struct describing a routing request for presentation or dismissal. */ -struct PendingRoute { +struct PendingRoute: Sendable { var operation: RouteOperation var animated: Bool - var metadata: Any? + nonisolated(unsafe) var metadata: Any? } extension PendingRoute: Equatable { @@ -26,7 +26,7 @@ extension PendingRoute: Equatable { /** Enum type describing an attempt to fulfill the route presentation request. **/ -enum PendingPresentationResult { +enum PendingPresentationResult: Sendable { /** Successfully presented the route. */ @@ -52,7 +52,7 @@ enum PendingPresentationResult { /** Enum type describing an attempt to fulfill the route dismissal request. */ -enum PendingDismissalResult { +enum PendingDismissalResult: Sendable { /** Successfully dismissed the route. */ @@ -76,7 +76,7 @@ enum PendingDismissalResult { /** Enum describing operation over the route. */ -enum RouteOperation: Equatable, CustomDebugStringConvertible { +enum RouteOperation: Equatable, CustomDebugStringConvertible, Sendable { /** Present route. */ @@ -114,7 +114,7 @@ enum RouteOperation: Equatable, CustomDebugStringCo /** Enum type describing a single route or a group of routes requested to be dismissed. */ -enum DismissMatch: Equatable, CustomDebugStringConvertible { +enum DismissMatch: Equatable, CustomDebugStringConvertible, Sendable { case group(RouteType.RouteGroupType) case singleRoute(RouteType) @@ -143,7 +143,7 @@ enum DismissMatch: Equatable, CustomDebugStringConv /** Struct describing presented route. */ -public struct PresentedRoute: Equatable { +public struct PresentedRoute: Equatable, Sendable { public var route: RouteType public var coordinator: Coordinator } @@ -171,7 +171,7 @@ public struct RouteDismissalContext { /** Struct holding information used by delegate to perform presentation of a specific route. */ -public struct RoutePresentationContext { +public struct RoutePresentationContext: Sendable { /** Route that's being presented. */ @@ -185,13 +185,13 @@ public struct RoutePresentationContext { /** Metadata associated with the route. */ - public var metadata: Any? + nonisolated(unsafe) public var metadata: Any? } /** Struct holding information used by delegate to perform sub-navigation of the route in subject. */ -public struct RouteSubnavigationContext { +public struct RouteSubnavigationContext: Sendable { public var presentedRoute: PresentedRoute public var route: RouteType public var isAnimated: Bool diff --git a/ios/RoutingTests/RouterBlockDelegate.swift b/ios/RoutingTests/RouterBlockDelegate.swift index 977454b7e42b..07204d5fa91f 100644 --- a/ios/RoutingTests/RouterBlockDelegate.swift +++ b/ios/RoutingTests/RouterBlockDelegate.swift @@ -9,26 +9,28 @@ import Foundation import Routing -class RouterBlockDelegate: ApplicationRouterDelegate { +final class RouterBlockDelegate: ApplicationRouterDelegate, @unchecked Sendable { var handleRoute: ((RoutePresentationContext, Bool, (Coordinator) -> Void) -> Void)? var handleDismiss: ((RouteDismissalContext, () -> Void) -> Void)? var shouldPresent: ((RouteType) -> Bool)? var shouldDismiss: ((RouteDismissalContext) -> Bool)? - var handleSubnavigation: ((RouteSubnavigationContext, () -> Void) -> Void)? + var handleSubnavigation: (@Sendable @MainActor (RouteSubnavigationContext, () -> Void) -> Void)? - func applicationRouter( + nonisolated func applicationRouter( _ router: ApplicationRouter, presentWithContext context: RoutePresentationContext, animated: Bool, - completion: @escaping (Coordinator) -> Void + completion: @escaping @Sendable (Coordinator) -> Void ) { - handleRoute?(context, animated, completion) ?? completion(Coordinator()) + MainActor.assumeIsolated { + handleRoute?(context, animated, completion) ?? completion(Coordinator()) + } } func applicationRouter( _ router: ApplicationRouter, dismissWithContext context: RouteDismissalContext, - completion: @escaping () -> Void + completion: @escaping @Sendable () -> Void ) { handleDismiss?(context, completion) ?? completion() } @@ -47,8 +49,10 @@ class RouterBlockDelegate: ApplicationRouterDelegat func applicationRouter( _ router: ApplicationRouter, handleSubNavigationWithContext context: RouteSubnavigationContext, - completion: @escaping () -> Void + completion: @escaping @Sendable @MainActor () -> Void ) { - handleSubnavigation?(context, completion) ?? completion() + MainActor.assumeIsolated { + handleSubnavigation?(context, completion) ?? completion() + } } } diff --git a/ios/RoutingTests/RoutingTests.swift b/ios/RoutingTests/RoutingTests.swift index c271a98970ca..bd52c3cb0795 100644 --- a/ios/RoutingTests/RoutingTests.swift +++ b/ios/RoutingTests/RoutingTests.swift @@ -9,6 +9,7 @@ @testable import Routing import XCTest +@MainActor final class RoutingTests: XCTestCase { private func createDelegate(shouldPresent: Bool = true) -> RouterBlockDelegate { let delegate = RouterBlockDelegate() @@ -18,11 +19,9 @@ final class RoutingTests: XCTestCase { return delegate } -} -// MARK: Horizontal flow tests + // MARK: Horizontal flow tests -extension RoutingTests { func testPresentHorizontalRoute() throws { enum TestRoute: AppRouteProtocol { case one @@ -71,11 +70,9 @@ extension RoutingTests { XCTAssertEqual(router.presentedRoutes[.horizontal]?.count, 1) } -} -// MARK: Modal flow tests + // MARK: Modal flow tests -extension RoutingTests { func testPresentModalRoutesOfDifferentLevels() throws { enum TestRoute: AppRouteProtocol { case one, two