react-native-unimodules icon indicating copy to clipboard operation
react-native-unimodules copied to clipboard

Using React Native Unimodules in Swift

Open RomanScott opened this issue 6 years ago • 8 comments

Hello!

I would like to use this Unimodules project in my React Native project. However, the files that need to be modified under the Configure for iOS section are AppDelegate.h and AppDelegate.m

However, I converted my AppDelegate into a Swift file (with a bridge file to accompany it). I was wondering what is the equivalent of these two files shown in this gist (https://gist.github.com/brentvatne/1ece8c32a3c5c9d0ac3a470460c65603) in Swift?

Here is my code:

import Foundation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var bridge: RCTBridge!

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let jsCodeLocation: URL

    jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
    let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "PROJECT", initialProperties: nil, launchOptions: launchOptions)
    let rootViewController = UIViewController()
    rootViewController.view = rootView

    self.window = UIWindow(frame: UIScreen.main.bounds)
    self.window?.rootViewController = rootViewController
    self.window?.makeKeyAndVisible()

    return true
  }
}

@brentvatne Since you authored the gist, I believe you might have some insight!

RomanScott avatar Aug 03 '19 20:08 RomanScott

Hi! I'm having a similar issue, is there anywhere to look for using this with swift?

nmutalik avatar Sep 30 '19 19:09 nmutalik

@sjchmiela @tsapeta Hey, sorry to be a pain :( I wouldn't be bothering y'all if I hadn't spent like four days bashing my head trying to convert the AppDelegate.h/m into swift. Unfortunately, my understanding of what's happening close to the metal isn't so good.

Here's a starting point that I may have found, but I'm not sure how to interpret it https://stackoverflow.com/questions/37142310/dependency-injection-in-react-native-modules/47468905#47468905

Any information at this point would be hugely appreciated, even if that information were "right now we don't support swift"

nmutalik avatar Oct 03 '19 14:10 nmutalik

Hey @nmutalik, could you please provide a working AppDelegate.swift that you're trying to integrate with unimodules?

sjchmiela avatar Oct 11 '19 12:10 sjchmiela

Here you are!

import Foundation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var bridge: RCTBridge!
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure()
    let jsCodeLocation: URL
    
    jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
    let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "FireflyHealth", initialProperties: nil, launchOptions: launchOptions)
    let rootViewController = UIViewController()
    rootViewController.view = rootView
    
    self.window = UIWindow(frame: UIScreen.main.bounds)
    self.window?.rootViewController = rootViewController
    self.window?.makeKeyAndVisible()
    
    return true
  }

  func applicationDidBecomeActive(_ application: UIApplication) {
    AppEvents.activateApp()
  }

  // Orientation control library
  // https://github.com/yamill/react-native-orientation
  func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return Orientation.getOrientation()
  }

  // DEEPLINKING
  func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    return RCTLinkingManager.application(application, open: url, options: options)
  }
  
  // Only if your app is using [Universal Links](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html).
  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler as! ([Any]?) -> Void)
  }
}

nmutalik avatar Oct 16 '19 20:10 nmutalik

@sjchmiela we're very excited to be able to use this package, but unfortunately we're still stuck on this step

nmutalik avatar Oct 30 '19 18:10 nmutalik

Step-by-step

Add moduleRegistryAdapter instance variable

 class AppDelegate: UIResponder, UIApplicationDelegate {
   var window: UIWindow?
   var bridge: RCTBridge!
+  var moduleRegistryAdapter: UMModuleRegistryAdapter!

Initialize moduleRegistryAdapter at the top of application:didFinishLaunching:

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+  self.moduleRegistryAdapter = UMModuleRegistryAdapter(moduleRegistryAdapter: UMModuleRegistryProvider())
     FirebaseApp.configure()

Add RCTBridgeDelegate protocol to AppDelegate

-class AppDelegate: UIResponder, UIApplicationDelegate {
+class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {

Initialize RCTBridge ourselves so we can set ourselves as the delegate

+  let bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
-  let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "FireflyHealth", initialProperties: nil, launchOptions: launchOptions)
+  let rootView = RCTRootView(bridge: bridge, moduleName: "FireflyHealth", initialProperties: nil)

Implement extraModulesForBridge method

Add the following method to inside of AppDelegate

  func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
    var extraModules = self.moduleRegistryAdapter.extraModulesForBridge(bridge)
    return extraModules
  }

Move url to RCTBridgeDelegate method

Add the following method

  func sourceURL(for bridge: RCTBridge!) -> URL! {
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
  }

and remove let url… from inside application:didFinishLaunching

End result

import Foundation

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
  var window: UIWindow?
  var bridge: RCTBridge!
  var moduleRegistryAdapter: UMModuleRegistryAdapter!
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    self.moduleRegistryAdapter = UMModuleRegistryAdapter(moduleRegistryAdapter: UMModuleRegistryProvider())
    FirebaseApp.configure()

    let bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
    let rootView = RCTRootView(bridge: bridge, moduleName: "FireflyHealth", initialProperties: nil)
    let rootViewController = UIViewController()
    rootViewController.view = rootView
    
    self.window = UIWindow(frame: UIScreen.main.bounds)
    self.window?.rootViewController = rootViewController
    self.window?.makeKeyAndVisible()
    
    return true
  }

  func applicationDidBecomeActive(_ application: UIApplication) {
    AppEvents.activateApp()
  }

  // Orientation control library
  // https://github.com/yamill/react-native-orientation
  func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return Orientation.getOrientation()
  }

  // DEEPLINKING
  func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    return RCTLinkingManager.application(application, open: url, options: options)
  }
  
  // Only if your app is using [Universal Links](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html).
  func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler as! ([Any]?) -> Void)
  }

  func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
    var extraModules = self.moduleRegistryAdapter.extraModulesForBridge(bridge)
    return extraModules
  }

  func sourceURL(for bridge: RCTBridge!) -> URL! {
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
  }
}

