From 833b4b410b4c2433dc12aa2e1d7f2fe49c4e80d5 Mon Sep 17 00:00:00 2001 From: Carlos Nogueira Date: Thu, 22 May 2025 14:17:54 +0100 Subject: [PATCH] RUM-9768: make shadow view retrieval on RCTTextViewRecorder async and control access with semaphore --- .../ios/Sources/RCTTextViewRecorder.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift index ef626f121..df283415d 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift +++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift @@ -78,8 +78,23 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder { var shadowView: RCTTextShadowView? = nil let tag = textView.reactTag - RCTGetUIManagerQueue().sync { - shadowView = uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView + let timeout: TimeInterval = 0.2 + let semaphore = DispatchSemaphore(value: 0) + + // We need to access the shadow view from the UIManager queue, but we're currently on the main thread. + // Calling `.sync` from the main thread to the UIManager queue is unsafe, because the UIManager queue + // may already be executing a layout operation that in turn requires the main thread (e.g. measuring a native view). + // That would create a circular dependency and deadlock the app. + // To avoid this, we dispatch the work asynchronously to the UIManager queue and wait with a timeout. + // This ensures we block only if absolutely necessary, and can fail gracefully if the queue is busy. + RCTGetUIManagerQueue().async { + shadowView = self.uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView + semaphore.signal() + } + + let waitResult = semaphore.wait(timeout: .now() + timeout) + if waitResult == .timedOut { + return nil } guard let shadow = shadowView else {