react-native-community / RNNewArchitectureLibraries

A collection of sample React Native Libraries that will show you how to use the New Architecture (Fabric & TurboModules) step-by-step.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Swift example does not compile (with solution for proposal)

louiszawadzki opened this issue Β· comments

Hi!

First, thanks a lot for the work on this, it helped me a lot!

Also I'm opening an issue as I don't think using a PR would be useful here as we want to keep a strict commit order, but I have a branch ready with all the changes I'm describing next so you can push force it if it works for you: https://github.com/louiszawadzki/RNNewArchitectureLibraries/commits/feat/turbomodule-swift
I'm keeping the same commit history and I also updated the commit hashes in the readme :)

Maybe my changes are not optimal, feel free to let me know if there's a better way to make this work!

Issue with building the Swift example

I tried to test the feat/turbomodule-swift branch by doing the following (my NewArchitecture app uses RN 0.71.8):

  1. Navigate to the NewArchitecture root folder:
  2. yarn add ../calculator
  3. cd ios
  4. RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
  5. cd ..
  6. xed ios and run the app from XCode

When I do so I get the following error:
image

Fixing the issue (for my package)

I realized that the error goes away for my library when removing this part of the Podspec that was added for the Swift example:

-  s.pod_target_xcconfig    = {
-    "DEFINES_MODULE" => "YES",
-    "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES",
-    "SWIFT_OBJC_BRIDGING_HEADER" => "../../node_modules/calculator/ios/calculator-Bridging-Header.h",
-    "OTHER_CPLUSPLUSFLAGS" => "-DRCT_NEW_ARCH_ENABLED=1"
-  }

But then I get this error in RNCalculator.mm when doing the same in this project:

image

Additional steps to fix this package

Instead of using RCT_EXPORT_MODULE and RCT_REMAP_METHOD, I use RCT_EXTERN_MODULE and RCT_EXTERN_METHOD as recommended here: https://reactnative.dev/docs/native-modules-ios#exporting-swift

Making the change to this approach makes everything work without any error.

To do so, remove calculator/ios/RNCalculator.h then apply the following changes:

calculator/ios/Calculator.swift:

 import Foundation

-@objc
+@objc(RNCalculator)
 class Calculator: NSObject {

-  @objc
-  static func add(a: Int, b: Int) -> Int {
-    return a+b;
+  @objc(add:andB:withResolver:withRejecter:)
+  func add(a: Int, b: Int, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
+    resolve(a+b);
   }
 }

calculator/ios/RNCalculator.mm:

-#import "RNCalculator.h"
 // Thanks to this guard, we won't import this header when we build for the old architecture.
 #ifdef RCT_NEW_ARCH_ENABLED
 #import "RNCalculatorSpec.h"
 #endif

-#import "calculator-Swift.h"
-
-@implementation RNCalculator
+@interface RCT_EXTERN_MODULE(RNCalculator, NSObject)

-RCT_EXPORT_MODULE()
-
-RCT_REMAP_METHOD(add, addA:(NSInteger)a
+RCT_EXTERN_METHOD(add:(NSInteger)a
                         andB:(NSInteger)b
                 withResolver:(RCTPromiseResolveBlock) resolve
                 withRejecter:(RCTPromiseRejectBlock) reject)
-{
-  return [self add:a b:b resolve:resolve reject:reject];
-}

// Thanks to this guard, we won't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:  // <- Actually not removed, just diff formatting here
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeCalculatorSpecJSI>(params);
}
#endif

-- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
-  NSNumber *result = @([Calculator addWithA:a b:b]);
-  resolve(result);
-}

calculator/ios/calculator-Bridging-Header.h:

 //
 // Add the Objective-C headers that must imported by Swift files
 //
+#import <React/RCTBridgeModule.h>

Amazing, thank you @louiszawadzki for opening this issue. I'll try to test and implement your suggestions this week, as they seem very good to me and helped me learning something new in the process! Also, thanks to these, we may unblock many other people as well!

You're welcome πŸ˜ƒ
Also I'm curious about why the s.pod_target_xcconfig part was added to the Podspec, I'm not an iOS expert so I'm interested in knowing what was its purpose to check if I didn't leave out a particular edge case with my solution.

That part is partially still required. You were probably modifying the project from Xcode and Xcode does some magic under the hoods. However, those changes are lost the next time you run pod install as the .xcworkspace gets recreated starting from the podspec configurations.

The pod_target_xcconfig is a setting that can be used to customise the xcconfig files that add some custom settings to the pod once it is installed in a workspace.

Specifically, in this case, we were adding these settings:

