We are upgrading a react native project from 0.66.4 to 0.69.6.
The project uses an AppDelegate.swift
file but the react native upgrade helper uses the AppDelegate.m
class in its upgrade example and instructs to delete it and replace it with a AppDelegate.mm
class. See Upgrade helper
To my understanding this allows the compiler to compile C and C++ code.
My question is how do we keep the logic in our AppDelegate swift class? Do we have to use the AppDelegate.mm and integrate the swift code into it or is there a better way?
Content from AppDelegate.swift
:
import UIKit
import Firebase
import Adyen
import GoogleCast
import Didomi
import os
import FBSDKCoreKit
#if DEBUG
// #if FB_SONARKIT_ENABLED
import FlipperKit
// #endif
#endif
@UIApplicationMain
class AppDelegate: UIResponder, RCTBridgeDelegate, UIApplicationDelegate,
UNUserNotificationCenterDelegate, RNAppAuthAuthorizationFlowManager {
private func initializeFlipper(with application: UIApplication) {
#if DEBUG
// Check if FB_SONARKIT_ENABLED and commented pluggins work in RN 0.67+ versions
// #if FB_SONARKIT_ENABLED
let client = FlipperClient.shared()
let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
// FlipperKitLayoutComponentKitSupport.setUpWith(layoutDescriptorMapper)
client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
client?.add(FKUserDefaultsPlugin(suiteName: nil))
client?.add(FlipperKitReactPlugin())
client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
// client?.add(FlipperReactPerformancePlugin.sharedInstance())
client?.start()
// #endif
#endif
}
func sourceURL(for bridge: RCTBridge!) -> URL! {
#if DEBUG
return RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
var window: UIWindow?
var bridge: RCTBridge!
var orientationLock: UIInterfaceOrientationMask = .portrait
var rootView: RCTRootView?
var isScreenRecordingEnabled: Bool?
var isCaptured: Bool?
public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
_ = localizedString(LocalizationKey(key: "adyen.card.expiryItem.title"), nil)
// Start screen recording in a disabled state
isScreenRecordingEnabled = false
let config = ReactNativeConfig.env()!
WonderPush.setClientId((config["WONDERPUSH_CLIENT_ID"] as? String)!,
secret: (config["WONDERPUSH_CLIENT_SECRET"] as? String)!)
WonderPush.setupDelegateForUserNotificationCenter()
WonderPush.setRequiresUserConsent(false)
WonderPush.setUserConsent(true)
return true
}
// swiftlint:disable block_based_kvo
// swiftlint:disable colon
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if #available(iOS 11.0, *) {
if keyPath == "captured" {
isCaptured = UIScreen.main.isCaptured
if !isScreenRecordingEnabled! {
if isCaptured! {
rootView?.isHidden = true
}
if !isCaptured! && rootView?.isHidden == true {
rootView?.isHidden = false
}
}
}
}
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// swiftlint:disable force_cast
launchDidomi(apiKey: ReactNativeConfig.env()!["DIDOMI_API_KEY"] as! String)
FirebaseApp.configure()
initializeFlipper(with: application)
let receiverAppID = kGCKDefaultMediaReceiverApplicationID // or "ABCD1234"
let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
GCKCastContext.setSharedInstanceWith(options)
GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
self.bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
guard let bridge = self.bridge else {
return false
}
let props: [AnyHashable: Any] = [
"APP_INFO": (Bundle.main.infoDictionary?["APP_INFO"] as? String) ?? "",
"VERSION": (Bundle.main.infoDictionary?["CFBundleShortVersionString"]) ?? ""
]
rootView = RCTRootView(bridge: bridge, moduleName: "PSG", initialProperties: props)
rootView?.backgroundColor = UIColor(red: 22.0/255.0, green: 33.0/255.0, blue: 45.0/255.0, alpha: 1.0)
self.window = UIWindow(frame: UIScreen.main.bounds)
let rootViewController = UIViewController()
rootViewController.view = rootView
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
RNSplashScreen.show()
WonderPush.application(application, didFinishLaunchingWithOptions: launchOptions)
UIScreen.main.addObserver(self, forKeyPath: "captured", options: .new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.disableScreenRecording(notification:)), name: Notification.Name("DisableScreenRecording"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.enableScreenRecording(notification:)), name: Notification.Name("EnableScreenRecording"), object: nil)
return true
}
private func launchDidomi(apiKey: String) {
Didomi.shared.setLogLevel(minLevel: OSLogType.info.rawValue)
Didomi.shared.initialize(
apiKey: apiKey,
localConfigurationPath: nil,
remoteConfigurationURL: nil,
providerId: nil,
disableDidomiRemoteConfig: false
)
}
@objc func disableScreenRecording(notification: NSNotification) {
isScreenRecordingEnabled = false
DispatchQueue.main.async {
if self.isCaptured ?? false {
self.rootView?.isHidden = true
}
}
}
@objc func enableScreenRecording(notification: NSNotification) {
isScreenRecordingEnabled = true
}
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// https://firebase.google.com/docs/dynamic-links/ios/receive#open-dynamic-links-in-your-app step 6
let handled = DynamicLinks.dynamicLinks()
.handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in
_ = dynamiclink
_ = error
// no-op
}
return handled || RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler)
}
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// https://firebase.google.com/docs/dynamic-links/ios/receive#open-dynamic-links-in-your-app step 7
if DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) != nil {
return true
}
RCTLinkingManager.application(app, open: url, options: options)
if ApplicationDelegate.shared.application(app, open: url, options: options) {
return true
}
// swiftlint:enable colon
if Adyen.RedirectComponent.applicationDidOpen(from: url) { return true }
return authorizationFlowManagerDelegate?.resumeExternalUserAgentFlow(with: url) ?? false
}
// swiftlint:disable colon
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
// swiftlint:enable colon
WonderPush.application(application,
didReceiveRemoteNotification: userInfo)
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
WonderPush.application(application,
didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
WonderPush.application(application,
didFailToRegisterForRemoteNotificationsWithError: error)
}
// swiftlint:disable colon
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// swiftlint:enable colon
WonderPush.application(application,
didReceiveRemoteNotification: userInfo,
fetchCompletionHandler: completionHandler)
}
func applicationDidBecomeActive(_ application: UIApplication) {
WonderPush.applicationDidBecomeActive(application)
}
func applicationDidEnterBackground(_ application: UIApplication) {
WonderPush.applicationDidEnterBackground(application)
}
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return orientationLock
}
}
How would an AppDelegate.mm
file that includes this functionality would look like?
React Native is experimenting with a new rendering system written in c++. C++ is not directly usable via Swift - and thats why they're recommending using .mm
file. .mm
file is an Objective c++ file which can have c++ code in it. Using Objective c++ is one way to bridge c++ code to Swift (the other is to write a C wrapper). So to initialize Fabric you need to have the AppDelegate written in objective c++.
If you prefer to work in swift, then we can write a base class in objective c++, and override relevant methods in swift. Something like this should work.
Add a new file: AppDelegateBase.h
:
#import <UIKit/UIKit.h>
@interface AppDelegateBase : UIResponder <UIApplicationDelegate>
- (NSDictionary *)prepareInitialProps;
@end
Add another new file: AppDelegateBase.mm
- (Based on AppDelegate.mm from react diff project)
#import "AppDelegateBase.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
#import <React/RCTAppSetupUtils.h>
#if RCT_NEW_ARCH_ENABLED
#import <React/CoreModulesPlugins.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
static NSString *const kRNConcurrentRoot = @"concurrentRoot";
@interface AppDelegateBase () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
RCTTurboModuleManager *_turboModuleManager;
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
facebook::react::ContextContainer::Shared _contextContainer;
}
@end
#endif
@implementation AppDelegateBase
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTAppSetupPrepareApp(application);
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
#if RCT_NEW_ARCH_ENABLED
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
#endif
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window makeKeyAndVisible];
return YES;
}
/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
///
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
/// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`.
- (BOOL)concurrentRootEnabled
{
// Switch this bool to turn on and off the concurrent root
return true;
}
- (NSDictionary *)prepareInitialProps
{
NSMutableDictionary *initProps = [NSMutableDictionary new];
#ifdef RCT_NEW_ARCH_ENABLED
initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]);
#endif
return initProps;
}
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTCxxBridgeDelegate
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:self
jsInvoker:bridge.jsCallInvoker];
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
}
#pragma mark RCTTurboModuleManagerDelegate
- (Class)getModuleClassFromName:(const char *)name
{
return RCTCoreModulesClassProvider(name);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return nullptr;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return RCTAppSetupDefaultModuleFromClass(moduleClass);
}
#endif
@end
Update your AppDelegate.swift
.. Have marked the changes with >>>
.
import UIKit
import Firebase
import Adyen
import GoogleCast
import Didomi
import os
import FBSDKCoreKit
#if DEBUG
// #if FB_SONARKIT_ENABLED
import FlipperKit
// #endif
#endif
@UIApplicationMain
class AppDelegate: AppDelegateBase, // >>> subclassing base class
UNUserNotificationCenterDelegate,
RCTBridgeDelegate,
RNAppAuthAuthorizationFlowManager {
private func initializeFlipper(with application: UIApplication) {
#if DEBUG
// Check if FB_SONARKIT_ENABLED and commented pluggins work in RN 0.67+ versions
// #if FB_SONARKIT_ENABLED
let client = FlipperClient.shared()
let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
// FlipperKitLayoutComponentKitSupport.setUpWith(layoutDescriptorMapper)
client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
client?.add(FKUserDefaultsPlugin(suiteName: nil))
client?.add(FlipperKitReactPlugin())
client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
// client?.add(FlipperReactPerformancePlugin.sharedInstance())
client?.start()
// #endif
#endif
}
func sourceURL(for bridge: RCTBridge!) -> URL! {
#if DEBUG
return RCTBundleURLProvider.sharedSettings()?.jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
// var window: UIWindow? // >>> window created in Base class
var bridge: RCTBridge!
var orientationLock: UIInterfaceOrientationMask = .portrait
var rootView: RCTRootView?
var isScreenRecordingEnabled: Bool?
var isCaptured: Bool?
public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate?
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
_ = localizedString(LocalizationKey(key: "adyen.card.expiryItem.title"), nil)
// Start screen recording in a disabled state
isScreenRecordingEnabled = false
let config = ReactNativeConfig.env()!
WonderPush.setClientId((config["WONDERPUSH_CLIENT_ID"] as? String)!,
secret: (config["WONDERPUSH_CLIENT_SECRET"] as? String)!)
WonderPush.setupDelegateForUserNotificationCenter()
WonderPush.setRequiresUserConsent(false)
WonderPush.setUserConsent(true)
return true
}
// swiftlint:disable block_based_kvo
// swiftlint:disable colon
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if #available(iOS 11.0, *) {
if keyPath == "captured" {
isCaptured = UIScreen.main.isCaptured
if !isScreenRecordingEnabled! {
if isCaptured! {
rootView?.isHidden = true
}
if !isCaptured! && rootView?.isHidden == true {
rootView?.isHidden = false
}
}
}
}
}
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// >>> important to call super.application(_, didFinishLaunchingWithOptions) that is in AppDelegateBase **
super.application(application, didFinishLaunchingWithOptions: launchOptions)
// swiftlint:disable force_cast
launchDidomi(apiKey: ReactNativeConfig.env()!["DIDOMI_API_KEY"] as! String)
FirebaseApp.configure()
initializeFlipper(with: application)
let receiverAppID = kGCKDefaultMediaReceiverApplicationID // or "ABCD1234"
let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
let options = GCKCastOptions(discoveryCriteria: criteria)
GCKCastContext.setSharedInstanceWith(options)
GCKCastContext.sharedInstance().useDefaultExpandedMediaControls = true
self.bridge = RCTBridge(delegate: self, launchOptions: launchOptions)
guard let bridge = self.bridge else {
return false
}
// >>> props created from AppDelegateBase
var props: [AnyHashable: Any] = prepareInitialProps()
props["APP_INFO"] = (Bundle.main.infoDictionary?["APP_INFO"] as? String) ?? ""
props["VERSION"]: (Bundle.main.infoDictionary?["CFBundleShortVersionString"]) ?? ""
rootView = RCTRootView(bridge: bridge, moduleName: "PSG", initialProperties: props)
rootView?.backgroundColor = UIColor(red: 22.0/255.0, green: 33.0/255.0, blue: 45.0/255.0, alpha: 1.0)
// >>> self.window is created in AppDelegateBase
let rootViewController = UIViewController()
rootViewController.view = rootView
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
RNSplashScreen.show()
WonderPush.application(application, didFinishLaunchingWithOptions: launchOptions)
UIScreen.main.addObserver(self, forKeyPath: "captured", options: .new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.disableScreenRecording(notification:)), name: Notification.Name("DisableScreenRecording"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.enableScreenRecording(notification:)), name: Notification.Name("EnableScreenRecording"), object: nil)
return true
}
private func launchDidomi(apiKey: String) {
Didomi.shared.setLogLevel(minLevel: OSLogType.info.rawValue)
Didomi.shared.initialize(
apiKey: apiKey,
localConfigurationPath: nil,
remoteConfigurationURL: nil,
providerId: nil,
disableDidomiRemoteConfig: false
)
}
@objc func disableScreenRecording(notification: NSNotification) {
isScreenRecordingEnabled = false
DispatchQueue.main.async {
if self.isCaptured ?? false {
self.rootView?.isHidden = true
}
}
}
@objc func enableScreenRecording(notification: NSNotification) {
isScreenRecordingEnabled = true
}
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
// https://firebase.google.com/docs/dynamic-links/ios/receive#open-dynamic-links-in-your-app step 6
let handled = DynamicLinks.dynamicLinks()
.handleUniversalLink(userActivity.webpageURL!) { dynamiclink, error in
_ = dynamiclink
_ = error
// no-op
}
return handled || RCTLinkingManager.application(
application,
continue: userActivity,
restorationHandler: restorationHandler)
}
func application(_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
// https://firebase.google.com/docs/dynamic-links/ios/receive#open-dynamic-links-in-your-app step 7
if DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) != nil {
return true
}
RCTLinkingManager.application(app, open: url, options: options)
if ApplicationDelegate.shared.application(app, open: url, options: options) {
return true
}
// swiftlint:enable colon
if Adyen.RedirectComponent.applicationDidOpen(from: url) { return true }
return authorizationFlowManagerDelegate?.resumeExternalUserAgentFlow(with: url) ?? false
}
// swiftlint:disable colon
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
// swiftlint:enable colon
WonderPush.application(application,
didReceiveRemoteNotification: userInfo)
}
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
WonderPush.application(application,
didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
WonderPush.application(application,
didFailToRegisterForRemoteNotificationsWithError: error)
}
// swiftlint:disable colon
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// swiftlint:enable colon
WonderPush.application(application,
didReceiveRemoteNotification: userInfo,
fetchCompletionHandler: completionHandler)
}
func applicationDidBecomeActive(_ application: UIApplication) {
WonderPush.applicationDidBecomeActive(application)
}
func applicationDidEnterBackground(_ application: UIApplication) {
WonderPush.applicationDidEnterBackground(application)
}
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return orientationLock
}
}
This should automatically create a Bridging header for your project. Add this to your bridging header:
#include "AppDelegateBase.h"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With