Let me know if there are any compile errors after those changes (there probably will be).

sjchmiela avatar Oct 31 '19 08:10 sjchmiela

@nmutalik - if this works for you, can you open a PR with some instructions on how to use the library with swift?

brentvatne avatar Nov 01 '19 19:11 brentvatne

Can confirm that this does not work (SDK 37). But I have an easy fix.

Steps

  • Bootstrap a new project npx crna
    • select the default template.
  • Create new file (⌘N)
    • select swift
    • name it AppDelegate.swift
  • Confirm generating a bridging header myproject-Bridging-Header.h
  • Delete AppDelegate.m, AppDelegate.h, main.m
  • Populate files:

AppDelegate.swift

import Foundation

@UIApplicationMain
class AppDelegate: UMAppDelegateWrapper, EXUpdatesAppControllerDelegate {
  var launchOptions: [UIApplication.LaunchOptionsKey: Any]?

  var moduleRegistryAdapter: UMModuleRegistryAdapter!
  
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    moduleRegistryAdapter = UMModuleRegistryAdapter(moduleRegistryProvider: UMModuleRegistryProvider())
    self.launchOptions = launchOptions
    
    window = UIWindow(frame: UIScreen.main.bounds)

    #if DEBUG
      initializeReactNativeApp()
    #else
      let controller = EXUpdatesAppController.sharedInstance()
      controller.delegate = self
      controller.startAndShowLaunchScreen(window!)
    #endif

    super.application(application, didFinishLaunchingWithOptions: launchOptions);
    
    return true
  }
  
  @discardableResult
  func initializeReactNativeApp() -> RCTBridge? {
    guard let bridge = RCTBridge(delegate: self, launchOptions: launchOptions) else { return nil }
    let rootView = RCTRootView(bridge: bridge, moduleName: "main", initialProperties: nil)
    let rootViewController = UIViewController()
    rootView.backgroundColor = UIColor.white;
    rootViewController.view = rootView
    
    window?.rootViewController = rootViewController
    window?.makeKeyAndVisible()
    
    return bridge
  }

  func appController(_ appController: EXUpdatesAppController, didStartWithSuccess success: Bool) {
    appController.bridge = initializeReactNativeApp();
  }
}

// MARK: - RCTBridgeDelegate

extension AppDelegate: RCTBridgeDelegate {
  func sourceURL(for bridge: RCTBridge!) -> URL! {
    #if DEBUG
      return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource:nil)
    #else
      return EXUpdatesAppController.sharedInstance().launchAssetUrl!
    #endif
  }
  
  func extraModules(for bridge: RCTBridge!) -> [RCTBridgeModule]! {
    let extraModules = moduleRegistryAdapter.extraModules(for: bridge)
    // You can inject any extra modules that you would like here, more information at:
    // https://facebook.github.io/react-native/docs/native-modules-ios.html#dependency-injection
    return extraModules
  }
}

myproject-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

// Unimodules

#import <UMCore/UMModuleRegistry.h>
#import <UMReactNativeAdapter/UMNativeModulesProxy.h>
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>
#import <UMCore/UMAppDelegateWrapper.h>

// OTA Updates

#import <EXUpdates/EXUpdatesAppController.h>

// React

#import <React/RCTBridgeModule.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTRootView.h>
#import <React/RCTUtils.h>
#import <React/RCTConvert.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTBridgeDelegate.h>

Issue

The project builds properly but throws a runtime error:

2020-05-27 15:23:25.171761-0700 swiftdemo[40384:6054772] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[swiftdemo.AppDelegate setWindow:]: unrecognized selector sent to instance 0x600000905a60'

With exception breakpoints enabled, the project fails on UMAppDelegateWrapper.m#L23.

Solution

Simply add the following to your pod @unimodules/core/ios/UMCore/UMAppDelegateWrapper.h

// Copyright © 2018 650 Industries. All rights reserved.
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface UMAppDelegateWrapper : UIResponder <UIApplicationDelegate>

+ @property (nullable, nonatomic, strong) UIWindow *window;

@end

NS_ASSUME_NONNULL_END
  • Notice DEBUG will not work unless this is configured: https://stackoverflow.com/a/24112024/4047926
  • Upstream solution https://github.com/expo/expo/pull/8526

EvanBacon avatar May 27 '20 22:05 EvanBacon