s.pod_target_xcconfig    = {
   "DEFINES_MODULE" => "YES",
   "BUILD_LIBRARY_FOR_DISTRIBUTION" => "YES",
   "SWIFT_OBJC_BRIDGING_HEADER" => "../../node_modules/calculator/ios/calculator-Bridging-Header.h",
   "OTHER_CPLUSPLUSFLAGS" => "-DRCT_NEW_ARCH_ENABLED=1"
}
  • DEFINES_MODULE: Swift works only with proper Clang modules. Without specifying this, Cocoapods does not create modules for the pods when they are installed, so we need this setting in the podspec. More info here
  • BUILD_LIBRARY_FOR_DISTRIBUTION: this can go. I added this to get rid of those errors but I was not convinced by it. It is a setting that changes how the library is built. More info here
  • SWIFT_OBJC_BRIDGING_HEADER: this specify a custom path for a bridging header. Given that you are also suggesting to modify it, we need to keep this setting
  • OTHER_CPLUSPLUSFLAGS: this is a setting that allows to pass some custom C++ flags to the compiler. The -DRCT_NEW_ARCH_ENABLED flag is a custom flag we are using in React Native to distiguish between legacy and New Architecture. Given that the guide is about creating the new architecture, we need that to enable some code paths.

I hope these quick explanations are helpful for you! πŸ˜„

Hi @louiszawadzki! I had a deeper look at the changes you suggested. Thank you so much for taking the time for work on those as they enabled me to find a better solution for everyone! :D

What you implemented is basically a Native Module for the old architecture. Old Architecture modules, right now, works as-is also on the New Architecture, that's why it is tricky to check whether the implementation is correct.

To work on the New Architecture there are basically two requirements:

  1. The Native Module has to extend the interface generated by the CodeGen. In case of the calculator, that interface is RNCalculatorSpec.h
  2. Implement the getTurboModule: method.

Unfortunately, the RCT_EXTERN_MODULE macro does not allow to specify any protocol conformance, so the line

@interface RCT_EXTERN_MODULE(RNCalculator, NSObject)

Defines and register a module that respond to RNCalculator, but that would not be a proper TurboModule.

So far, the only way to use Swift in a Turbomodule is to delegate the computation to a Swift object, like I show in the previous example.

However, I manage to reproduce the build errors and now I can fix that for everyone! πŸ˜„

Hi @louiszawadzki, I updated the tutorial of turbomodule-swift with the recent changes. One thing I learned while working on that is that the import of calculator-Swift.h must be with angular brackets (<>) as Instructed here by Apple. I missed this paragraph last time!

Could you try this version out and tell me whether it works? :D

@cipolleschi thanks a lot for your work!
I am able to make it run in the end :D

I still need to polish things a little bit on my end, I'll let you know if I see any improvement, otherwise I'll close the issue :)

Hi @cipolleschi!
I'm finally seeing some light at the end of the tunnel πŸ˜„

A few things are missing in the example to make it work:

in Calculator.swift, the class must be marked as open and the function public to be available:

import Foundation

@objc
open class Calculator: NSObject {

  @objc
  public static func add(a: Int, b: Int) -> Int {
    return a+b;
  }
}

and in the calculator.podspec, to have retro-compatibility we need to apply the added snippet only if the new arch is enabled:

require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
  s.name            = "calculator"
  s.version         = package["version"]
  s.summary         = package["description"]
  s.description     = package["description"]
  s.homepage        = package["homepage"]
  s.license         = package["license"]
  s.platforms       = { :ios => "11.0" }
  s.author          = package["author"]
  s.source          = { :git => package["repository"], :tag => "#{s.version}" }

  s.source_files    = "ios/**/*.{h,m,mm,swift}"

  if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
    s.pod_target_xcconfig    = {
      "DEFINES_MODULE" => "YES",
      "OTHER_CPLUSPLUSFLAGS" => "-DRCT_NEW_ARCH_ENABLED=1"
    }

    install_modules_dependencies(s)
  end
end
  • I include install_modules_dependencies(s) in the if as this function is not available for RN versions <0.71. My plan is to declare support for the new architecture only for RN>=0.71, but I'm curious if there's a way for library maintainers to use this code in a way that supports older RN versions as well.

Thanks a lot for your help on this!!

EDIT: I still have compilation errors when I'm trying to compile the demo app with the old architecture, but not with my lib :) It might be an XCode cache issue, so I won't investigate more on it for now.

Thanks again for the feedback. open and public should be the same, tbh. I agree that the class must be public, especially after this WWDC, where they also explained why!

for the rest, you are right, they should be gated by if ENV['RCT_NEW_ARCH_ENABLED'] == '1' if you want to support both architectures!