From 821094955046f530c4200bf8806d8eba80d05d81 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Sat, 7 Feb 2026 05:51:10 -0800 Subject: [PATCH 1/2] Add basic support for TM in Swift (#52710) Summary: This PR adds basic support to the TM infra to run Turbomodules written directly in Swift. ## Changelog: [iOS][Added] - Add basic support for Swift modules Pull Request resolved: https://github.com/facebook/react-native/pull/52710 Test Plan: Tested locally in Hello World https://github.com/user-attachments/assets/9f5f2a06-0d3d-40b7-b555-94d28242300c Differential Revision: D92527173 Pulled By: cipolleschi --- .../modules/GenerateModuleObjCpp/index.js | 77 ++++++ .../core/platform/ios/ReactCommon/RCTModule.h | 11 + .../ios/ReactCommon/RCTSwiftTurboModule.h | 13 + .../platform/ios/ReactCommon/RCTTurboModule.h | 14 +- .../ios/ReactCommon/RCTTurboModule.mm | 3 +- .../ios/ReactCommon/RCTTurboModuleManager.mm | 62 +++-- .../__docs__/Swift-TurboModules.md | 250 ++++++++++++++++++ .../generate-artifacts-executor/index.js | 42 ++- ...eactAppDependencyProvider.podspec.template | 3 +- 9 files changed, 450 insertions(+), 25 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h create mode 100644 packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h create mode 100644 packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/__docs__/Swift-TurboModules.md diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js index 8d93d9ad29c210..0a6e285c02182e 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/index.js @@ -22,6 +22,20 @@ const {StructCollector} = require('./StructCollector'); type FilesOutput = Map; +const SwiftCompatibleDeclarationTemplate = ({ + hasteModuleName, + protocolMethods, +}: $ReadOnly<{ + hasteModuleName: string, + protocolMethods: string, +}>) => ` +@protocol ${hasteModuleName}Spec + +${protocolMethods} + +@end +` + const ModuleDeclarationTemplate = ({ hasteModuleName, structDeclarations, @@ -58,6 +72,51 @@ namespace facebook::react { }; } // namespace facebook::react`; +const SwiftCompatibleHeaderFileTemplate = ({ + headerFileName, + moduleDeclarations, + assumeNonnull, +}: $ReadOnly<{ + headerFileName: string, + moduleDeclarations: string, + assumeNonnull: boolean, +}>) => { + const headerFileNameWithNoExt = headerFileName + .replace(/\.h$/, '') + .replace(/-/, ''); + + return ( + `/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * ${'@'}generated by codegen project: GenerateModuleObjCpp + * + * We create an umbrella header (and corresponding implementation) here since + * Cxx compilation in BUCK has a limitation: source-code producing genrule()s + * must have a single output. More files => more genrule()s => slower builds. + */ + +// Avoid multiple includes of ${headerFileNameWithNoExt} symbols +#ifndef ${headerFileNameWithNoExt}_H +#define ${headerFileNameWithNoExt}_H + +#import +#import +#import + +` + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_BEGIN\n' : '') + + moduleDeclarations + + '\n' + + (assumeNonnull ? '\nNS_ASSUME_NONNULL_END\n' : '\n') + + `#endif // ${headerFileNameWithNoExt}_H` + + '\n' + ); +}; + const HeaderFileTemplate = ({ headerFileName, moduleDeclarations, @@ -150,6 +209,7 @@ module.exports = { const nativeModules = getModules(schema); const moduleDeclarations: Array = []; + const swiftCompatibleModulesDeclarations: Array = []; const structInlineMethods: Array = []; const moduleImplementations: Array = []; @@ -209,6 +269,15 @@ module.exports = { }), ); + swiftCompatibleModulesDeclarations.push( + SwiftCompatibleDeclarationTemplate({ + hasteModuleName: hasteModuleName, + protocolMethods: methodSerializations + .map(({protocolMethod}) => protocolMethod) + .join('\n'), + }), + ); + structInlineMethods.push(methodStrs.join('\n')); moduleImplementations.push( @@ -232,6 +301,13 @@ module.exports = { assumeNonnull, }); + const swiftCompatibleHeaderFileName = `${libraryName}-Swift.h`; + const swiftHeaderFile = SwiftCompatibleHeaderFileTemplate({ + headerFileName: swiftCompatibleHeaderFileName, + moduleDeclarations: swiftCompatibleModulesDeclarations.join('\n'), + assumeNonnull, + }); + const sourceFileName = `${libraryName}-generated.mm`; const sourceFile = SourceFileTemplate({ headerFileName, @@ -240,6 +316,7 @@ module.exports = { return new Map([ [headerFileName, headerFile], + [swiftCompatibleHeaderFileName, swiftHeaderFile], [sourceFileName, sourceFile], ]); }, diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h new file mode 100644 index 00000000000000..19ba1cbf925040 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTModule.h @@ -0,0 +1,11 @@ +// +// RCTModule.mm +// Pods +// +// Created by Riccardo Cipolleschi on 18/07/2025. +// + +@protocol RCTModule + +@end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h new file mode 100644 index 00000000000000..51ae3ed4bd4b54 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTSwiftTurboModule.h @@ -0,0 +1,13 @@ +// +// RCTSwiftTurboModule.h +// Pods +// +// Created by Riccardo Cipolleschi on 18/07/2025. +// + +#import "RCTModule.h" + +@protocol RCTSwiftTurboModule + +@end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h index d3fc87e6f148db..85681aecc7836f 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h @@ -19,8 +19,10 @@ #import #import +#import "RCTModule.h" + #define RCT_IS_TURBO_MODULE_CLASS(klass) \ - ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTTurboModule)])) + ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTModule)])) #define RCT_IS_TURBO_MODULE_INSTANCE(module) RCT_IS_TURBO_MODULE_CLASS([(module) class]) namespace facebook::react { @@ -182,13 +184,20 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { */ - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params; + +@optional +/** + * Return an instance of an Apple Module + */ +- (Class)getAppleModule; + @end /** * Protocol that objects can inherit to conform to be treated as turbomodules. * It inherits from RCTTurboModuleProvider, meaning that a TurboModule can create itself */ -@protocol RCTTurboModule +@protocol RCTTurboModule @optional - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper; @@ -206,3 +215,4 @@ class JSI_EXPORT ObjCTurboModule : public TurboModule { - (std::shared_ptr)decorateNativeMethodCallInvoker: (std::shared_ptr)nativeMethodCallInvoker; @end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm index b36c819cca85dc..fd9007d9066be6 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm @@ -865,7 +865,7 @@ TraceSection s( void ObjCTurboModule::setEventEmitterCallback(EventEmitterCallback eventEmitterCallback) { - if ([instance_ conformsToProtocol:@protocol(RCTTurboModule)] && + if ([instance_ conformsToProtocol:@protocol(RCTModule)] && [instance_ respondsToSelector:@selector(setEventEmitterCallback:)]) { EventEmitterCallbackWrapper *wrapper = [EventEmitterCallbackWrapper new]; wrapper->_eventEmitterCallback = std::move(eventEmitterCallback); @@ -877,3 +877,4 @@ TraceSection s( @implementation EventEmitterCallbackWrapper @end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm index 4f709dcfd58017..7d37db639018e7 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm @@ -157,7 +157,7 @@ void invokeSync(const std::string &methodName, std::function &&work) ove bool isTurboModuleClass(Class cls) { - return [cls conformsToProtocol:@protocol(RCTTurboModule)]; + return [cls conformsToProtocol:@protocol(RCTModule)]; } bool isTurboModuleInstance(id module) @@ -356,18 +356,24 @@ - (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy /** * Step 2: Look for platform-specific modules. */ - id module = [self _moduleProviderForName:moduleName]; + id moduleProvider = [self _moduleProviderForName:moduleName]; TurboModulePerfLogger::moduleJSRequireEndingStart(moduleName); // If we request that a TurboModule be created, its respective ObjC class must exist // If the class doesn't exist, then _provideObjCModule returns nil - if (!module) { + if (!moduleProvider) { return nullptr; } + + id module = nullptr; + if ([moduleProvider respondsToSelector:@selector(getAppleModule)]) { + module = (id)[self _provideObjCModule:moduleName moduleProvider:moduleProvider]; + } + id moduleOrProvider = module ? module : moduleProvider; std::shared_ptr nativeMethodCallInvoker = nullptr; - dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(module, &kAssociatedMethodQueueKey); + dispatch_queue_t methodQueue = (dispatch_queue_t)objc_getAssociatedObject(moduleOrProvider, &kAssociatedMethodQueueKey); if (methodQueue) { /** * Step 2c: Create and native CallInvoker from the TurboModule's method queue. @@ -384,32 +390,44 @@ - (instancetype)initWithBridgeProxy:(RCTBridgeProxy *)bridgeProxy } /** - * Step 3: Return an exact sub-class of ObjC TurboModule + * Step 2d: If the moduleClass is a legacy CxxModule, return a TurboCxxModule instance that + * wraps CxxModule. + */ + Class moduleClass = [moduleOrProvider class]; +// if ([moduleClass isSubclassOfClass:RCTCxxModule.class]) { +// // Use TurboCxxModule compat class to wrap the CxxModule instance. +// // This is only for migration convenience, despite less performant. +// auto turboModule = std::make_shared([((RCTCxxModule *)moduleOrProvider) createModule], _jsInvoker); +// _turboModuleCache.insert({moduleName, turboModule}); +// return turboModule; +// } + + /** + * Step 2e: Return an exact sub-class of ObjC TurboModule * * Use respondsToSelector: below to infer conformance to @protocol(RCTTurboModule). Using conformsToProtocol: is * expensive. */ - Class moduleClass = [module class]; - if ([module respondsToSelector:@selector(getTurboModule:)]) { + if ([moduleProvider respondsToSelector:@selector(getTurboModule:)]) { ObjCTurboModule::InitParams params = { .moduleName = moduleName, - .instance = (id)module, + .instance = (id)moduleOrProvider, .jsInvoker = _jsInvoker, .nativeMethodCallInvoker = nativeMethodCallInvoker, .isSyncModule = methodQueue == RCTJSThread, }; - auto turboModule = [(id)module getTurboModule:params]; + auto turboModule = [(id)moduleProvider getTurboModule:params]; if (turboModule == nullptr) { RCTLogError(@"TurboModule \"%@\"'s getTurboModule: method returned nil.", moduleClass); } _turboModuleCache.insert({moduleName, turboModule}); - if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:callInvoker:)]) { - [(id)module installJSIBindingsWithRuntime:*runtime callInvoker:_jsInvoker]; - } else if ([module respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) { + if ([moduleOrProvider respondsToSelector:@selector(installJSIBindingsWithRuntime:callInvoker:)]) { + [(id)moduleOrProvider installJSIBindingsWithRuntime:*runtime callInvoker:_jsInvoker]; + } else if ([moduleOrProvider respondsToSelector:@selector(installJSIBindingsWithRuntime:)]) { // Old API without CallInvoker (deprecated) - [(id)module installJSIBindingsWithRuntime:*runtime]; + [(id)moduleOrProvider installJSIBindingsWithRuntime:*runtime]; } return turboModule; } @@ -490,16 +508,16 @@ - (BOOL)_isLegacyModuleClass:(Class)moduleClass if ([_delegate respondsToSelector:@selector(getModuleProvider:)]) { moduleProvider = [_delegate getModuleProvider:moduleName]; } - + if (RCTTurboModuleInteropEnabled() && ![self _isTurboModule:moduleName] && !moduleProvider) { return nil; } - + if (moduleProvider) { - if ([moduleProvider conformsToProtocol:@protocol(RCTTurboModule)]) { - // moduleProvider is also a TM, we need to initialize objectiveC properties, like the dispatch queue + if ([moduleProvider conformsToProtocol:@protocol(RCTModule)]) { return (id)[self _provideObjCModule:moduleName moduleProvider:moduleProvider]; } + // module is Cxx module return moduleProvider; } @@ -579,7 +597,12 @@ - (ModuleHolder *)_getOrCreateModuleHolder:(const char *)moduleName /** * Step 2a: Resolve platform-specific class. */ - Class moduleClass = moduleProvider ? [moduleProvider class] : [self _getModuleClassFromName:moduleName]; + Class moduleClass = moduleProvider ? + ([moduleProvider respondsToSelector:@selector(getAppleModule)] ? + [moduleProvider getAppleModule] : + [moduleProvider class]) : + [self _getModuleClassFromName:moduleName]; + __block id module = nil; @@ -634,7 +657,7 @@ - (BOOL)_shouldCreateObjCModule:(Class)moduleClass return [moduleClass conformsToProtocol:@protocol(RCTBridgeModule)]; } - return [moduleClass conformsToProtocol:@protocol(RCTTurboModule)]; + return [moduleClass conformsToProtocol:@protocol(RCTModule)]; } /** @@ -1105,3 +1128,4 @@ - (void)_invalidateModules } @end + diff --git a/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/__docs__/Swift-TurboModules.md b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/__docs__/Swift-TurboModules.md new file mode 100644 index 00000000000000..f843b00cb9fd81 --- /dev/null +++ b/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/__docs__/Swift-TurboModules.md @@ -0,0 +1,250 @@ +# Swift TurboModules Support in React Native + +This document explains the approach implemented in [D92527173](https://www.internalfb.com/diff/D92527173) to enable writing TurboModules directly in pure Swift, without requiring Objective-C++ wrappers. + +## Overview + +Traditionally, React Native TurboModules on iOS require Objective-C++ implementation files (`.mm`) to bridge between JavaScript and native code. This change introduces infrastructure to write TurboModules entirely in Swift, simplifying the developer experience for iOS developers who prefer Swift. + +## Architecture + +### Protocol Hierarchy + +The implementation introduces a new protocol hierarchy: + +``` +RCTModule (base protocol) + ├── RCTSwiftTurboModule (for pure Swift modules) + └── RCTTurboModule (existing Obj-C++ modules, also conforms to RCTModuleProvider) +``` + +#### New Protocols + +1. **`RCTModule`**: A minimal base protocol that serves as the root marker for all React Native module types. Both Swift and Objective-C++ TurboModules conform to this protocol. + +2. **`RCTSwiftTurboModule`**: A protocol specifically designed for Swift TurboModules. It inherits from `RCTModule` and is used by the codegen to generate Swift-compatible headers. + +### Module Provider Pattern + +A new optional method has been added to `RCTModuleProvider`: + +```objc +@optional +- (Class)getAppleModule; +``` + +This method allows a module provider (typically Objective-C++) to return a reference to a Swift module class. The TurboModuleManager uses this to: + +1. Identify when a Swift module is being requested +2. Properly instantiate the Swift class +3. Wire it up to the TurboModule infrastructure + +### Codegen Changes + +The React Native Codegen now generates an additional header file for Swift compatibility: + +- **`{LibraryName}-Swift.h`**: Contains Swift-compatible protocol declarations that inherit from both `RCTBridgeModule` and `RCTSwiftTurboModule` + +Example generated protocol: + +```objc +@protocol NativeCalculatorSpec + +- (NSNumber *)add:(double)a b:(double)b; +- (NSNumber *)subtract:(double)a b:(double)b; + +@end +``` + +## How to Use + +### Step 1: Define Your Module Spec + +Create a TypeScript specification file for your module: + +```typescript +// NativeCalculator.ts +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + add(a: number, b: number): number; + subtract(a: number, b: number): number; +} + +export default TurboModuleRegistry.getEnforcing('NativeCalculator'); +``` + +### Step 2: Run Codegen + +Run the React Native codegen to generate the Swift-compatible headers: + +```bash +# This happens automatically during pod install or can be run manually +./scripts/generate-codegen-artifacts.js +``` + +This generates: +- `NativeCalculatorSpec.h` (standard Obj-C++ header) +- `NativeCalculator-Swift.h` (Swift-compatible header) + +### Step 3: Implement in Swift + +Create your Swift implementation: + +```swift +// NativeCalculator.swift +import Foundation +import React +import ReactCommon + +@objc(NativeCalculator) +class NativeCalculator: NSObject, NativeCalculatorSpec { + + @objc static func moduleName() -> String! { + return "NativeCalculator" + } + + @objc func add(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a + b) + } + + @objc func subtract(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a - b) + } +} +``` + +### Step 4: Create the Module Provider + +Create an Objective-C++ file that bridges the Swift module to the TurboModule system: + +```objc +// NativeCalculatorProvider.mm +#import +#import "YourApp-Swift.h" // Swift bridging header + +@interface NativeCalculatorProvider : NSObject +@end + +@implementation NativeCalculatorProvider + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + // Return the TurboModule wrapping the Swift class + return std::make_shared(params); +} + +- (Class)getAppleModule { + return [NativeCalculator class]; +} + +@end +``` + +### Step 5: Register the Module + +Register your module by configuring the `modulesProvider` mapping in your `package.json`. This tells React Native which provider class to use for each Swift TurboModule. + +Add the `modulesProvider` field under the `ios` section of your `codegenConfig`: + +```json +{ + "name": "your-app", + "version": "1.0.0", + "codegenConfig": { + "name": "AppSpecs", + "type": "modules", + "jsSrcsDir": "specs", + "android": { + "javaPackageName": "com.yourapp.specs" + }, + "ios": { + "modulesProvider": { + "NativeCalculator": "NativeCalculatorProvider" + } + } + } +} +``` + +The `modulesProvider` is a dictionary that maps: +- **Key**: The module name (as defined in your TypeScript spec and returned by `moduleName()` in Swift) +- **Value**: The name of the Objective-C++ provider class that implements `getAppleModule` + +When you run `pod install`, the codegen will use this mapping to generate the `RCTAppDependencyProvider` which automatically wires up your Swift modules to the TurboModule infrastructure. + +You can register multiple Swift modules by adding additional entries: + +```json +"ios": { + "modulesProvider": { + "NativeCalculator": "NativeCalculatorProvider", + "NativeStorage": "NativeStorageProvider", + "NativeAuth": "NativeAuthProvider" + } +} +``` + +## Key Implementation Details + +### TurboModuleManager Changes + +The `RCTTurboModuleManager` has been updated to: + +1. **Detect Swift modules**: When `getAppleModule` is implemented, the manager knows to treat the module as a Swift-based implementation. + +2. **Separate provider and module**: The manager now distinguishes between: + - `moduleProvider`: The Obj-C++ wrapper that provides the TurboModule + - `module`: The actual Swift class instance + +3. **Proper initialization**: Swift modules are instantiated through the `getAppleModule` class reference, ensuring proper Swift runtime initialization. + +### Protocol Conformance Check + +The macro `RCT_IS_TURBO_MODULE_CLASS` now checks for `RCTModule` conformance (instead of `RCTTurboModule`), enabling both Swift and Obj-C++ modules to be recognized: + +```objc +#define RCT_IS_TURBO_MODULE_CLASS(klass) \ + ((RCTTurboModuleEnabled() && [(klass) conformsToProtocol:@protocol(RCTModule)])) +``` + +### Pod Configuration + +The podspec template has been updated to include Swift-generated files: + +```ruby +s.source_files = ["**/RCTAppDependencyProvider.{h,mm}", "**/*-Swift.{h,mm}"] +``` + +## Benefits + +1. **Pure Swift Development**: Write TurboModules entirely in Swift without managing Objective-C++ complexity. + +2. **Type Safety**: Leverage Swift's type system while still benefiting from codegen-generated protocols. + +3. **Familiar Patterns**: Use standard Swift classes and methods with `@objc` annotations for bridging. + +4. **Incremental Adoption**: Existing Objective-C++ TurboModules continue to work unchanged. + +## Limitations (Current State) + +This is a basic implementation with the following current limitations: + +- Requires an Objective-C++ provider file to bridge the Swift module +- Event emitter support may need additional configuration +- Complex types (arrays, dictionaries with nested objects) may require additional bridging + +## Future Improvements + +Future iterations may include: + +- Automatic provider generation from codegen +- Direct Swift-to-JSI bridging without Objective-C++ intermediary +- Enhanced type mapping for complex Swift types +- Swift macro support for reducing boilerplate + +## References + +- [Pull Request #52710](https://github.com/facebook/react-native/pull/52710) +- [React Native TurboModules Documentation](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules) diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js index 4d41c5c5e88d8d..316d64c61a05f2 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/index.js @@ -43,8 +43,40 @@ const { readPkgJsonInDirectory, readReactNativeConfig, } = require('./utils'); +const fs = require('fs'); const path = require('path'); +/** + * Moves Swift-compatible header files from ReactCodegen to ReactAppDependencyProvider. + * These headers define protocols for Swift TurboModules and need to be in the + * ReactAppDependencyProvider pod for proper import with angle brackets. + */ +function moveSwiftHeadersToAppDependencyProvider( + reactCodegenOutputPath /*: string */, + appDependencyProviderPath /*: string */, +) { + if (!fs.existsSync(reactCodegenOutputPath)) { + return; + } + + const entries = fs.readdirSync(reactCodegenOutputPath, {withFileTypes: true}); + for (const entry of entries) { + if (entry.isDirectory()) { + const subdir = path.join(reactCodegenOutputPath, entry.name); + const files = fs.readdirSync(subdir); + for (const file of files) { + if (file.endsWith('-Swift.h')) { + const sourcePath = path.join(subdir, file); + const destPath = path.join(appDependencyProviderPath, file); + fs.mkdirSync(appDependencyProviderPath, {recursive: true}); + fs.renameSync(sourcePath, destPath); + codegenLog(`Moved ${file} to ReactAppDependencyProvider`); + } + } + } + } +} + /** * This function is the entry point for the codegen. It: * - reads the package json @@ -163,9 +195,15 @@ function execute( libraries, reactCodegenOutputPath, ); - generateAppDependencyProvider( - path.join(outputPath, 'ReactAppDependencyProvider'), + const appDependencyProviderPath = path.join( + outputPath, + 'ReactAppDependencyProvider', + ); + moveSwiftHeadersToAppDependencyProvider( + reactCodegenOutputPath, + appDependencyProviderPath, ); + generateAppDependencyProvider(appDependencyProviderPath); generateReactCodegenPodspec( projectRoot, pkgJson, diff --git a/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template b/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template index 327bc9c102e37d..6959ac25271caa 100644 --- a/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template +++ b/packages/react-native/scripts/codegen/templates/ReactAppDependencyProvider.podspec.template @@ -22,7 +22,8 @@ Pod::Spec.new do |s| s.author = "Meta Platforms, Inc. and its affiliates" s.platforms = min_supported_versions s.source = source - s.source_files = "**/RCTAppDependencyProvider.{h,mm}" + s.source_files = ["**/RCTAppDependencyProvider.{h,mm}", "**/*-Swift.{h,mm}"] + s.public_header_files = ["**/RCTAppDependencyProvider.h", "**/*-Swift.h"] # This guard prevent to install the dependencies when we run `pod install` in the old architecture. s.pod_target_xcconfig = { From 91d4754bbbe1f0d4304c0ff2f185523320b722aa Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Sat, 7 Feb 2026 05:59:01 -0800 Subject: [PATCH 2/2] Add sample Swift TurboModule to HelloWorld app Summary: This diff adds a sample Swift TurboModule (NativeCalculator) to the HelloWorld app to demonstrate the new Swift TurboModule support introduced in D92527173. The sample includes: - TypeScript spec file defining the module interface (add, subtract, multiply, divide) - Pure Swift implementation of the TurboModule - Objective-C++ provider that bridges the Swift module to the TurboModule infrastructure - Updated package.json with codegenConfig and modulesProvider mapping - Updated App.tsx to demonstrate using the module This serves as a reference implementation for developers looking to write TurboModules in pure Swift. Differential Revision: D92530466 --- private/helloworld/App.tsx | 39 ++++++++++++++ private/helloworld/Gemfile.lock | 2 + .../ios/HelloWorld-Bridging-Header.h | 6 +++ .../ios/HelloWorld.xcodeproj/project.pbxproj | 54 ++++++++++++++----- private/helloworld/ios/HelloWorld/Info.plist | 3 +- .../ios/HelloWorld/NativeCalculator.swift | 38 +++++++++++++ .../HelloWorld/NativeCalculatorProvider.mm | 30 +++++++++++ private/helloworld/ios/NativeCalculator.swift | 38 +++++++++++++ .../ios/NativeCalculatorProvider.mm | 30 +++++++++++ private/helloworld/package.json | 10 ++++ private/helloworld/specs/NativeCalculator.ts | 20 +++++++ 11 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 private/helloworld/ios/HelloWorld-Bridging-Header.h create mode 100644 private/helloworld/ios/HelloWorld/NativeCalculator.swift create mode 100644 private/helloworld/ios/HelloWorld/NativeCalculatorProvider.mm create mode 100644 private/helloworld/ios/NativeCalculator.swift create mode 100644 private/helloworld/ios/NativeCalculatorProvider.mm create mode 100644 private/helloworld/specs/NativeCalculator.ts diff --git a/private/helloworld/App.tsx b/private/helloworld/App.tsx index 09f7928a852a2f..71ad412af20a23 100644 --- a/private/helloworld/App.tsx +++ b/private/helloworld/App.tsx @@ -17,16 +17,41 @@ import { View, useColorScheme, } from 'react-native'; +import NativeCalculator from './specs/NativeCalculator'; function App(): React.ReactNode { const isDarkMode = useColorScheme() === 'dark'; + // Demo: Using the Swift TurboModule + const a = 10; + const b = 3; + + const sum = NativeCalculator.add(a, b); + const difference = NativeCalculator.subtract(a, b); + const product = NativeCalculator.multiply(a, b); + const quotient = NativeCalculator.divide(a, b); + return ( Hello, World! + Swift TurboModule Demo + + + {a} + {b} = {sum} + + + {a} - {b} = {difference} + + + {a} * {b} = {product} + + + {a} / {b} = {quotient.toFixed(2)} + + @@ -38,6 +63,20 @@ const styles = StyleSheet.create({ fontSize: 24, fontWeight: '600', }, + subtitle: { + fontSize: 16, + fontWeight: '400', + marginTop: 8, + color: '#666', + }, + resultsContainer: { + marginTop: 20, + padding: 16, + }, + result: { + fontSize: 18, + marginVertical: 4, + }, }); export default App; diff --git a/private/helloworld/Gemfile.lock b/private/helloworld/Gemfile.lock index dc0c892f686b0c..9212923dd15e30 100644 --- a/private/helloworld/Gemfile.lock +++ b/private/helloworld/Gemfile.lock @@ -98,6 +98,7 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.5, < 7.1.0) + base64 benchmark bigdecimal cocoapods (~> 1.13, != 1.15.1, != 1.15.0) @@ -105,6 +106,7 @@ DEPENDENCIES ffi (>= 1.17.2) logger mutex_m + nkf rexml (>= 3.3.9) xcodeproj (< 1.26.0) diff --git a/private/helloworld/ios/HelloWorld-Bridging-Header.h b/private/helloworld/ios/HelloWorld-Bridging-Header.h new file mode 100644 index 00000000000000..4d9fa56893d3bb --- /dev/null +++ b/private/helloworld/ios/HelloWorld-Bridging-Header.h @@ -0,0 +1,6 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import +#import diff --git a/private/helloworld/ios/HelloWorld.xcodeproj/project.pbxproj b/private/helloworld/ios/HelloWorld.xcodeproj/project.pbxproj index dca49419fb7ced..c180f7833bb092 100644 --- a/private/helloworld/ios/HelloWorld.xcodeproj/project.pbxproj +++ b/private/helloworld/ios/HelloWorld.xcodeproj/project.pbxproj @@ -11,9 +11,11 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 6EA01F72FAC10D00AECACF94 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0EC7AB76F90EED035707BA4E /* PrivacyInfo.xcprivacy */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + C2BCF5212460036C4783057C /* libPods-HelloWorld.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7038AA8D3705D523CBA20DED /* libPods-HelloWorld.a */; }; + CD8C241A2F36649F004EC1C7 /* NativeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD8C24182F36649F004EC1C7 /* NativeCalculator.swift */; }; + CD8C241B2F36649F004EC1C7 /* NativeCalculatorProvider.mm in Sources */ = {isa = PBXBuildFile; fileRef = CD8C24192F36649F004EC1C7 /* NativeCalculatorProvider.mm */; }; CDA0ED1A2D0B2D810079F561 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA0ED192D0B2D810079F561 /* AppDelegate.swift */; }; - D462E9F4436EDF91C8A1FA0A /* Pods_HelloWorld.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3F2101C8317E5C933C1BD4C /* Pods_HelloWorld.framework */; }; - D693EA25CB545D6C1C7F8538 /* Pods_HelloWorld_HelloWorldTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A97924660E462ECF2425C3A /* Pods_HelloWorld_HelloWorldTests.framework */; }; + E4E8FFC8C8ED1ABDC6FE61FA /* libPods-HelloWorld-HelloWorldTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146C816C66DD227EC06C6C11 /* libPods-HelloWorld-HelloWorldTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,13 +37,16 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = HelloWorld/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = HelloWorld/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = HelloWorld/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 146C816C66DD227EC06C6C11 /* libPods-HelloWorld-HelloWorldTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HelloWorld-HelloWorldTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B4392A12AC88292D35C810B /* Pods-HelloWorld.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld.debug.xcconfig"; path = "Target Support Files/Pods-HelloWorld/Pods-HelloWorld.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-HelloWorld.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld.release.xcconfig"; path = "Target Support Files/Pods-HelloWorld/Pods-HelloWorld.release.xcconfig"; sourceTree = ""; }; 5B7EB9410499542E8C5724F5 /* Pods-HelloWorld-HelloWorldTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld-HelloWorldTests.debug.xcconfig"; path = "Target Support Files/Pods-HelloWorld-HelloWorldTests/Pods-HelloWorld-HelloWorldTests.debug.xcconfig"; sourceTree = ""; }; - 7A97924660E462ECF2425C3A /* Pods_HelloWorld_HelloWorldTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HelloWorld_HelloWorldTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7038AA8D3705D523CBA20DED /* libPods-HelloWorld.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-HelloWorld.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = HelloWorld/LaunchScreen.storyboard; sourceTree = ""; }; 89C6BE57DB24E9ADA2F236DE /* Pods-HelloWorld-HelloWorldTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HelloWorld-HelloWorldTests.release.xcconfig"; path = "Target Support Files/Pods-HelloWorld-HelloWorldTests/Pods-HelloWorld-HelloWorldTests.release.xcconfig"; sourceTree = ""; }; - B3F2101C8317E5C933C1BD4C /* Pods_HelloWorld.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HelloWorld.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CD8C24182F36649F004EC1C7 /* NativeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCalculator.swift; sourceTree = ""; }; + CD8C24192F36649F004EC1C7 /* NativeCalculatorProvider.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeCalculatorProvider.mm; sourceTree = ""; }; + CD8C241C2F3664A3004EC1C7 /* HelloWorld-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "HelloWorld-Bridging-Header.h"; sourceTree = ""; }; CDA0ED192D0B2D810079F561 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = HelloWorld/AppDelegate.swift; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -51,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D693EA25CB545D6C1C7F8538 /* Pods_HelloWorld_HelloWorldTests.framework in Frameworks */, + E4E8FFC8C8ED1ABDC6FE61FA /* libPods-HelloWorld-HelloWorldTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -59,7 +64,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D462E9F4436EDF91C8A1FA0A /* Pods_HelloWorld.framework in Frameworks */, + C2BCF5212460036C4783057C /* libPods-HelloWorld.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -86,12 +91,15 @@ 13B07FAE1A68108700A75B9A /* HelloWorld */ = { isa = PBXGroup; children = ( + CD8C24182F36649F004EC1C7 /* NativeCalculator.swift */, + CD8C24192F36649F004EC1C7 /* NativeCalculatorProvider.mm */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, 0EC7AB76F90EED035707BA4E /* PrivacyInfo.xcprivacy */, CDA0ED192D0B2D810079F561 /* AppDelegate.swift */, + CD8C241C2F3664A3004EC1C7 /* HelloWorld-Bridging-Header.h */, ); name = HelloWorld; sourceTree = ""; @@ -100,8 +108,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - B3F2101C8317E5C933C1BD4C /* Pods_HelloWorld.framework */, - 7A97924660E462ECF2425C3A /* Pods_HelloWorld_HelloWorldTests.framework */, + 7038AA8D3705D523CBA20DED /* libPods-HelloWorld.a */, + 146C816C66DD227EC06C6C11 /* libPods-HelloWorld-HelloWorldTests.a */, ); name = Frameworks; sourceTree = ""; @@ -206,7 +214,7 @@ TestTargetID = 13B07F861A680F5B00A75B9A; }; 13B07F861A680F5B00A75B9A = { - LastSwiftMigration = 1610; + LastSwiftMigration = 2620; }; }; }; @@ -395,6 +403,8 @@ buildActionMask = 2147483647; files = ( CDA0ED1A2D0B2D810079F561 /* AppDelegate.swift in Sources */, + CD8C241A2F36649F004EC1C7 /* NativeCalculator.swift in Sources */, + CD8C241B2F36649F004EC1C7 /* NativeCalculatorProvider.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -484,6 +494,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = HelloWorld; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../packages/react-native"; + SWIFT_OBJC_BRIDGING_HEADER = "HelloWorld-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -511,6 +522,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = HelloWorld; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../packages/react-native"; + SWIFT_OBJC_BRIDGING_HEADER = "HelloWorld-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -588,6 +600,10 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -595,9 +611,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); - OTHER_LDFLAGS = "$(inherited) "; - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../react-native"; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../packages/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; USE_HERMES = true; @@ -668,6 +688,10 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -675,9 +699,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", ); - OTHER_LDFLAGS = "$(inherited) "; - REACT_NATIVE_PATH = "${PODS_ROOT}/../../../react-native"; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../packages/react-native"; SDKROOT = iphoneos; USE_HERMES = true; VALIDATE_PRODUCT = YES; diff --git a/private/helloworld/ios/HelloWorld/Info.plist b/private/helloworld/ios/HelloWorld/Info.plist index 07b050898d9d30..73938030310d40 100644 --- a/private/helloworld/ios/HelloWorld/Info.plist +++ b/private/helloworld/ios/HelloWorld/Info.plist @@ -26,7 +26,6 @@ NSAppTransportSecurity - NSAllowsArbitraryLoads NSAllowsLocalNetworking @@ -34,6 +33,8 @@ NSLocationWhenInUseUsageDescription + RCTNewArchEnabled + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/private/helloworld/ios/HelloWorld/NativeCalculator.swift b/private/helloworld/ios/HelloWorld/NativeCalculator.swift new file mode 100644 index 00000000000000..a4ef0665000711 --- /dev/null +++ b/private/helloworld/ios/HelloWorld/NativeCalculator.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Foundation +import React + +/// A sample Swift TurboModule that performs basic arithmetic operations. +/// This demonstrates how to write TurboModules in pure Swift. +@objc(NativeCalculator) +public class NativeCalculator: NSObject, NativeCalculatorSpec { + + @objc public static func moduleName() -> String! { + return "NativeCalculator" + } + + @objc public func add(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a + b) + } + + @objc public func subtract(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a - b) + } + + @objc public func multiply(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a * b) + } + + @objc public func divide(_ a: Double, b: Double) -> NSNumber { + guard b != 0 else { + return NSNumber(value: Double.nan) + } + return NSNumber(value: a / b) + } +} diff --git a/private/helloworld/ios/HelloWorld/NativeCalculatorProvider.mm b/private/helloworld/ios/HelloWorld/NativeCalculatorProvider.mm new file mode 100644 index 00000000000000..3c7dd1f7f0757a --- /dev/null +++ b/private/helloworld/ios/HelloWorld/NativeCalculatorProvider.mm @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "HelloWorld-Swift.h" + +using namespace facebook::react; + +/** + * Provider class that bridges the Swift NativeCalculator module + * to the TurboModule infrastructure. + */ +@interface NativeCalculatorProvider : NSObject +@end + +@implementation NativeCalculatorProvider + +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (Class)getAppleModule { + return [NativeCalculator class]; +} + +@end diff --git a/private/helloworld/ios/NativeCalculator.swift b/private/helloworld/ios/NativeCalculator.swift new file mode 100644 index 00000000000000..a4ef0665000711 --- /dev/null +++ b/private/helloworld/ios/NativeCalculator.swift @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Foundation +import React + +/// A sample Swift TurboModule that performs basic arithmetic operations. +/// This demonstrates how to write TurboModules in pure Swift. +@objc(NativeCalculator) +public class NativeCalculator: NSObject, NativeCalculatorSpec { + + @objc public static func moduleName() -> String! { + return "NativeCalculator" + } + + @objc public func add(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a + b) + } + + @objc public func subtract(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a - b) + } + + @objc public func multiply(_ a: Double, b: Double) -> NSNumber { + return NSNumber(value: a * b) + } + + @objc public func divide(_ a: Double, b: Double) -> NSNumber { + guard b != 0 else { + return NSNumber(value: Double.nan) + } + return NSNumber(value: a / b) + } +} diff --git a/private/helloworld/ios/NativeCalculatorProvider.mm b/private/helloworld/ios/NativeCalculatorProvider.mm new file mode 100644 index 00000000000000..3c7dd1f7f0757a --- /dev/null +++ b/private/helloworld/ios/NativeCalculatorProvider.mm @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import "HelloWorld-Swift.h" + +using namespace facebook::react; + +/** + * Provider class that bridges the Swift NativeCalculator module + * to the TurboModule infrastructure. + */ +@interface NativeCalculatorProvider : NSObject +@end + +@implementation NativeCalculatorProvider + +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (Class)getAppleModule { + return [NativeCalculator class]; +} + +@end diff --git a/private/helloworld/package.json b/private/helloworld/package.json index 10f94b7f3b4a01..e201712eca584b 100644 --- a/private/helloworld/package.json +++ b/private/helloworld/package.json @@ -2,6 +2,16 @@ "name": "helloworld", "version": "0.0.0", "private": true, + "codegenConfig": { + "name": "HelloWorldSpecs", + "type": "modules", + "jsSrcsDir": "specs", + "ios": { + "modulesProvider": { + "NativeCalculator": "NativeCalculatorProvider" + } + } + }, "scripts": { "bootstrap": "node ./cli.js bootstrap", "build": "node ./cli.js build", diff --git a/private/helloworld/specs/NativeCalculator.ts b/private/helloworld/specs/NativeCalculator.ts new file mode 100644 index 00000000000000..49bdf5f5f6001b --- /dev/null +++ b/private/helloworld/specs/NativeCalculator.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; + +export interface Spec extends TurboModule { + add(a: number, b: number): number; + subtract(a: number, b: number): number; + multiply(a: number, b: number): number; + divide(a: number, b: number): number; +} + +export default TurboModuleRegistry.getEnforcing('NativeCalculator');