Swift 6 Migration (#5733)

* Enable Swift 6.2 with default main actor isolation for the app target

Turn on SWIFT_VERSION 6.2, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the ElementX target, matching the NSE and Share Extension (which already use 6.2 without the main actor default).

# Conflicts:
#	ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

* Update the code generation templates for Swift 6

Emit nonisolated/@unchecked Sendable mocks and nonisolated string enums, and mark async mock methods @concurrent so they keep suspending like the real implementations (otherwise intermediate states are unobservable in tests). Includes the regenerated mocks and strings.

# Conflicts:
#	ElementX/Sources/Mocks/Generated/GeneratedMocks.swift

* Make the data layer and services nonisolated and Sendable

Annotate the value-type models, Rust SDK proxy wrappers, timeline item graph, settings, logging and NSE-shared types as nonisolated, and conform the service protocols to Sendable so their existentials can cross isolation boundaries. These types run off the main actor and no longer inherit the target's default main actor isolation.

* Adopt the new isolation across call sites, mocks and signatures

Update call sites, closure signatures and mock configuration helpers to match the nonisolated/Sendable data layer, including the main actor isolated mock inits and the @Sendable filter on recentlyVisitedRooms.

^ Conflicts:
^	ElementX/Sources/Services/SecureBackup/SecureBackupController.swift

* Remove redundant @MainActor annotations

With the app target defaulting to main actor isolation, the explicit @MainActor on view models, coordinators, views and their protocols is no longer required.

* Fix concurrency hotspots surfaced by the migration

Resolve the room summary main-thread deadlock by moving diff processing onto AsyncStreams, add the SDKListener.onMainActor FIFO bridge for SDK callbacks, hop KVO/timer/UIKit callbacks to the main actor via assumeIsolated, add isolated deinits, and apply the few justified nonisolated(unsafe) escapes for queue-confined Apple APIs.

* Fix the unit tests for Swift 6 concurrency

Hop the waitForConfirmation body onto the caller's actor (Swift Testing runs confirmation closures off-actor), and adjust the affected suites and async signatures.

* offload diffs on concurrent threads instead of main actor

* make room info protocols sendable

* use a task group for fetching data so that everything runs concurrently in the TextRoomTimelineView when getting the preview.

* use the generic onMainActor implementation, which already makes a stream and send the continuation on the main actor.

* small pr comments issues being addressed

* update enterprise

* Update StaticCode to 1.1.0 and specify the required Swift isolation.

* fix ElementCallServiceTests

* update enterprise

* re-generate mocks

* update enterprise

* enterprise new code conformed to swift 6

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Mauro
2026-06-18 17:55:31 +02:00
committed by GitHub
parent 7edca8b5a1
commit a520d94760
444 changed files with 3677 additions and 3361 deletions
+6
View File
@@ -9653,8 +9653,11 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(APP_NAME)";
SDKROOT = iphoneos;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_OBJC_BRIDGING_HEADER = "ElementX/SupportingFiles/ElementX-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
SWIFT_VERSION = 6.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -9685,8 +9688,11 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(APP_NAME)";
SDKROOT = iphoneos;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_OBJC_BRIDGING_HEADER = "ElementX/SupportingFiles/ElementX-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
SWIFT_VERSION = 6.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -4,8 +4,7 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -79,8 +74,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "HTTPS_PROXY"
@@ -115,8 +108,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -4,8 +4,7 @@
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -78,8 +73,6 @@
ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference>
</MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
@@ -87,8 +80,6 @@
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -40,8 +40,6 @@ class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol {
MXLog.configure(currentTarget: "accessibility-tests")
let appSettings = AppSettings.volatile()
guard let name = ProcessInfo.accessibilityViewID,
let previewType = TestablePreviewsDictionary.dictionary[name] else {
fatalError("Unable to launch with unknown screen.")
@@ -125,7 +123,6 @@ struct PreviewsWrapperView: View {
self.previews = previews
}
@MainActor
func updateCurrentIndex() async {
let newIndex = currentIndex + 1
guard newIndex < previews.count else {
@@ -142,14 +139,17 @@ struct PreviewsWrapperView: View {
switch fulfillmentSource {
case .publisher(let publisher):
_ = await publisher
// Not sure whye byt some publisher seem to not properly comunicate their completion,
// so we added a timeout. Since we are going to migrate from publishers to stream,
// this is a temporary solution
// Not sure why but some publishers seem to not properly communicate their completion,
// so we added a timeout. Since we are going to migrate from publishers to streams,
// this is a temporary solution
var iterator = publisher
.timeout(.seconds(1), scheduler: DispatchQueue.main)
.values.first { $0 == true }
.values
.makeAsyncIterator()
while let value = await iterator.next(isolation: #isolation), value == false { }
case .sequence(let sequence):
_ = await sequence.first { $0 == true }
var iterator = sequence.makeAsyncIterator()
while let value = await iterator.next(isolation: #isolation), value == false { }
case .none:
break
}
+3 -3
View File
@@ -9,7 +9,7 @@
import Foundation
import Synchronization
final class AppHooks: AppHooksProtocol {
final nonisolated class AppHooks: AppHooksProtocol {
#if IS_MAIN_APP
func configure(with userSession: UserSessionProtocol?) async {
await roomScreenHook.configure(with: userSession)
@@ -107,10 +107,10 @@ final class AppHooks: AppHooksProtocol {
}
}
protocol AppHooksProtocol: Sendable {
nonisolated protocol AppHooksProtocol: Sendable {
func setUp()
}
extension AppHooksProtocol {
nonisolated extension AppHooksProtocol {
func setUp() { }
}
@@ -8,8 +8,8 @@
import Foundation
protocol AppSettingsHookProtocol {
func configure(_ appSettings: AppSettings) -> AppSettings
nonisolated protocol AppSettingsHookProtocol: Sendable {
@MainActor func configure(_ appSettings: AppSettings) -> AppSettings
}
struct DefaultAppSettingsHook: AppSettingsHookProtocol {
@@ -8,7 +8,7 @@
import Foundation
protocol BugReportHookProtocol {
nonisolated protocol BugReportHookProtocol: Sendable {
func update(_ bugReport: BugReport) -> BugReport
}
@@ -8,11 +8,11 @@
import MatrixRustSDK
protocol ClientBuilderHookProtocol: Sendable {
nonisolated protocol ClientBuilderHookProtocol: Sendable {
func configure(_ builder: ClientBuilder) -> ClientBuilder
}
struct DefaultClientBuilderHook: ClientBuilderHookProtocol {
nonisolated struct DefaultClientBuilderHook: ClientBuilderHookProtocol {
func configure(_ builder: ClientBuilder) -> ClientBuilder {
builder
}
@@ -8,7 +8,7 @@
import Compound
protocol CompoundHookProtocol {
nonisolated protocol CompoundHookProtocol: Sendable {
@MainActor func override(colors: CompoundColors, uiColors: CompoundUIColors)
}
@@ -7,7 +7,7 @@
import SwiftUI
protocol DeveloperOptionsScreenHookProtocol {
nonisolated protocol DeveloperOptionsScreenHookProtocol: Sendable {
func generalSectionRows() -> AnyView?
}
@@ -8,8 +8,8 @@
import Foundation
protocol OAuthPresenterHookProtocol {
func update(_ url: URL) -> URL
nonisolated protocol OAuthPresenterHookProtocol: Sendable {
@MainActor func update(_ url: URL) -> URL
}
struct DefaultOAuthPresenterHook: OAuthPresenterHookProtocol {
@@ -12,7 +12,7 @@ import SwiftUI
var actions: AnyPublisher<SecureBackupRecoveryKeyScreenCoordinatorAction, Never> { get }
}
protocol RecoveryKeyScreenHookProtocol {
nonisolated protocol RecoveryKeyScreenHookProtocol: Sendable {
@MainActor func makeCoordinator(parameters: SecureBackupRecoveryKeyScreenCoordinatorParameters, homeserver: String) -> any SecureBackupRecoveryKeyCoordinatorProtocol
}
@@ -8,9 +8,9 @@
import Foundation
protocol RoomScreenHookProtocol {
func configure(with userSession: UserSessionProtocol?) async
func update(_ viewState: RoomScreenViewState) -> RoomScreenViewState
nonisolated protocol RoomScreenHookProtocol: Sendable {
@MainActor func configure(with userSession: UserSessionProtocol?) async
@MainActor func update(_ viewState: RoomScreenViewState) -> RoomScreenViewState
}
struct DefaultRoomScreenHook: RoomScreenHookProtocol {
@@ -8,10 +8,10 @@
import MatrixRustSDK
protocol TracingHookProtocol: Sendable {
nonisolated protocol TracingHookProtocol: Sendable {
func update(_ configuration: TracingConfiguration, with rageshakeURL: RemotePreference<RageshakeConfiguration>)
}
struct DefaultTracingHook: TracingHookProtocol {
nonisolated struct DefaultTracingHook: TracingHookProtocol {
func update(_ configuration: TracingConfiguration, with rageshakeURL: RemotePreference<RageshakeConfiguration>) { }
}
@@ -1262,6 +1262,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
// this does not crash on the main thread, even though the whole AppCoordinator is on the Main actor. As such,
// we hop onto the main actor explicitly before pausing.
// https://sentry.tools.element.io/organizations/element/issues/4477794/
// BGTask isn't annotated but is documented as thread-safe, with
// setTaskCompleted(success:) being callable from any thread.
nonisolated(unsafe) let task = task
task.expirationHandler = { @Sendable [weak self] in
MXLog.info("Background app refresh task is about to expire.")
@@ -8,7 +8,6 @@
import Foundation
@MainActor
protocol AppCoordinatorProtocol: CoordinatorProtocol {
var windowManager: SecureWindowManagerProtocol { get }
@@ -36,7 +36,7 @@ class AppMediator: AppMediatorProtocol {
}
}
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
func beginBackgroundTask(expirationHandler handler: (@MainActor @Sendable () -> Void)?) -> UIBackgroundTaskIdentifier {
application.beginBackgroundTask(expirationHandler: handler)
}
@@ -10,14 +10,13 @@ import Foundation
import UIKit
// sourcery: AutoMockable
@MainActor
protocol AppMediatorProtocol {
var windowManager: WindowManagerProtocol { get }
var networkMonitor: NetworkMonitorProtocol { get }
var appState: UIApplication.State { get }
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
func beginBackgroundTask(expirationHandler handler: (@MainActor @Sendable () -> Void)?) -> UIBackgroundTaskIdentifier
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
@@ -14,6 +14,7 @@ struct Application: App {
@Environment(\.openURL) private var openURL
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
@Environment(\.displayScale) private var displayScale
private var appCoordinator: AppCoordinatorProtocol!
@@ -51,6 +52,7 @@ struct Application: App {
appCoordinator.handleUserActivity(userActivity)
}
.task {
UserAgentBuilder.displayScale = displayScale
appCoordinator.start()
appCoordinator.windowManager.configure(withOpenWindowAction: openWindow,
dismissWindowAction: dismissWindow)
@@ -8,7 +8,6 @@
import SwiftUI
@MainActor
protocol CoordinatorProtocol: AnyObject {
func start()
func stop()
@@ -9,7 +9,6 @@
import Foundation
// periphery:ignore - markdown protocol
@MainActor
protocol FlowCoordinatorProtocol {
func start(animated: Bool)
func handleAppRoute(_ appRoute: AppRoute, animated: Bool)
@@ -25,7 +24,6 @@ extension FlowCoordinatorProtocol {
/// Core parameters that are shared across the main flows for easy dependency injection.
///
/// Please do **not** pass this type directly to screen coordinators/view models.
@MainActor
struct CommonFlowParameters {
let userSession: UserSessionProtocol
let bugReportService: BugReportServiceProtocol
@@ -10,7 +10,6 @@ import Foundation
/// A CoordinatorProtocol wrapper and type erasing component that allows
/// dynamically presenting arbitrary screens
@MainActor
class NavigationModule: Identifiable, Hashable {
let id = UUID()
@@ -17,7 +17,6 @@ import SwiftUI
var dismissalCallback: (() -> Void)?
}
@MainActor
@Observable class TabDetails {
/// A unique tab that identifies the tab for selection.
let tag: Tag
@@ -58,7 +57,7 @@ import SwiftUI
module.id
}
@MainActor var coordinator: CoordinatorProtocol? {
var coordinator: CoordinatorProtocol? {
module.coordinator
}
}
@@ -9,7 +9,7 @@
import Foundation
/// A type that represents how to set up the analytics module in the app.
struct AnalyticsConfiguration {
nonisolated struct AnalyticsConfiguration {
/// The host to use for PostHog analytics.
let host: String
/// The public key for submitting analytics.
@@ -8,7 +8,7 @@
import Foundation
enum AnalyticsConsentState: String, Codable {
nonisolated enum AnalyticsConsentState: String, Codable {
case optedOut
case optedIn
case unknown
@@ -9,7 +9,7 @@
import SwiftUI
/// Used to specify the user's app specific appearance preference
enum AppAppearance: CaseIterable, Codable {
nonisolated enum AppAppearance: CaseIterable, Codable {
case system
case dark
case light
@@ -14,7 +14,7 @@ import Foundation
import SwiftUI
/// Common settings between app and NSE
protocol CommonSettingsProtocol: AnyObject, Sendable {
nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable {
var lastNotificationBootTime: TimeInterval? { get set }
var selectedNotificationTone: NotificationTone? { get set }
@@ -27,7 +27,7 @@ protocol CommonSettingsProtocol: AnyObject, Sendable {
var hideQuietNotificationAlerts: Bool { get }
}
enum AppBuildType {
nonisolated enum AppBuildType {
case debug
case nightly
case release
@@ -36,7 +36,7 @@ enum AppBuildType {
/// Store Element specific app settings.
///
/// State is persisted in `UserDefaults`, which is thread-safe per Apple's documentation, hence `@unchecked`.
final class AppSettings: @unchecked Sendable {
final nonisolated class AppSettings: @unchecked Sendable {
fileprivate enum UserDefaultsKeys: String, PreferenceKeyable {
case lastVersionLaunched
case seenInvites
@@ -173,16 +173,16 @@ final class AppSettings: @unchecked Sendable {
/// used to detect when migrations should be run. When `nil` the app may have been
/// deleted between runs so should clear data in the shared container and keychain.
@UserPreference
var lastVersionLaunched: String?
nonisolated(unsafe) var lastVersionLaunched: String?
/// The Set of room identifiers of invites that the user already saw in the invites list.
/// This Set is being used to implement badges for unread invites.
@UserPreference
var seenInvites: Set<String>
nonisolated(unsafe) var seenInvites: Set<String>
/// Defaults to `true` for new users, and we use a migration to set it to `false` for existing users.
@UserPreference
var hasSeenNewSoundBanner: Bool
nonisolated(unsafe) var hasSeenNewSoundBanner: Bool
/// The initial set of account providers shown to the user in the authentication flow.
///
@@ -227,7 +227,7 @@ final class AppSettings: @unchecked Sendable {
let elementProAppStoreURL: URL = "https://apps.apple.com/app/element-pro-for-work/id6502951615"
@UserPreference
var appAppearance: AppAppearance
nonisolated(unsafe) var appAppearance: AppAppearance
// MARK: - Security
@@ -239,7 +239,7 @@ final class AppSettings: @unchecked Sendable {
let appLockPINCodeBlockList = ["0000", "1234"]
/// The number of attempts the user has made to unlock the app with a PIN code (resets when unlocked).
@UserPreference
var appLockNumberOfPINAttempts: Int
nonisolated(unsafe) var appLockNumberOfPINAttempts: Int
// MARK: - Authentication
@@ -247,15 +247,17 @@ final class AppSettings: @unchecked Sendable {
let oAuthStaticRegistrations: [URL: String] = ["https://id.thirdroom.io/realms/thirdroom": "elementx"]
/// The redirect URL used for OAuth. For the normal case we don't actually need the bundle ID as the web authentication session handles the redirect internally.
/// However in the case where MAS sends the user to an external app, we need to make sure that the system will open the correct variant of the app (e.g. Nightly).
private(set) var oAuthRedirectURL: URL! = URL(string: "https://element.io/oauth/ios/\(InfoPlistReader.main.bundleIdentifier)")
private(set) nonisolated(unsafe) var oAuthRedirectURL: URL! = URL(string: "https://element.io/oauth/ios/\(InfoPlistReader.main.bundleIdentifier)")
private(set) lazy var oAuthConfiguration = OAuthConfiguration(clientName: InfoPlistReader.main.bundleDisplayName,
redirectURI: oAuthRedirectURL,
clientURI: websiteURL,
logoURI: logoURL,
tosURI: acceptableUseURL,
policyURI: privacyURL,
staticRegistrations: oAuthStaticRegistrations.mapKeys { $0.absoluteString })
var oAuthConfiguration: OAuthConfiguration {
OAuthConfiguration(clientName: InfoPlistReader.main.bundleDisplayName,
redirectURI: oAuthRedirectURL,
clientURI: websiteURL,
logoURI: logoURL,
tosURI: acceptableUseURL,
policyURI: privacyURL,
staticRegistrations: oAuthStaticRegistrations.mapKeys { $0.absoluteString })
}
/// Whether or not the Create Account button is shown on the start screen.
///
@@ -278,33 +280,33 @@ final class AppSettings: @unchecked Sendable {
}
@UserPreference
var enableNotifications: Bool
nonisolated(unsafe) var enableNotifications: Bool
@UserPreference
var enableInAppNotifications: Bool
nonisolated(unsafe) var enableInAppNotifications: Bool
@UserPreference
var hideQuietNotificationAlerts: Bool
nonisolated(unsafe) var hideQuietNotificationAlerts: Bool
/// Tag describing which set of device specific rules a pusher executes.
@UserPreference
var pusherProfileTag: String?
nonisolated(unsafe) var pusherProfileTag: String?
/// The device's last boot time as recorded by the NSE.
@UserPreference
var lastNotificationBootTime: TimeInterval?
nonisolated(unsafe) var lastNotificationBootTime: TimeInterval?
/// The sound played when delivering noisy notifications. If nil, use the ElementX default
@UserPreference
var selectedNotificationTone: NotificationTone?
nonisolated(unsafe) var selectedNotificationTone: NotificationTone?
// MARK: - Logging
@UserPreference
var logLevel: LogLevel
nonisolated(unsafe) var logLevel: LogLevel
@UserPreference
var traceLogPacks: Set<TraceLogPack>
nonisolated(unsafe) var traceLogPacks: Set<TraceLogPack>
// MARK: - Bug report
@@ -332,46 +334,46 @@ final class AppSettings: @unchecked Sendable {
/// Whether the user has opted in to send analytics.
@UserPreference
var analyticsConsentState: AnalyticsConsentState
nonisolated(unsafe) var analyticsConsentState: AnalyticsConsentState
@UserPreference
var hasRunNotificationPermissionsOnboarding: Bool
nonisolated(unsafe) var hasRunNotificationPermissionsOnboarding: Bool
@UserPreference
var hasRunIdentityConfirmationOnboarding: Bool
nonisolated(unsafe) var hasRunIdentityConfirmationOnboarding: Bool
@UserPreference
var hasRequestedLocationAlwaysLocationAuthorization: Bool
nonisolated(unsafe) var hasRequestedLocationAlwaysLocationAuthorization: Bool
@UserPreference
var frequentlyUsedSystemEmojis: [FrequentlyUsedEmoji]
nonisolated(unsafe) var frequentlyUsedSystemEmojis: [FrequentlyUsedEmoji]
// MARK: - Live Location
@UserPreference
var liveLocationSharingSessionsByRoomID: [String: LiveLocationSession]
nonisolated(unsafe) var liveLocationSharingSessionsByRoomID: [String: LiveLocationSession]
@UserPreference
var liveLocationMinimumDistanceUpdate: Int
nonisolated(unsafe) var liveLocationMinimumDistanceUpdate: Int
@UserPreference
var liveLocationDisclaimerDisplayed: Bool
nonisolated(unsafe) var liveLocationDisclaimerDisplayed: Bool
// MARK: - Home Screen
@UserPreference
var roomListActivityVisibility: RoomListActivityVisibility
nonisolated(unsafe) var roomListActivityVisibility: RoomListActivityVisibility
// MARK: - Room Screen
@UserPreference
var viewSourceEnabled: Bool
nonisolated(unsafe) var viewSourceEnabled: Bool
@UserPreference
var optimizeMediaUploads: Bool
nonisolated(unsafe) var optimizeMediaUploads: Bool
@UserPreference
var voiceMessagePlaybackSpeed: AudioPlaybackSpeed
nonisolated(unsafe) var voiceMessagePlaybackSpeed: AudioPlaybackSpeed
/// Whether or not to show a warning on the media caption composer so the user knows
/// that captions might not be visible to users who are using other Matrix clients.
@@ -390,7 +392,7 @@ final class AppSettings: @unchecked Sendable {
let elementCallPosthogSentryDSN = "https://3bd2f95ba5554d4497da7153b552ffb5@sentry.tools.element.io/41"
@UserPreference
var elementCallBaseURLOverride: URL?
nonisolated(unsafe) var elementCallBaseURLOverride: URL?
// MARK: - Users
@@ -413,50 +415,50 @@ final class AppSettings: @unchecked Sendable {
// MARK: - Presence
@UserPreference
var sharePresence: Bool
nonisolated(unsafe) var sharePresence: Bool
// MARK: - Feature Flags
/// Others
@UserPreference
var fuzzyRoomListSearchEnabled: Bool
nonisolated(unsafe) var fuzzyRoomListSearchEnabled: Bool
@UserPreference
var lowPriorityFilterEnabled: Bool
nonisolated(unsafe) var lowPriorityFilterEnabled: Bool
/// Configuration to enable only signed device isolation mode for crypto. In this mode only devices signed by their owner will be considered in e2ee rooms.
@UserPreference
var enableOnlySignedDeviceIsolationMode: Bool
nonisolated(unsafe) var enableOnlySignedDeviceIsolationMode: Bool
@UserPreference
var knockingEnabled: Bool
nonisolated(unsafe) var knockingEnabled: Bool
@UserPreference
var threadsEnabled: Bool
nonisolated(unsafe) var threadsEnabled: Bool
@UserPreference
var roomThreadListEnabled: Bool
nonisolated(unsafe) var roomThreadListEnabled: Bool
@UserPreference
var focusEventOnNotificationTap: Bool
nonisolated(unsafe) var focusEventOnNotificationTap: Bool
@UserPreference
var linkPreviewsEnabled: Bool
nonisolated(unsafe) var linkPreviewsEnabled: Bool
@UserPreference
var jumpToReadMarkerEnabled: Bool
nonisolated(unsafe) var jumpToReadMarkerEnabled: Bool
@UserPreference
var linkNewDeviceEnabled: Bool
nonisolated(unsafe) var linkNewDeviceEnabled: Bool
@UserPreference
var automaticBackPaginationEnabled: Bool
nonisolated(unsafe) var automaticBackPaginationEnabled: Bool
@UserPreference
var clientPausingAndResumingEnabled: Bool
nonisolated(unsafe) var clientPausingAndResumingEnabled: Bool
@UserPreference
var developerOptionsEnabled: Bool
nonisolated(unsafe) var developerOptionsEnabled: Bool
init(store: UserDefaultsProtocol) {
// UserDefaults to be used on reads and writes.
@@ -509,9 +511,9 @@ final class AppSettings: @unchecked Sendable {
}
}
extension AppSettings: CommonSettingsProtocol { }
nonisolated extension AppSettings: CommonSettingsProtocol { }
private extension UserPreference {
private nonisolated extension UserPreference {
convenience init(key: AppSettings.UserDefaultsKeys, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) {
self.init(key: key as any PreferenceKeyable, defaultValue: defaultValue, storage: backingStorage, mode: mode)
}
@@ -5,7 +5,7 @@
// Please see LICENSE files in the repository root for full details.
//
enum AudioPlaybackSpeed: Float, CaseIterable, Codable {
nonisolated enum AudioPlaybackSpeed: Float, CaseIterable, Codable {
case `default` = 1.0
case fast = 1.5
case fastest = 2.0
@@ -6,7 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
struct FrequentlyUsedEmoji: Codable, Hashable {
nonisolated struct FrequentlyUsedEmoji: Codable, Hashable {
let count: UInt
let key: String
@@ -7,7 +7,7 @@
import Foundation
struct LiveLocationSession: Codable, Equatable {
nonisolated struct LiveLocationSession: Codable, Equatable {
let eventID: String
let expirationDate: Date
}
@@ -12,7 +12,7 @@ import Foundation
///
/// Either resolves to a locally-bundled MapTiler configuration, or to a remote
/// `style.json` URL
enum MapTilerSettings: Equatable {
nonisolated enum MapTilerSettings: Equatable {
/// All of the configuration necessary to use MapTiler maps.
///
/// The style IDs need to be generated with the account that the API key belongs to. For more information read
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct OAuthConfiguration {
nonisolated struct OAuthConfiguration {
let clientName: String
let redirectURI: URL
let clientURI: URL
@@ -22,7 +22,7 @@ struct OAuthConfiguration {
#if canImport(MatrixRustSDK)
import MatrixRustSDK
extension OAuthConfiguration {
nonisolated extension OAuthConfiguration {
var rustValue: MatrixRustSDK.OAuthConfiguration {
MatrixRustSDK.OAuthConfiguration(clientName: clientName,
redirectUri: redirectURI.absoluteString,
@@ -9,7 +9,7 @@
import Foundation
/// A type that represents how rageshakes are collected by the app.
enum RageshakeConfiguration: Equatable, Codable {
nonisolated enum RageshakeConfiguration: Equatable, Codable {
/// Rageshakes should be sent to the provided URL
case url(URL)
/// Rageshakes are disabled.
@@ -13,7 +13,7 @@ import Combine
///
/// Unlike ``UserPreference``, this type of setting isn't settable by the user, nor is the
/// remote value persisted between app launches.
class RemotePreference<T: Equatable> {
nonisolated class RemotePreference<T: Equatable> {
private let defaultValue: T
private let subject: CurrentValueSubject<T, Never>
var publisher: CurrentValuePublisher<T, Never> {
@@ -5,7 +5,7 @@
// Please see LICENSE files in the repository root for full details.
//
enum RoomListActivityVisibility: String, CaseIterable, Codable {
nonisolated enum RoomListActivityVisibility: String, CaseIterable, Codable {
/// Show unread badges for all unread messages (the default).
case current
/// Don't show badges but bold unread room names and their latest message
@@ -7,7 +7,7 @@
import Foundation
final class TrackedUserDefaults: UserDefaults, UserDefaultsProtocol {
final nonisolated class TrackedUserDefaults: UserDefaults, UserDefaultsProtocol {
private let lock = NSLock()
private nonisolated(unsafe) var _trackedSuites: [String] = []
@@ -8,7 +8,7 @@
import Foundation
public protocol UserDefaultsProtocol: AnyObject {
public nonisolated protocol UserDefaultsProtocol: AnyObject {
func data(forKey key: String) -> Data?
func object(forKey key: String) -> Any?
func removeObject(forKey key: String)
@@ -13,7 +13,7 @@ import Foundation
/// to listen for value changes. The publisher does not skip consecutive duplicates, as there is no
/// `Equatable` enforcement at this level.
@propertyWrapper
final class UserPreference<T: Codable> {
final nonisolated class UserPreference<T: Codable> {
static var remotePrefix: String {
"remote-"
}
@@ -94,7 +94,7 @@ final class UserPreference<T: Codable> {
// MARK: - UserPreference convenience initializers
extension UserPreference {
nonisolated extension UserPreference {
convenience init(key: any PreferenceKeyable, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) {
let storage: any KeyedStorage<T> = UserDefaultsStorage(userDefaults: backingStorage)
@@ -108,11 +108,11 @@ extension UserPreference {
// MARK: - Keys
protocol PreferenceKeyable: RawRepresentable where RawValue == String { }
nonisolated protocol PreferenceKeyable: RawRepresentable where RawValue == String { }
// MARK: - Storage
protocol KeyedStorage<Value> {
nonisolated protocol KeyedStorage<Value> {
associatedtype Value: Codable
subscript(key: String) -> Value? { get set }
@@ -122,7 +122,7 @@ protocol KeyedStorage<Value> {
///
/// When used with a `Value` that conforms to `PlistRepresentable` the Codable encode/decode
/// phase is skipped, and values are stored natively in the plist.
final class UserDefaultsStorage<Value: Codable>: KeyedStorage {
final nonisolated class UserDefaultsStorage<Value: Codable>: KeyedStorage {
private let userDefaults: UserDefaultsProtocol
init(userDefaults: UserDefaultsProtocol) {
@@ -181,11 +181,11 @@ final class UserDefaultsStorage<Value: Codable>: KeyedStorage {
}
}
private protocol Nullable {
private nonisolated protocol Nullable {
var isNil: Bool { get }
}
extension Optional: Nullable {
nonisolated extension Optional: Nullable {
var isNil: Bool {
switch self {
case .none:
@@ -198,13 +198,13 @@ extension Optional: Nullable {
}
}
extension Dictionary: KeyedStorage where Key == String, Value: Codable { }
nonisolated extension Dictionary: KeyedStorage where Key == String, Value: Codable { }
// MARK: - PlistRepresentable
/// A protocol to mark types as being plist compliant.
/// UserDefaultsStorage uses this protocol to avoid to encode/decode with Codable plist compliant values.
protocol PlistRepresentable { }
nonisolated protocol PlistRepresentable { }
extension Bool: PlistRepresentable { }
extension String: PlistRepresentable { }
@@ -7,7 +7,7 @@
import Foundation
public final class VolatileUserDefaults: UserDefaultsProtocol, Sendable {
public final nonisolated class VolatileUserDefaults: UserDefaultsProtocol, Sendable {
private let lock = NSLock()
private nonisolated(unsafe) var _storage: [String: Any]
@@ -11,7 +11,7 @@ import Foundation
import MapLibre
import MatrixRustSDK
enum Target: String {
nonisolated enum Target: String {
case mainApp = "elementx"
case nse
case shareExtension = "shareextension"
@@ -18,7 +18,6 @@ protocol SecureWindowManagerDelegate: AnyObject {
func windowManagerDidConfigureWindows(_ windowManager: SecureWindowManagerProtocol)
}
@MainActor
protocol SecureWindowManagerProtocol: WindowManagerProtocol {
var delegate: SecureWindowManagerDelegate? { get set }
@@ -10,7 +10,6 @@ import Combine
import SwiftState
import SwiftUI
@MainActor
protocol AuthenticationFlowCoordinatorDelegate: AnyObject {
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol)
}
@@ -9,7 +9,7 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum UntranslatedL10n {
internal nonisolated enum UntranslatedL10n {
/// Clear all data currently stored on this device?
/// Sign in again to access your account data and messages.
internal static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
@@ -43,7 +43,7 @@ internal enum UntranslatedL10n {
// MARK: - Implementation Details
extension UntranslatedL10n {
nonisolated extension UntranslatedL10n {
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
// No need to check languages, we always default to en for untranslated strings
guard let bundle = Bundle.lprojBundle(for: "en") else { return key }
+2 -2
View File
@@ -9,7 +9,7 @@ import Foundation
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
internal enum L10n {
internal nonisolated enum L10n {
/// Add avatar
internal static var a11yAddAvatar: String { return L10n.tr("Localizable", "a11y_add_avatar") }
/// Add reaction: %1$@
@@ -3973,7 +3973,7 @@ internal enum L10n {
// MARK: - Implementation Details
extension L10n {
nonisolated extension L10n {
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
// Use preferredLocalizations to get a language that is in the bundle and the user's preferred list of languages.
let languages = Bundle.overrideLocalizations ?? Bundle.app.preferredLocalizations
+4 -4
View File
@@ -122,18 +122,18 @@ extension ClientProxyMock {
let joinedRoomIDs = configuration.overrides.joinedRoomIDs
switch room.joinRequestType {
case .invite where !joinedRoomIDs.contains(room.id):
let roomProxy = await InvitedRoomProxyMock(.init(id: room.id, name: room.name, isSpace: room.isSpace))
let roomProxy = InvitedRoomProxyMock(.init(id: room.id, name: room.name, isSpace: room.isSpace))
return .invited(roomProxy)
case .knock where !joinedRoomIDs.contains(room.id):
let roomProxy = await KnockedRoomProxyMock(.init(id: room.id, name: room.name))
let roomProxy = KnockedRoomProxyMock(.init(id: room.id, name: room.name))
return .knocked(roomProxy)
default:
let roomProxy = await JoinedRoomProxyMock(.init(id: room.id, name: room.name, isSpace: room.isSpace, members: configuration.defaultRoomMembers))
let roomProxy = JoinedRoomProxyMock(.init(id: room.id, name: room.name, isSpace: room.isSpace, members: configuration.defaultRoomMembers))
roomProxy.loadOrFetchEventDetailsForReturnValue = .success(TimelineEventSDKMock())
return .joined(roomProxy)
}
} else if let spaceServiceRoom = configuration.spaceServiceConfiguration.topLevelSpaces.first(where: { $0.id == identifier }) {
let roomProxy = await JoinedRoomProxyMock(.init(id: spaceServiceRoom.id, name: spaceServiceRoom.name, isSpace: spaceServiceRoom.isSpace, members: configuration.defaultRoomMembers))
let roomProxy = JoinedRoomProxyMock(.init(id: spaceServiceRoom.id, name: spaceServiceRoom.name, isSpace: spaceServiceRoom.isSpace, members: configuration.defaultRoomMembers))
roomProxy.loadOrFetchEventDetailsForReturnValue = .success(TimelineEventSDKMock())
return .joined(roomProxy)
} else {
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@ struct RoomLiveLocationServiceMockConfiguration {
var shares: [LiveLocationShare] = []
}
extension RoomLiveLocationServiceMock {
@MainActor extension RoomLiveLocationServiceMock {
convenience init(_ configuration: RoomLiveLocationServiceMockConfiguration = .init()) {
self.init()
liveLocationsPublisher = CurrentValueSubject(configuration.shares).asCurrentValuePublisher()
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
struct RoomMemberProxyMockConfiguration {
nonisolated struct RoomMemberProxyMockConfiguration {
var userID: String
var displayName: String?
var avatarURL: URL?
@@ -14,7 +14,7 @@ struct RoomNotificationSettingsProxyMockConfiguration {
var isDefault = true
}
extension RoomNotificationSettingsProxyMock {
@MainActor extension RoomNotificationSettingsProxyMock {
convenience init(with configuration: RoomNotificationSettingsProxyMockConfiguration) {
self.init()
@@ -8,7 +8,7 @@
import MatrixRustSDK
extension RoomPowerLevelsProxyMock {
@MainActor extension RoomPowerLevelsProxyMock {
struct Configuration {
var canUserSendMessage = true
var canUserSendState = false
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
extension RoomPreviewProxyMock {
@MainActor extension RoomPreviewProxyMock {
struct Configuration {
var roomID = "1"
var canonicalAlias = "#3-body-problem:matrix.org"
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
import MatrixRustSDKMocks
extension ClientSDKMock {
nonisolated extension ClientSDKMock {
struct Configuration {
// MARK: Authentication
@@ -73,7 +73,7 @@ extension ClientSDKMock {
}
}
extension HomeserverLoginDetailsSDKMock {
nonisolated extension HomeserverLoginDetailsSDKMock {
convenience init(configuration: ClientSDKMock.Configuration) {
self.init()
@@ -89,7 +89,7 @@ extension HomeserverLoginDetailsSDKMock {
}
}
extension OAuthAuthorizationDataSDKMock {
nonisolated extension OAuthAuthorizationDataSDKMock {
convenience init(configuration: ClientSDKMock.Configuration) {
self.init()
@@ -11,7 +11,7 @@ import LoremSwiftum
import MatrixRustSDK
import MatrixRustSDKMocks
struct EventTimelineItemSDKMockConfiguration {
nonisolated struct EventTimelineItemSDKMockConfiguration {
var eventID: String = UUID().uuidString
var sender = ""
var senderProfile: ProfileDetails?
@@ -27,7 +27,7 @@ struct EventTimelineItemSDKMockConfiguration {
threadSummary: nil))
}
extension EventTimelineItem {
nonisolated extension EventTimelineItem {
init(configuration: EventTimelineItemSDKMockConfiguration) {
let lazyProvider = LazyTimelineItemProviderSDKMock()
lazyProvider.containsOnlyEmojisReturnValue = false
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
import MatrixRustSDKMocks
extension IdentityResetHandleSDKMock {
nonisolated extension IdentityResetHandleSDKMock {
struct Configuration { }
convenience init(_ configuration: Configuration) {
@@ -10,7 +10,7 @@ import Foundation
import MatrixRustSDK
import MatrixRustSDKMocks
extension LeaveSpaceHandleSDKMock {
nonisolated extension LeaveSpaceHandleSDKMock {
struct Configuration {
var rooms: [LeaveSpaceRoom] = .mockRooms
}
@@ -22,7 +22,7 @@ extension LeaveSpaceHandleSDKMock {
}
}
extension [LeaveSpaceRoom] {
nonisolated extension [LeaveSpaceRoom] {
static func mockRoomsWithSpace(spaceServiceRoom: SpaceServiceRoom, isLastOwner: Bool, areCreatorsPrivileged: Bool) -> [LeaveSpaceRoom] {
mockRooms + mockSingleSpace(spaceServiceRoom: spaceServiceRoom, isLastOwner: isLastOwner, areCreatorsPrivileged: areCreatorsPrivileged)
}
@@ -138,7 +138,7 @@ extension [LeaveSpaceRoom] {
}
}
private extension SpaceRoom {
private nonisolated extension SpaceRoom {
init(id: String,
canonicalAlias: String? = nil,
name: String,
@@ -27,7 +27,7 @@ extension SessionVerificationControllerProxyMock {
mock.acknowledgeVerificationRequestDetailsReturnValue = .success(())
mock.requestDeviceVerificationClosure = { [weak mock] in
Task.detached {
Task {
guard let mock else { return }
try await Task.sleep(for: requestDelay)
@@ -46,13 +46,13 @@ extension SessionVerificationControllerProxyMock {
}
mock.startSasVerificationClosure = { [weak mock] in
Task.detached {
Task {
guard let mock else { return }
try await Task.sleep(for: requestDelay)
mock.actions.send(.startedSasVerification)
Task.detached {
Task {
try await Task.sleep(for: requestDelay)
mock.actions.send(.receivedVerificationData(emojis))
}
@@ -62,7 +62,7 @@ extension SessionVerificationControllerProxyMock {
}
mock.approveVerificationClosure = { [weak mock] in
Task.detached {
Task {
guard let mock else { return }
try await Task.sleep(for: requestDelay)
@@ -73,7 +73,7 @@ extension SessionVerificationControllerProxyMock {
}
mock.declineVerificationClosure = { [weak mock] in
Task.detached {
Task {
guard let mock else { return }
try await Task.sleep(for: requestDelay)
@@ -84,7 +84,7 @@ extension SessionVerificationControllerProxyMock {
}
mock.cancelVerificationClosure = { [weak mock] in
Task.detached {
Task {
guard let mock else { return }
try await Task.sleep(for: requestDelay)
@@ -14,7 +14,7 @@ import MatrixRustSDK
/// A ``TimelineItemProviderMock`` subclass that optionally listens for ``UITestsSignalling`` signals
/// to drive pagination and incoming message simulation.
@MainActor
class SignallingTimelineItemProviderMock: TimelineItemProviderMock {
class SignallingTimelineItemProviderMock: TimelineItemProviderMock, @unchecked Sendable {
private let stateSubject: CurrentValueSubject<([TimelineItemProxy], TimelinePaginationState), Never>
/// An array of item arrays that will be prepended in order on each paginate signal.
@@ -25,6 +25,10 @@ class SignallingTimelineItemProviderMock: TimelineItemProviderMock {
private var client: UITestsSignalling.Client?
private var signalCancellable: AnyCancellable?
override nonisolated init() {
fatalError("Use init(itemProxies:paginationState:listenForSignals:) instead.")
}
init(itemProxies: [TimelineItemProxy] = [],
paginationState: TimelinePaginationState = .init(backward: .idle, forward: .endReached),
listenForSignals: Bool = false) {
@@ -10,7 +10,7 @@ import Combine
import Foundation
import MatrixRustSDK
extension SpaceRoomListProxyMock {
@MainActor extension SpaceRoomListProxyMock {
class Configuration {
var spaceServiceRoom: SpaceServiceRoom
var initialSpaceRooms: [SpaceServiceRoom]
@@ -22,7 +22,7 @@ struct TimelineControllerMockConfiguration {
var paginationState: TimelinePaginationState = .initial
}
extension TimelineControllerMock {
@MainActor extension TimelineControllerMock {
static var mediaGallery: TimelineControllerMock {
TimelineControllerMock(.init(timelineKind: .media(.mediaFilesScreen), timelineItems: (0..<5).reduce([]) { partialResult, _ in
partialResult + [TimelineFixtures.separator] + TimelineFixtures.mediaChunk
+1 -1
View File
@@ -13,7 +13,7 @@ struct UserSessionMockConfiguration {
var clientProxy: ClientProxyProtocol = ClientProxyMock(.init())
}
extension UserSessionMock {
@MainActor extension UserSessionMock {
convenience init(_ configuration: UserSessionMockConfiguration) {
self.init()
+2 -2
View File
@@ -29,11 +29,11 @@ enum Avatars {
}
/// Value in pixels by using the scale of the main screen
var scaledValue: CGFloat {
@MainActor var scaledValue: CGFloat {
value * UIScreen.main.scale
}
var scaledSize: CGSize {
@MainActor var scaledSize: CGSize {
CGSize(width: scaledValue, height: scaledValue)
}
}
+2 -2
View File
@@ -8,11 +8,11 @@
import Foundation
import MatrixRustSDK
enum CallIntent: String, Codable, CaseIterable {
nonisolated enum CallIntent: String, Codable, CaseIterable {
case video, audio
}
extension CallIntent {
nonisolated extension CallIntent {
// periphery:ignore - Unused, but added to detect new cases when updating the SDK.
init(rustCallIntent: MatrixRustSDK.RtcCallIntent) {
switch rustCallIntent {
@@ -12,7 +12,7 @@ import SwiftUI
/// on the same row (moving them both to a new row if necessary).
/// Each subview should be marked with the appropriate `ReactionLayoutItem` using the `reactionLayoutItem` modified
/// so the layout can appropriately treat each type of item.
struct CollapsibleReactionLayout: Layout {
nonisolated struct CollapsibleReactionLayout: Layout {
static let pointOffscreen = CGPoint(x: -10000, y: -10000)
/// The horizontal spacing between items
var itemSpacing: CGFloat = 0
@@ -250,33 +250,33 @@ struct CollapsibleReactionLayout: Layout {
}
}
struct ReactionSubviews {
nonisolated struct ReactionSubviews {
var reactions: [FlowLayoutSubview]
var collapseButton: FlowLayoutSubview
var addMoreButton: FlowLayoutSubview
}
/// A protocol representing subviews so that we can inject mocks in unit tests.
protocol FlowLayoutSubviews: RandomAccessCollection where Element: FlowLayoutSubview, Index == Int, SubSequence == Self { }
nonisolated protocol FlowLayoutSubviews: RandomAccessCollection where Element: FlowLayoutSubview, Index == Int, SubSequence == Self { }
extension LayoutSubviews: FlowLayoutSubviews { }
nonisolated extension LayoutSubviews: FlowLayoutSubviews { }
/// A protocol representing a subview so that we can inject mocks in unit tests.
protocol FlowLayoutSubview {
nonisolated protocol FlowLayoutSubview {
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
func place(at position: CGPoint, anchor: UnitPoint, proposal: ProposedViewSize)
subscript<K>(key: K.Type) -> K.Value where K: LayoutValueKey { get }
}
extension LayoutSubview: FlowLayoutSubview { }
nonisolated extension LayoutSubview: FlowLayoutSubview { }
enum ReactionLayoutItem {
nonisolated enum ReactionLayoutItem {
case reaction
case expandCollapse
case addMore
}
struct ReactionLayoutItemType: LayoutValueKey {
nonisolated struct ReactionLayoutItemType: LayoutValueKey {
static let defaultValue: ReactionLayoutItem = .reaction
}
@@ -12,7 +12,7 @@ import Combine
/// The purpose of this type is to remove the possibility to send new values on the underlying subject.
///
/// `CurrentValueSubject` is documented as thread-safe but is not formally `Sendable`, hence `@unchecked`.
struct CurrentValuePublisher<Output, Failure: Error>: Publisher, @unchecked Sendable {
nonisolated struct CurrentValuePublisher<Output, Failure: Error>: Publisher, @unchecked Sendable {
private let subject: CurrentValueSubject<Output, Failure>
init(_ subject: CurrentValueSubject<Output, Failure>) {
@@ -32,7 +32,7 @@ struct CurrentValuePublisher<Output, Failure: Error>: Publisher, @unchecked Send
}
}
extension CurrentValueSubject {
nonisolated extension CurrentValueSubject {
func asCurrentValuePublisher() -> CurrentValuePublisher<Output, Failure> {
.init(self)
}
@@ -9,7 +9,7 @@
import SceneKit
import SwiftUI
class EffectsScene: SCNScene {
nonisolated class EffectsScene: SCNScene {
static let colors: [Color] = [
.blue,
.red,
@@ -54,7 +54,7 @@ class EffectsScene: SCNScene {
}
}
private extension Color {
private nonisolated extension Color {
/// The color's components as an array of floats in the extended linear sRGB colorspace.
///
/// SceneKit works in a colorspace with a linear gamma, which is why this conversion is necessary.
@@ -11,13 +11,13 @@
import AVKit
import Foundation
enum AVMetadataBinaryValueError: Error {
nonisolated enum AVMetadataBinaryValueError: Error {
case unhandledQRSegmentMode(UInt8)
case bitError(BitError)
case unknown(Error)
}
extension AVMetadataMachineReadableCodeObject {
nonisolated extension AVMetadataMachineReadableCodeObject {
var qrBinaryValue: Data? {
get throws(AVMetadataBinaryValueError) {
guard type == .qr else { return nil }
@@ -66,11 +66,11 @@ enum BitError: Error {
case unhandledAlphanumericCharacter(UInt8)
}
private struct Bit {
private nonisolated struct Bit {
let value: UInt8
}
private extension [Bit] {
private nonisolated extension [Bit] {
mutating func takeBits(_ count: Int) throws(BitError) -> UInt8 {
guard count <= 8 else { throw .moreThan8BitsTaken }
@@ -163,7 +163,7 @@ private extension [Bit] {
}
}
private extension Data {
private nonisolated extension Data {
func bits() -> [Bit] {
var result = [Bit]()
forEach { (byte: UInt8) in
@@ -173,7 +173,7 @@ private extension Data {
}
}
private extension UInt8 {
private nonisolated extension UInt8 {
func bits() -> [Bit] {
var bits: [Bit] = []
for i in 0..<8 {
@@ -8,7 +8,7 @@
import Foundation
extension Array {
nonisolated extension Array {
func groupBy(_ isGroupable: (Element) -> Bool) -> [[Element]] {
var newItems = [[Element]]()
@@ -60,7 +60,7 @@ extension Array {
}
}
extension Array where Element == RoomTimelineItemProtocol {
nonisolated extension Array where Element == RoomTimelineItemProtocol {
func firstUsingStableID(_ id: TimelineItemIdentifier) -> Element? {
first { $0.id.uniqueID == id.uniqueID }
}
@@ -6,8 +6,9 @@
// Please see LICENSE files in the repository root for full details.
//
extension AsyncSequence {
nonisolated extension AsyncSequence {
func first() async rethrows -> Self.Element? {
try await first { _ in true }
var iterator = makeAsyncIterator()
return try await iterator.next(isolation: #isolation)
}
}
@@ -8,7 +8,7 @@
import Foundation
extension AttributedString {
nonisolated extension AttributedString {
/// faster than doing `String(characters)`: https://forums.swift.org/t/attributedstring-to-string/61667
var string: String {
String(characters[...])
@@ -9,7 +9,7 @@
import Foundation
import Synchronization
public extension Bundle {
public nonisolated extension Bundle {
/// The top-level bundle that contains the entire app.
static var app: Bundle {
var bundle = Bundle.main
@@ -8,7 +8,7 @@
import Foundation
extension CharacterSet {
nonisolated extension CharacterSet {
private static let urlAllowedSets: [CharacterSet] = [
.urlUserAllowed,
.urlPasswordAllowed,
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
extension ClientBuilder {
nonisolated extension ClientBuilder {
/// A helper method that applies the common builder modifiers needed for the app.
static func baseBuilder(setupEncryption: Bool = true,
httpProxy: String? = nil,
@@ -8,7 +8,7 @@
import Foundation
extension Collection {
nonisolated extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
+4 -4
View File
@@ -10,7 +10,7 @@ import Foundation
extension Date {
/// The date formatted with the minimal necessary units given how long ago it occurred.
func formattedMinimal() -> String {
nonisolated func formattedMinimal() -> String {
let calendar = Calendar.current
if calendar.isDateInToday(self) {
@@ -35,7 +35,7 @@ extension Date {
}
/// The date of an expiration formatted with the minimal necessary units given how long in the future it is.
func formattedExpiration() -> String {
nonisolated func formattedExpiration() -> String {
let calendar = Calendar.current
let now = Date.now
@@ -75,12 +75,12 @@ extension Date {
}
/// The date formatted as just the time, for use in timeline items specifically.
func formattedTime() -> String {
nonisolated func formattedTime() -> String {
formatted(date: .omitted, time: .shortened)
}
/// A fixed date used for mocks, previews etc.
static var mock: Date {
nonisolated static var mock: Date {
DateComponents(calendar: .current, year: 2007, month: 1, day: 9, hour: 9, minute: 41).date ?? .now
}
}
@@ -8,7 +8,7 @@
import Foundation
extension Dictionary {
nonisolated extension Dictionary {
var jsonString: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self,
options: [.fragmentsAllowed, .sortedKeys]) else {
@@ -12,7 +12,7 @@ enum FileManagerError: Error {
case invalidFileSize
}
extension FileManager {
nonisolated extension FileManager {
func directoryExists(at url: URL) -> Bool {
var isDirectory: ObjCBool = false
guard fileExists(atPath: url.path(percentEncoded: false), isDirectory: &isDirectory) else {
@@ -10,7 +10,6 @@ import Compound
import HTMLParser
import SwiftUI
@MainActor
extension HTMLParserStyle {
static let elementX = HTMLParserStyle(textColor: UIColor.label,
linkColor: UIColor.link,
@@ -9,7 +9,7 @@
import Foundation
import Kingfisher
extension ImageCache {
nonisolated extension ImageCache {
static var onlyInMemory: ImageCache {
let result = ImageCache.default
result.memoryStorage.config.keepWhenEnteringBackground = true
@@ -9,7 +9,7 @@
import Foundation
/// NSRegularExpressions work internally on NSStrings, we need to be careful how we build the ranges for extended grapheme clusters https://stackoverflow.com/a/27880748/730924
extension NSRegularExpression {
nonisolated extension NSRegularExpression {
func matches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] {
matches(in: string, options: options, range: .init(location: 0, length: (string as NSString).length))
}
@@ -14,26 +14,34 @@ extension Observable {
/// https://github.com/swiftlang/swift-evolution/blob/main/proposals/0475-observed.md
///
/// - Parameter property: The key path to the property you would like to observe.
func observe<Value>(_ property: KeyPath<Self, Value>) -> AsyncStream<Value> {
AsyncStream { continuation in
let isActive = Mutex(true)
@Sendable func observe() {
@MainActor func observe<Value: Sendable>(_ property: KeyPath<Self, Value>) -> AsyncStream<Value> {
let (stream, continuation) = AsyncStream<Value>.makeStream()
let (changeSignals, changeSignaller) = AsyncStream<Void>.makeStream()
// Read and yield the initial value synchronously at subscription time,
// otherwise observers miss changes made right after creating the stream.
let initialValue = withObservationTracking {
self[keyPath: property]
} onChange: {
changeSignaller.yield()
}
continuation.yield(initialValue)
let task = Task { @MainActor in
// The signal is sent on willSet, but as this task runs on the next main
// executor job the new value will have been set by the time we read it.
for await _ in changeSignals {
let value = withObservationTracking {
self[keyPath: property]
} onChange: {
// Handle the update on the next run loop as this is willSet not didSet.
DispatchQueue.main.async {
guard isActive.withLock({ $0 }) else { return }
observe()
}
changeSignaller.yield()
}
continuation.yield(value)
}
continuation.onTermination = { _ in isActive.withLock { $0 = false } }
observe()
}
continuation.onTermination = { _ in task.cancel() }
return stream
}
}
@@ -8,7 +8,7 @@
import Foundation
extension ProcessInfo {
nonisolated extension ProcessInfo {
/// Flag indicating whether the app is running the unit tests.
static var isRunningUnitTests: Bool {
#if DEBUG
@@ -8,7 +8,7 @@
import SwiftUI
extension ProposedViewSize: @retroactive Hashable {
nonisolated extension ProposedViewSize: @retroactive Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(width)
hasher.combine(height)
@@ -46,8 +46,8 @@ extension Published.Publisher {
var iterator = values.makeAsyncIterator()
// skips the publisher's current value
_ = await iterator.next()
return await iterator.next()
_ = await iterator.next(isolation: #isolation)
return await iterator.next(isolation: #isolation)
}
}
}
@@ -9,7 +9,7 @@
import SwiftUI
import UniformTypeIdentifiers
extension String {
nonisolated extension String {
/// Returns the string as an `AttributedString` with the specified character tinted in a different color.
/// - Parameters:
/// - character: The character to be tinted.
@@ -41,7 +41,7 @@ extension String {
}
}
extension String {
nonisolated extension String {
func ellipsize(length: Int) -> String {
guard count > length else {
return self
@@ -50,7 +50,7 @@ extension String {
}
}
extension String {
nonisolated extension String {
func replacingHtmlBreaksOccurrences() -> String {
var result = self
let pattern = #"</p>(\n+)<p>"#
@@ -74,14 +74,14 @@ extension String {
}
}
extension String {
nonisolated extension String {
/// detects if the string is empty or contains only whitespaces and newlines
var isBlank: Bool {
trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
extension String {
nonisolated extension String {
static func makeCanonicalAlias(aliasLocalPart: Self?, serverName: Self?) -> Self? {
guard let aliasLocalPart, !aliasLocalPart.isEmpty,
let serverName, !serverName.isEmpty else {
@@ -91,7 +91,7 @@ extension String {
}
}
extension String {
nonisolated extension String {
var validatedFileExtension: String {
let fileExtension = (self as NSString).pathExtension
guard !fileExtension.isEmpty else {
@@ -101,7 +101,7 @@ extension String {
}
}
extension String {
nonisolated extension String {
/// Whether the first character with a strong BiDi direction is right-to-left.
/// Mirrors the Unicode BiDi "first strong" rule used by TextKit to resolve
/// paragraph direction when `baseWritingDirection` is `.natural`.
@@ -120,7 +120,7 @@ extension String {
}
}
extension String {
nonisolated extension String {
/// To be used if the string is actually a URL
var asSanitizedLink: String {
var link = self
@@ -9,7 +9,7 @@
import Foundation
import UserNotifications
extension UNNotificationContent {
nonisolated extension UNNotificationContent {
@objc var receiverID: String? {
userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String
}
@@ -35,7 +35,7 @@ extension UNNotificationContent {
}
}
extension UNMutableNotificationContent {
nonisolated extension UNMutableNotificationContent {
override var receiverID: String? {
get {
userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String
+12 -12
View File
@@ -10,7 +10,7 @@ import Foundation
// MARK: - Custom URLs
extension URL {
nonisolated extension URL {
/// The URL of the primary app group container.
static var appGroupContainerDirectory: URL {
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: InfoPlistReader.main.appGroupIdentifier) else {
@@ -135,34 +135,34 @@ extension URL {
// MARK: Mocks
static var mockMXCAudio: URL {
nonisolated static var mockMXCAudio: URL {
"mxc://matrix.org/1234567890AuDiO"
}
static var mockMXCFile: URL {
nonisolated static var mockMXCFile: URL {
"mxc://matrix.org/1234567890FiLe"
}
static var mockMXCImage: URL {
nonisolated static var mockMXCImage: URL {
"mxc://matrix.org/1234567890ImAgE"
}
static var mockMXCVideo: URL {
nonisolated static var mockMXCVideo: URL {
"mxc://matrix.org/1234567890ViDeO"
}
static var mockMXCAvatar: URL {
nonisolated static var mockMXCAvatar: URL {
"mxc://matrix.org/1234567890AvAtAr"
}
static var mockMXCUserAvatar: URL {
nonisolated static var mockMXCUserAvatar: URL {
"mxc://matrix.org/1234567890AvAtArUsEr"
}
}
// MARK: - Helpers
extension URL: @retroactive ExpressibleByStringLiteral {
nonisolated extension URL: @retroactive ExpressibleByStringLiteral {
public init(stringLiteral value: StaticString) {
guard let url = URL(string: "\(value)") else {
fatalError("The static string used to create this URL is invalid")
@@ -177,7 +177,7 @@ extension URL: @retroactive ExpressibleByStringLiteral {
}
}
extension String {
nonisolated extension String {
/// Assumes that the string is a URL and sanitises it for use as the name of a directory.
func asURLDirectoryName() -> String {
replacingOccurrences(of: "https://", with: "")
@@ -188,7 +188,7 @@ extension String {
// MARK: - Phishing Confirmation URL
extension URL {
nonisolated extension URL {
static let confirmationScheme = "confirm"
var requiresConfirmation: Bool {
@@ -204,7 +204,7 @@ extension URL {
}
}
struct ConfirmURLParameters {
nonisolated struct ConfirmURLParameters {
static let internalURLKey = "internalURL"
static let displayStringKey = "displayString"
@@ -217,7 +217,7 @@ struct ConfirmURLParameters {
}
}
extension ConfirmURLParameters {
nonisolated extension ConfirmURLParameters {
init?(queryItems: [URLQueryItem]) {
guard let internalURLString = queryItems.first(where: { $0.name == Self.internalURLKey })?.value,
let internalURL = URL(string: internalURLString),
@@ -8,7 +8,7 @@
import Foundation
extension URLSession {
nonisolated extension URLSession {
/// The same as `data(for:delegate:)` but with an additional immediate retry if the first attempt fails.
func dataWithRetry(for request: URLRequest, delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) {
if let firstTryResult = try? await data(for: request, delegate: delegate) {
@@ -8,7 +8,7 @@
import UniformTypeIdentifiers
extension UTType {
nonisolated extension UTType {
/// Creates a type based on an optional mime type, falling back to a filename when this type is missing or unknown.
init?(mimeType: String?, fallbackFilename: String) {
guard let mimeType, let type = UTType(mimeType: mimeType) else {
@@ -13,7 +13,7 @@ import SwiftSoup
import Synchronization
import UIKit
protocol MentionBuilderProtocol {
nonisolated protocol MentionBuilderProtocol: Sendable {
func handleUserMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, userID: String, userDisplayName: String?)
func handleRoomIDMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomID: String)
func handleRoomAliasMention(for attributedString: NSMutableAttributedString, in range: NSRange, url: URL, roomAlias: String, roomDisplayName: String?)
@@ -22,7 +22,7 @@ protocol MentionBuilderProtocol {
func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange)
}
extension NSAttributedString.Key {
nonisolated extension NSAttributedString.Key {
static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name)
static let MatrixUserDisplayName: NSAttributedString.Key = .init(rawValue: UserDisplayNameAttribute.name)
@@ -36,7 +36,7 @@ extension NSAttributedString.Key {
static let InlineCode: NSAttributedString.Key = .init(rawValue: InlineCodeAttribute.name)
}
struct AttributedStringBuilder: AttributedStringBuilderProtocol {
nonisolated struct AttributedStringBuilder: AttributedStringBuilderProtocol {
private static let defaultKey = "default"
private let cacheKey: String
@@ -468,7 +468,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
}
}
private struct TextParsingMatch {
private nonisolated struct TextParsingMatch {
enum MatchType {
case userID(identifier: String)
case roomAlias(alias: String)
@@ -492,7 +492,7 @@ private struct TextParsingMatch {
}
}
private extension NSMutableAttributedString {
private nonisolated extension NSMutableAttributedString {
func setFontPreservingSymbolicTraits(_ newFont: UIFont) {
enumerateAttribute(.font, in: NSRange(location: 0, length: length)) { value, range, _ in
if let oldFont = value as? UIFont {
@@ -512,7 +512,7 @@ private extension NSMutableAttributedString {
}
}
private extension NSString {
private nonisolated extension NSString {
func hasSuffixCharacter(from characterSet: CharacterSet) -> Bool {
if length == 0 {
return false
@@ -8,7 +8,7 @@
import Foundation
struct AttributedStringBuilderComponent: Hashable, Identifiable {
nonisolated struct AttributedStringBuilderComponent: Hashable, Identifiable {
enum ComponentType {
case plainText
case blockquote
@@ -21,7 +21,7 @@ struct AttributedStringBuilderComponent: Hashable, Identifiable {
let type: ComponentType
}
protocol AttributedStringBuilderProtocol {
nonisolated protocol AttributedStringBuilderProtocol: Sendable {
func fromPlain(_ string: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) -> AttributedString?
@@ -74,7 +74,7 @@ enum InlineCodeAttribute: AttributedStringKey {
}
// periphery: ignore - required to make NSAttributedString to AttributedString conversion even if not used directly
extension AttributeScopes {
nonisolated extension AttributeScopes {
struct ElementXAttributes: AttributeScope {
let blockquote: BlockquoteAttribute
@@ -101,7 +101,7 @@ extension AttributeScopes {
}
// periphery: ignore - required to make NSAttributedString to AttributedString conversion even if not used directly
extension AttributeDynamicLookup {
nonisolated extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.ElementXAttributes, T>) -> T {
self[T.self]
}
@@ -6,7 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
enum HTMLFixtures: String, CaseIterable {
nonisolated enum HTMLFixtures: String, CaseIterable {
case plainText
case headers
case paragraphs
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
enum PhishingDetector {
nonisolated enum PhishingDetector {
static func isPhishingAttempt(displayString: String, internalURL: URL) -> Bool {
// Some phishing attempts can be hidden by using the unicode character "﹒" instead of "."
let disambiguatedDisplayString = displayString.replacingOccurrences(of: "﹒", with: ".")
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation
struct InfoPlistReader {
nonisolated struct InfoPlistReader {
private enum Keys {
static let appGroupIdentifier = "appGroupIdentifier"
static let baseBundleIdentifier = "baseBundleIdentifier"
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
enum ExtensionLogger {
nonisolated enum ExtensionLogger {
/// Memory formatter, uses exact 2 fraction digits and no grouping
private static var numberFormatter: NumberFormatter {
let formatter = NumberFormatter()
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
enum LogLevel: String, Codable, Hashable {
nonisolated enum LogLevel: String, Codable, Hashable {
case error, warn, info, debug, trace
var title: String {
+1 -1
View File
@@ -12,7 +12,7 @@ import MatrixRustSDK
/// Logging utility that provies multiple logging levels as well as file output and rolling.
/// Its purpose is to provide a common entry for customizing logging and should be used throughout the code.
enum MXLog {
nonisolated enum MXLog {
private nonisolated(unsafe) static var rootSpan: Span!
private nonisolated(unsafe) static var currentTarget: String!
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
enum TraceLogPack: Codable, CaseIterable {
nonisolated enum TraceLogPack: Codable, CaseIterable {
case eventCache, sendQueue, timeline, notificationClient, syncProfiling, latestEvents
var title: String {
@@ -24,7 +24,7 @@ enum TraceLogPack: Codable, CaseIterable {
}
}
extension TraceLogPack {
nonisolated extension TraceLogPack {
// periphery:ignore - Unused, but added to detect new cases when updating the SDK.
init(rustLogPack: MatrixRustSDK.TraceLogPacks) {
switch rustLogPack {
+1 -1
View File
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
enum Tracing {
nonisolated enum Tracing {
/// The directory that stores all of the log files.
static var logsDirectory: URL {
if ProcessInfo.isRunningIntegrationTests {
@@ -233,7 +233,10 @@ extension MapLibreMapView {
// Mirror every animated coordinate update to keep the camera as smooth as the marker.
followedAnnotationObservation = annotation.observe(\.coordinate) { [weak mapView] annotation, _ in
mapView?.setCenter(annotation.coordinate, animated: false)
// The coordinate is only ever updated by SwiftUI so KVO fires on the main actor.
MainActor.assumeIsolated {
mapView?.setCenter(annotation.coordinate, animated: false)
}
}
}
}
@@ -303,7 +306,7 @@ extension MapLibreMapView {
// is an option set and gestures can come combined with other reasons, so check for
// containment instead of an exact match.
let centerChangingGestures: MLNCameraChangeReason = [.gesturePan, .gesturePinch, .gestureRotate]
if !reason.intersection(centerChangingGestures).isEmpty {
if !reason.isDisjoint(with: centerChangingGestures) {
// Stop following immediately, the camera would fight the gesture otherwise.
stopFollowingMarker()
mapLibreView.userDidPan?()

Some files were not shown because too many files have changed in this diff Show More