From e4a99a6f0641007079598f6a1bb3b6e5b4c089f9 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Fri, 17 Jul 2026 15:17:20 +0200 Subject: [PATCH] fix(ios): Restore ObjC SentrySwizzle for RNSScreen to link against xcframework The Swift SPI enum case SentryInternalSwizzleApi.Mode.oncePerClass isn't exported as a linkable symbol from sentry-cocoa's prebuilt static xcframework, so consumers on the default SENTRY_USE_XCFRAMEWORK path fail to link. Route the RNSScreen viewDidAppear: swizzle back through the ObjC SentrySwizzle API, whose class symbols are exported in the xcframework slice. Fixes #6465. --- CHANGELOG.md | 6 ++++ packages/core/ios/RNSentryInternal.swift | 46 ------------------------ packages/core/ios/RNSentryRNSScreen.m | 36 ++++++++++++++----- 3 files changed, 34 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e708fddb..06ee39c254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Fix iOS link failure `Undefined symbols: SentryInternalSwizzleApi.Mode.oncePerClass` when consuming the default prebuilt `Sentry.xcframework` ([#6465](https://github.com/getsentry/sentry-react-native/issues/6465)) + ## 8.19.0 ### Features diff --git a/packages/core/ios/RNSentryInternal.swift b/packages/core/ios/RNSentryInternal.swift index 94859b44d6..3bf4032bde 100644 --- a/packages/core/ios/RNSentryInternal.swift +++ b/packages/core/ios/RNSentryInternal.swift @@ -182,52 +182,6 @@ import Foundation ) {} #endif - // MARK: - Swizzle - - // `SentryInternalSwizzleApi` in sentry-cocoa has no platform gating. - // We enable the wrapper on every UIKit-capable platform (iOS/tvOS/visionOS - // — matching `SENTRY_UIKIT_AVAILABLE`, which is what `SENTRY_HAS_UIKIT` - // resolves to in the RNSentry.mm caller). `os(iOS)` in Swift already - // covers Mac Catalyst, so no separate `targetEnvironment(macCatalyst)` is - // needed. - #if os(iOS) || os(tvOS) || os(visionOS) - /// Stable identity for the `RNSScreen.viewDidAppear:` swizzle so the underlying - /// `oncePerClass` bookkeeping in sentry-cocoa can dedupe re-entries. - private static var rnsScreenViewDidAppearKey: UInt8 = 0 - - /// Swizzles `-[RNSScreen viewDidAppear:]`, invoking `hook` before the original - /// implementation on every call. No-op when RNSScreen is unavailable. - @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) { - guard let cls = NSClassFromString("RNSScreen") else { return } - let selector = NSSelectorFromString("viewDidAppear:") - - // `withUnsafePointer(to:)` scopes the pointer's validity to the closure - // body. Perform the entire swizzle call inside so we never rely on the - // pointer surviving beyond the closure. The backing storage is a - // `static var`, so the address itself stays stable across calls — - // sentry-cocoa's `oncePerClass` bookkeeping continues to dedupe. - withUnsafePointer(to: &rnsScreenViewDidAppearKey) { keyPtr in - _ = SentrySDK.internal.swizzle.instanceMethod( - selector, - in: cls, - mode: .oncePerClass, - key: UnsafeRawPointer(keyPtr), - factory: { getOriginal in - let block: @convention(block) (AnyObject, ObjCBool) -> Void = { receiver, animated in - hook() - typealias OriginalIMP = @convention(c) (AnyObject, Selector, ObjCBool) -> Void - let original = unsafeBitCast(getOriginal(), to: OriginalIMP.self) - original(receiver, selector, animated) - } - return block as AnyObject - } - ) - } - } - #else - @_spi(Private) @objc public static func swizzleRNSScreenViewDidAppear(hook: @escaping () -> Void) {} - #endif - // MARK: - Profiling #if !(os(watchOS) || os(tvOS) || os(visionOS)) diff --git a/packages/core/ios/RNSentryRNSScreen.m b/packages/core/ios/RNSentryRNSScreen.m index 52c3658fba..2fc386cd03 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,21 +2,41 @@ #if SENTRY_HAS_UIKIT -# if __has_include() -# import -# else -# import "RNSentry-Swift.h" -# endif # import "RNSentryDependencyContainer.h" # import "RNSentryFramesTrackerListener.h" +// `SentrySwizzle.h` is a private header of `Sentry.framework`. We deliberately +// keep using the ObjC swizzle API here instead of routing through +// `SentrySDK.internal.swizzle` (the Swift `@_spi(Private)` equivalent): +// the Swift SPI enum case `SentryInternalSwizzleApi.Mode.oncePerClass` is not +// exported as a linkable symbol from sentry-cocoa's prebuilt static +// xcframework, so consumers on the default (`SENTRY_USE_XCFRAMEWORK` unset) +// path fail to link with `Undefined symbols: enum case for +// SentryInternalSwizzleApi.Mode.oncePerClass`. The ObjC `SentrySwizzle` +// class + `SentrySwizzleMode` enum have proper external symbols in the +// xcframework slice, so this path links cleanly for both the xcframework +// and source-built (`SENTRY_USE_XCFRAMEWORK=0`) configurations. See #6465. +# if __has_include() +# import +# else +# import "SentrySwizzle.h" +# endif @implementation RNSentryRNSScreen + (void)swizzleViewDidAppear { - [RNSentryInternal swizzleRNSScreenViewDidAppearWithHook:^{ - [[[RNSentryDependencyContainer sharedInstance] framesTrackerListener] startListening]; - }]; + Class rnsscreenclass = NSClassFromString(@"RNSScreen"); + if (rnsscreenclass == nil) { + return; + } + + SEL selector = NSSelectorFromString(@"viewDidAppear:"); + SentrySwizzleInstanceMethod(rnsscreenclass, selector, SentrySWReturnType(void), + SentrySWArguments(BOOL animated), SentrySWReplacement({ + [[[RNSentryDependencyContainer sharedInstance] framesTrackerListener] startListening]; + SentrySWCallOriginal(animated); + }), + SentrySwizzleModeOncePerClass, (void *)selector); } @end