diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index b27589f6279..b1bbc42e0ee 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -40,11 +40,6 @@ final class GatewayConnectionController { /// Reused instance — avoids creating CLLocationManager on the main thread /// each time currentPermissions() is called, which triggers a UI-thread warning. private let locationManager = CLLocationManager() - /// Cached off the main thread to avoid the UI-responsiveness warning from - /// calling SFSpeechRecognizer.authorizationStatus() on the main actor. - private nonisolated var speechRecognitionStatus: SFSpeechRecognizerAuthorizationStatus { - SFSpeechRecognizer.authorizationStatus() - } private weak var appModel: NodeAppModel? private var didAutoConnect = false private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:] @@ -236,15 +231,18 @@ final class GatewayConnectionController { guard let cfg = appModel.activeGatewayConnectConfig else { return } guard appModel.gatewayAutoReconnectEnabled else { return } - let refreshedConfig = GatewayConnectConfig( - url: cfg.url, - stableID: cfg.stableID, - tls: cfg.tls, - token: cfg.token, - bootstrapToken: cfg.bootstrapToken, - password: cfg.password, - nodeOptions: self.makeConnectOptions(stableID: cfg.stableID)) - appModel.applyGatewayConnectConfig(refreshedConfig) + Task { [weak self, weak appModel] in + guard let self, let appModel else { return } + let refreshedConfig = GatewayConnectConfig( + url: cfg.url, + stableID: cfg.stableID, + tls: cfg.tls, + token: cfg.token, + bootstrapToken: cfg.bootstrapToken, + password: cfg.password, + nodeOptions: await self.makeConnectOptions(stableID: cfg.stableID)) + appModel.applyGatewayConnectConfig(refreshedConfig) + } } func clearPendingTrustPrompt() { @@ -470,10 +468,10 @@ final class GatewayConnectionController { password: String?) { guard let appModel else { return } - let connectOptions = self.makeConnectOptions(stableID: gatewayStableID) - Task { [weak appModel] in - guard let appModel else { return } + Task { [weak self, weak appModel] in + guard let self, let appModel else { return } + let connectOptions = await self.makeConnectOptions(stableID: gatewayStableID) await MainActor.run { appModel.gatewayStatusText = "Connecting…" } @@ -749,7 +747,7 @@ final class GatewayConnectionController { "manual|\(host.lowercased())|\(port)" } - private func makeConnectOptions(stableID: String?) -> GatewayConnectOptions { + private func makeConnectOptions(stableID: String?) async -> GatewayConnectOptions { let defaults = UserDefaults.standard let displayName = self.resolvedDisplayName(defaults: defaults) let resolvedClientId = self.resolvedClientId(defaults: defaults, stableID: stableID) @@ -759,7 +757,7 @@ final class GatewayConnectionController { scopes: [], caps: self.currentCaps(), commands: self.currentCommands(), - permissions: self.currentPermissions(), + permissions: await self.currentPermissions(), clientId: resolvedClientId, clientMode: "node", clientDisplayName: displayName) @@ -895,11 +893,13 @@ final class GatewayConnectionController { return commands } - private func currentPermissions() -> [String: Bool] { + private func currentPermissions() async -> [String: Bool] { + let speechRecognitionStatus = await Self.currentSpeechRecognitionStatus() + var permissions: [String: Bool] = [:] permissions["camera"] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized permissions["microphone"] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized - permissions["speechRecognition"] = self.speechRecognitionStatus == .authorized + permissions["speechRecognition"] = speechRecognitionStatus == .authorized permissions["location"] = Self.isLocationAuthorized( status: self.locationManager.authorizationStatus) && CLLocationManager.locationServicesEnabled() @@ -929,6 +929,14 @@ final class GatewayConnectionController { return permissions } + private nonisolated static func currentSpeechRecognitionStatus() async + -> SFSpeechRecognizerAuthorizationStatus + { + await Task.detached(priority: .utility) { + SFSpeechRecognizer.authorizationStatus() + }.value + } + private static func isLocationAuthorized(status: CLAuthorizationStatus) -> Bool { switch status { case .authorizedAlways, .authorizedWhenInUse: @@ -961,8 +969,8 @@ extension GatewayConnectionController { self.currentCommands() } - func _test_currentPermissions() -> [String: Bool] { - self.currentPermissions() + func _test_currentPermissions() async -> [String: Bool] { + await self.currentPermissions() } func _test_platformString() -> String {