From b2e11c504bbfa9b5d3efb0220674d9bb93facce4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 20 Dec 2025 01:48:14 +0100 Subject: [PATCH] fix: tighten iOS main-actor handling --- apps/ios/Sources/Screen/ScreenController.swift | 15 +++++---------- apps/ios/Sources/Screen/ScreenRecordService.swift | 9 ++++++++- apps/ios/Sources/Voice/VoiceWakeManager.swift | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index ef2da373b..5dbe696d0 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -294,13 +294,14 @@ extension Double { // MARK: - Navigation Delegate /// Handles navigation policy to intercept clawdis:// deep links from canvas +@MainActor private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { weak var controller: ScreenController? func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, - decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) + decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) @@ -310,9 +311,7 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { // Intercept clawdis:// deep links if url.scheme == "clawdis" { decisionHandler(.cancel) - Task { @MainActor in - self.controller?.onDeepLink?(url) - } + self.controller?.onDeepLink?(url) return } @@ -324,15 +323,11 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { didFailProvisionalNavigation _: WKNavigation?, withError error: any Error) { - Task { @MainActor in - self.controller?.errorText = error.localizedDescription - } + self.controller?.errorText = error.localizedDescription } func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { - Task { @MainActor in - self.controller?.errorText = error.localizedDescription - } + self.controller?.errorText = error.localizedDescription } } diff --git a/apps/ios/Sources/Screen/ScreenRecordService.swift b/apps/ios/Sources/Screen/ScreenRecordService.swift index 55be26aae..ad9fc0309 100644 --- a/apps/ios/Sources/Screen/ScreenRecordService.swift +++ b/apps/ios/Sources/Screen/ScreenRecordService.swift @@ -3,6 +3,10 @@ import ReplayKit @MainActor final class ScreenRecordService { + private struct UncheckedSendableBox: @unchecked Sendable { + let value: T + } + enum ScreenRecordError: LocalizedError { case invalidScreenIndex(Int) case captureFailed(String) @@ -20,6 +24,7 @@ final class ScreenRecordService { } } + // swiftlint:disable:next cyclomatic_complexity func record( screenIndex: Int?, durationMs: Int?, @@ -165,8 +170,10 @@ final class ScreenRecordService { videoInput.markAsFinished() audioInput?.markAsFinished() + let writerBox = UncheckedSendableBox(value: writer) try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in - writer.finishWriting { + writerBox.value.finishWriting { + let writer = writerBox.value if let err = writer.error { cont.resume(throwing: ScreenRecordError.writeFailed(err.localizedDescription)) } else if writer.status != .completed { diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index 7807d1ea6..889263aa8 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -94,7 +94,7 @@ final class VoiceWakeManager: NSObject { private var lastDispatched: String? private var onCommand: (@Sendable (String) async -> Void)? - private nonisolated(unsafe) var userDefaultsObserver: NSObjectProtocol? + private var userDefaultsObserver: NSObjectProtocol? override init() { super.init() @@ -110,7 +110,7 @@ final class VoiceWakeManager: NSObject { }) } - deinit { + @MainActor deinit { if let userDefaultsObserver = self.userDefaultsObserver { NotificationCenter.default.removeObserver(userDefaultsObserver) }