From af7eae7fd1e4db5879bb448a35bbaffc69c47eda Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 13 Oct 2026 10:38:51 -0700 Subject: [PATCH 1/4] fix: support Swift builds that use an Objective-C bridging header Hosts can compile FURN's Swift sources against React Native through an Objective-C bridging header instead of a `React` clang module. - Import `React` in the Avatar and NativeDatePicker iOS Swift sources only when the module is available. - Implement the macOS Paper Callout `focusWindow` and `blurWindow` commands in Swift and declare them with `RCT_EXTERN_METHOD`, so FRNCalloutManager.m no longer imports the generated FRNNativeCore-Swift.h. With a bridging header, that generated header also declares the internal RCTCalloutManager class, which duplicates the `RCT_EXTERN_MODULE` interface. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea215348-486c-45ef-8205-2a81a1dd75a7 --- .changeset/optional-react-import.md | 6 +++++ .changeset/swift-callout-commands.md | 5 ++++ packages/agentic/native-core/README.md | 6 ++++- .../Callout/paper/CalloutManager.swift | 15 ++++++++++++ .../Callout/paper/FRNCalloutManager.m | 24 ++----------------- .../tests/native-ownership.test.mjs | 16 +++++++++---- .../Avatar/ios/AvatarViewManager.swift | 2 ++ .../ios/DatePickerDelegate.swift | 2 ++ .../ios/DatePickerManager.swift | 2 ++ 9 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 .changeset/optional-react-import.md create mode 100644 .changeset/swift-callout-commands.md diff --git a/.changeset/optional-react-import.md b/.changeset/optional-react-import.md new file mode 100644 index 00000000000..120c65b601c --- /dev/null +++ b/.changeset/optional-react-import.md @@ -0,0 +1,6 @@ +--- +'@fluentui-react-native/experimental-avatar': patch +'@fluentui-react-native/experimental-native-date-picker': patch +--- + +Import `React` in iOS Swift sources only when the module is available, so hosts can provide React Native headers through an Objective-C bridging header. diff --git a/.changeset/swift-callout-commands.md b/.changeset/swift-callout-commands.md new file mode 100644 index 00000000000..f603673b611 --- /dev/null +++ b/.changeset/swift-callout-commands.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/native-core': patch +--- + +Implement the macOS Paper Callout `focusWindow` and `blurWindow` commands in Swift so the Objective-C manager no longer imports the generated Swift header, which duplicates the manager's interface when hosts compile Swift with an Objective-C bridging header. diff --git a/packages/agentic/native-core/README.md b/packages/agentic/native-core/README.md index 94f76086e5f..bd101489108 100644 --- a/packages/agentic/native-core/README.md +++ b/packages/agentic/native-core/README.md @@ -40,7 +40,11 @@ to use its host-provided `RCTCallout` and `RCTFocusZone`. The macOS module-named header at `macos/shared/FRNNativeCore.h` is required by the generated `FRNNativeCore-Swift.h`; keep it aligned with the pod's -`module_name` when changing native ownership. +`module_name` when changing native ownership. Only Fabric adapters import the +generated header. Hosts that compile the Swift sources with an Objective-C +bridging header also get internal Swift classes declared there, duplicating +Paper `RCT_EXTERN_MODULE` interfaces, so implement Paper manager methods in +Swift and declare them with `RCT_EXTERN_METHOD`. ## Public entrypoints diff --git a/packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift b/packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift index 89350cd109e..739986c194c 100644 --- a/packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift +++ b/packages/agentic/native-core/macos/components/Callout/paper/CalloutManager.swift @@ -13,4 +13,19 @@ class CalloutManager: RCTViewManager { override class func requiresMainQueueSetup() -> Bool { return true } + + // React Native calls these on the module's method queue; views can only be looked up on the main queue. + @objc func focusWindow(_ viewTag: NSNumber) { + nonisolated(unsafe) let manager = self + DispatchQueue.main.async { + (manager.bridge?.uiManager?.view(forReactTag: viewTag) as? CalloutView)?.focusWindow() + } + } + + @objc func blurWindow(_ viewTag: NSNumber) { + nonisolated(unsafe) let manager = self + DispatchQueue.main.async { + (manager.bridge?.uiManager?.view(forReactTag: viewTag) as? CalloutView)?.blurWindow() + } + } } diff --git a/packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m b/packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m index f162f6cc3b3..49d3ae6dae3 100644 --- a/packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m +++ b/packages/agentic/native-core/macos/components/Callout/paper/FRNCalloutManager.m @@ -1,7 +1,5 @@ #import "FRNCalloutManager.h" -#import "FRNNativeCore-Swift.h" - @implementation RCTConvert (FRNCalloutAdditions) // RCTConvert does not properly convert a JS screenRect into a native CGRect/NSRect, @@ -39,27 +37,9 @@ + (NSRect)screenRect:(id)json @interface RCT_EXTERN_MODULE(RCTCalloutManager, RCTViewManager) -RCT_EXPORT_METHOD(focusWindow : (nonnull NSNumber *)viewTag) -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSView *view = [self.bridge.uiManager viewForReactTag:viewTag]; - - if ([view isKindOfClass:[FRNCalloutView class]]) { - [(FRNCalloutView *)view focusWindow]; - } - }); -} +RCT_EXTERN_METHOD(focusWindow : (nonnull NSNumber *)viewTag) -RCT_EXPORT_METHOD(blurWindow : (nonnull NSNumber *)viewTag) -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSView *view = [self.bridge.uiManager viewForReactTag:viewTag]; - - if ([view isKindOfClass:[FRNCalloutView class]]) { - [(FRNCalloutView *)view blurWindow]; - } - }); -} +RCT_EXTERN_METHOD(blurWindow : (nonnull NSNumber *)viewTag) RCT_EXPORT_VIEW_PROPERTY(target, NSNumber) diff --git a/packages/agentic/native-core/tests/native-ownership.test.mjs b/packages/agentic/native-core/tests/native-ownership.test.mjs index ceb5ab99cea..38bdf30322d 100644 --- a/packages/agentic/native-core/tests/native-ownership.test.mjs +++ b/packages/agentic/native-core/tests/native-ownership.test.mjs @@ -53,10 +53,18 @@ test('macOS Swift interoperability uses the consolidated pod module name', async assert.equal(moduleName, 'FRNNativeCore'); await readFile(new URL(`macos/shared/${moduleName}.h`, packageRoot), 'utf8'); - for (const adapter of ['fabric/RCTCalloutComponentView.mm', 'paper/FRNCalloutManager.m']) { - const source = await readFile(new URL(`macos/components/Callout/${adapter}`, packageRoot), 'utf8'); - assert.ok(source.includes(`#import "${moduleName}-Swift.h"`)); - assert.doesNotMatch(source, /FRNCallout-Swift\.h/); + const fabric = await readFile(new URL('macos/components/Callout/fabric/RCTCalloutComponentView.mm', packageRoot), 'utf8'); + assert.ok(fabric.includes(`#import "${moduleName}-Swift.h"`)); + assert.doesNotMatch(fabric, /FRNCallout-Swift\.h/); + + // Bridging-header builds also declare internal Swift classes in the generated header, duplicating the Paper + // manager's RCT_EXTERN_MODULE interface, so Paper commands are implemented in Swift instead. + const paper = await readFile(new URL('macos/components/Callout/paper/FRNCalloutManager.m', packageRoot), 'utf8'); + const manager = await readFile(new URL('macos/components/Callout/paper/CalloutManager.swift', packageRoot), 'utf8'); + assert.doesNotMatch(paper, /-Swift\.h/); + for (const command of ['focusWindow', 'blurWindow']) { + assert.match(paper, new RegExp(`RCT_EXTERN_METHOD\\(${command} : \\(nonnull NSNumber \\*\\)viewTag\\)`)); + assert.match(manager, new RegExp(`@objc func ${command}\\(_ viewTag: NSNumber\\)`)); } }); diff --git a/packages/experimental/Avatar/ios/AvatarViewManager.swift b/packages/experimental/Avatar/ios/AvatarViewManager.swift index 0221f0784e9..ef862e8278f 100644 --- a/packages/experimental/Avatar/ios/AvatarViewManager.swift +++ b/packages/experimental/Avatar/ios/AvatarViewManager.swift @@ -1,6 +1,8 @@ import Foundation import FluentUI +#if canImport(React) import React +#endif import UIKit @objc(FRNAvatarViewManager) diff --git a/packages/experimental/NativeDatePicker/ios/DatePickerDelegate.swift b/packages/experimental/NativeDatePicker/ios/DatePickerDelegate.swift index 65336e737f4..d48f74b1659 100644 --- a/packages/experimental/NativeDatePicker/ios/DatePickerDelegate.swift +++ b/packages/experimental/NativeDatePicker/ios/DatePickerDelegate.swift @@ -1,6 +1,8 @@ import FluentUI import Foundation +#if canImport(React) import React +#endif class DatePickerDelegate: DateTimePickerDelegate { weak var manager: DatePickerManager? diff --git a/packages/experimental/NativeDatePicker/ios/DatePickerManager.swift b/packages/experimental/NativeDatePicker/ios/DatePickerManager.swift index 3a5efb03a9f..8e503fbf853 100644 --- a/packages/experimental/NativeDatePicker/ios/DatePickerManager.swift +++ b/packages/experimental/NativeDatePicker/ios/DatePickerManager.swift @@ -1,6 +1,8 @@ import FluentUI import Foundation +#if canImport(React) import React +#endif @objc(FRNDatePickerManager) public class DatePickerManager: NSObject { From ce644ad0b01b73d52a57bc5ac1e17f5d553cf478 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 13 Oct 2026 13:04:59 -0700 Subject: [PATCH 2/4] fix: import React Native headers with the form in iOS sources FRNAppearanceAdditions.m and FRNDatePickerManager.m included RCTUIManager.h and RCTBridgeModule.h by bare name, which only resolves in hosts that also expose React Native headers without the React/ prefix. Use the canonical form, which the rest of these files and CocoaPods already use. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea215348-486c-45ef-8205-2a81a1dd75a7 --- .changeset/qualified-react-headers.md | 6 ++++++ .../AppearanceAdditions/ios/FRNAppearanceAdditions.m | 2 +- .../NativeDatePicker/ios/FRNDatePickerManager.m | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/qualified-react-headers.md diff --git a/.changeset/qualified-react-headers.md b/.changeset/qualified-react-headers.md new file mode 100644 index 00000000000..6dbd7d3ce8a --- /dev/null +++ b/.changeset/qualified-react-headers.md @@ -0,0 +1,6 @@ +--- +'@fluentui-react-native/experimental-appearance-additions': patch +'@fluentui-react-native/experimental-native-date-picker': patch +--- + +Import React Native headers in iOS Objective-C sources with the canonical `` form instead of by bare header name. diff --git a/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m b/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m index 9adafc69d31..84f3aedc97e 100644 --- a/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m +++ b/packages/experimental/AppearanceAdditions/ios/FRNAppearanceAdditions.m @@ -1,5 +1,5 @@ #import "FRNAppearanceAdditions.h" -#import "RCTUIManager.h" +#import #import #import diff --git a/packages/experimental/NativeDatePicker/ios/FRNDatePickerManager.m b/packages/experimental/NativeDatePicker/ios/FRNDatePickerManager.m index 77d8ad3d361..a638f1da420 100644 --- a/packages/experimental/NativeDatePicker/ios/FRNDatePickerManager.m +++ b/packages/experimental/NativeDatePicker/ios/FRNDatePickerManager.m @@ -1,4 +1,4 @@ -#import "RCTBridgeModule.h" +#import #import #import #import From 2bc8e206a0543f2ea11e2aef255a9ae53c5ee8f0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 13 Oct 2026 13:19:10 -0700 Subject: [PATCH 3/4] fix(native-core): import RCTI18nUtil.h with the form in FocusZone The macOS FocusZone implementation included "RCTi18nUtil.h" by bare name and with different capitalization from React Native's React/Modules/RCTI18nUtil.h, so it only resolved through a case-insensitive bare-name header map. Use the canonical form. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea215348-486c-45ef-8205-2a81a1dd75a7 --- .changeset/focus-zone-i18n-header.md | 5 +++++ .../macos/components/FocusZone/shared/RCTFocusZone.m | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/focus-zone-i18n-header.md diff --git a/.changeset/focus-zone-i18n-header.md b/.changeset/focus-zone-i18n-header.md new file mode 100644 index 00000000000..f7e9046c0c9 --- /dev/null +++ b/.changeset/focus-zone-i18n-header.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/native-core': patch +--- + +Import `RCTI18nUtil.h` in the macOS FocusZone implementation with the canonical `` form instead of by bare, differently cased header name. diff --git a/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m b/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m index e3582553848..5ca071c95aa 100644 --- a/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m +++ b/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m @@ -1,7 +1,7 @@ #import "KeyCodes.h" #import #import "RCTFocusZone.h" -#import "RCTi18nUtil.h" +#import typedef enum { FocusZoneActionNone, From d6d11c3efb765e8a4227578c806527fd236e30d0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 13 Oct 2026 21:15:04 -0700 Subject: [PATCH 4/4] fix(native-core): silence unused block parameter warnings in macOS FocusZone Mark the unused `index` and `stop` parameters of the indexOfObjectPassingTest: block in RCTFocusZone.m as `__unused` so the file compiles cleanly when -Wunused-parameter is treated as an error. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d7aea072-fc8e-4a8d-890a-81311025130d --- .changeset/focus-zone-unused-params.md | 5 +++++ .../macos/components/FocusZone/shared/RCTFocusZone.m | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/focus-zone-unused-params.md diff --git a/.changeset/focus-zone-unused-params.md b/.changeset/focus-zone-unused-params.md new file mode 100644 index 00000000000..984783db307 --- /dev/null +++ b/.changeset/focus-zone-unused-params.md @@ -0,0 +1,5 @@ +--- +'@fluentui-react-native/native-core': patch +--- + +Mark the unused block parameters in the macOS FocusZone traversal as `__unused` so the file compiles cleanly with `-Wunused-parameter` treated as an error. diff --git a/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m b/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m index 5ca071c95aa..28100c9f17b 100644 --- a/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m +++ b/packages/agentic/native-core/macos/components/FocusZone/shared/RCTFocusZone.m @@ -560,7 +560,7 @@ - (NSView *)nextViewToFocusInHierarchyForAction:(FocusZoneAction)action AppendFocusableViewsWithin(self, focusableViews); NSView *firstResponder = GetFirstResponder([self window]); - NSUInteger currentIndex = [focusableViews indexOfObjectPassingTest:^BOOL(NSView *candidateView, NSUInteger index, BOOL *stop) { + NSUInteger currentIndex = [focusableViews indexOfObjectPassingTest:^BOOL(NSView *candidateView, __unused NSUInteger index, __unused BOOL *stop) { return candidateView == firstResponder || [candidateView isDescendantOf:firstResponder] || [firstResponder isDescendantOf:candidateView];