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

SceneDelegate - Linking getInitialURL() and addEventListener() don't work on iOS

Open mikeleruo opened this issue 1 year ago • 12 comments

Description

I thank you for the work you are doing with React Native.

Recently in one of my projects I correctly integrated the Linking.getInitialUrl and Linking.addEventListener APIs.

However out of necessity I had to modify AppDelegate.m and introduce AppDelegate.swift handling (+ adding a SceneDelegate).

There are two scenes PhoneScene and CarPlayScene, this was necessary for the introduction of CarPlay within the project.

Since I made these changes on the native side, the functions related to Linking have stopped working completely, always returning null.

What we have been able to understand is that with the introduction from iOS 13 of SceneDelegates the initial app launch parameters (launchOptions in iOS) and subsequent calls from background mode are no longer handled by AppDelegate.swift but by SceneDelegate. In our case in PhoneScene.swift.

Expected Behavior

I expect to get the initial URL called via deepLink via getInitialUrl or listener handler.

Version

0.68.2

Output of npx react-native info

System:
    OS: macOS 11.6.5
    CPU: (8) x64 Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
    Memory: 79.48 MB / 16.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 14.19.1 - ~/.nvm/versions/node/v14.19.1/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 6.14.16 - ~/.nvm/versions/node/v14.19.1/bin/npm
    Watchman: 2022.03.21.00 - /usr/local/bin/watchman
  Managers:
    CocoaPods: 1.11.3 - /usr/local/bin/pod
  SDKs:
    iOS SDK:
      Platforms: DriverKit 21.0.1, iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0
    Android SDK: Not Found
  IDEs:
    Android Studio: 2021.1 AI-211.7628.21.2111.8092744
    Xcode: 13.1/13A1030d - /usr/bin/xcodebuild
  Languages:
    Java: 11.0.16 - /usr/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 17.0.2 => 17.0.2 
    react-native: 0.68.2 => 0.68.2 
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found 

Steps to reproduce

1. Remove previous ObjC files

  • Delete main.m
  • Delete AppDelegate.h and AppDelegate.mm

2. Create the new Swift AppDelegate

Create a new AppDelegate.swift file, once prompted, click Create bridging headers for Swift.

Paste the following code to the AppDelegate.swift, it includes Flipper code.

// ios/AppDelegate.swift
import Foundation
import UIKit
import CarPlay
import React
#if DEBUG
#if FB_SONARKIT_ENABLED
import FlipperKit
#endif
#endif

@main
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
  var window: UIWindow?
  var bridge: RCTBridge?;
  var rootView: RCTRootView?;
  static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }

  func sourceURL(for bridge: RCTBridge!) -> URL! {
    #if DEBUG
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackURLProvider:nil);
    #else
    return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
     #endif
  }

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    initializeFlipper(with: application)
    print("launchOptions", launchOptions?[UIApplication.LaunchOptionsKey.url])
    self.bridge = RCTBridge.init(delegate: self, launchOptions: launchOptions)
    self.rootView = RCTRootView.init(bridge: self.bridge!, moduleName: "PROJECT_NAME", initialProperties: nil)
    
    RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView);
    
    return true
  }

  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    print("connectionScene")
    if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) {
      let scene =  UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role)
      scene.delegateClass = CarSceneDelegate.self
      return scene
    } else {
      let scene =  UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role)
      scene.delegateClass = PhoneSceneDelegate.self
      return scene
    }
  }

  func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
  }

  private func initializeFlipper(with application: UIApplication) {
    #if DEBUG
    #if FB_SONARKIT_ENABLED
    let client = FlipperClient.shared()
    let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
    client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
    client?.add(FKUserDefaultsPlugin(suiteName: nil))
    client?.add(FlipperKitReactPlugin())
    client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
    client?.start()
    #endif
    #endif
  }
}

Paste the following to your bridging header file ProjectName-Bridging-Header.h:

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

#import <React/RCTLinkingManager.h>
#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 "RNCarPlay.h"
#import "RNBootSplash.h"
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>

#ifdef DEBUG
#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
#endif
#endif

3. Add flags for Swift compiler in debug mode

Go to XCode project, hit Build Settings, search for Swift Compiler - Custom Flags and then under Active Compilation Conditions, add the following flags to Debug only:

  • DEBUG
  • FB_SONARKIT_ENABLED

4. Create Phone Scene

Add a new Swift file called PhoneScene.swift:

// ios/PhoneScene.swift
import Foundation
import UIKit
import SwiftUI
class PhoneSceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?
  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let appDelegate = (UIApplication.shared.delegate as? AppDelegate) else { return }
    guard let windowScene = (scene as? UIWindowScene) else { return }
    let rootViewController = UIViewController()
    rootViewController.view = appDelegate.rootView;
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = rootViewController
    self.window = window
    window.makeKeyAndVisible()
  }
}

5. Create Car Scene

Add a new Swift file called CarScene.swift

// ios/CarScene.swift
import Foundation
import CarPlay
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
  func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                  didConnect interfaceController: CPInterfaceController) {
    RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow);
  }
  func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
    RNCarPlay.disconnect()
  }
}

6. Update the manifest in Info.plist

Add the following ApplicationScene manifest to your Info.plist file.

	<key>UIApplicationSceneManifest</key>
	<dict>
		<key>UIApplicationSupportsMultipleScenes</key>
		<true/>
		<key>UISceneConfigurations</key>
		<dict>
			<key>CPTemplateApplicationSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneClassName</key>
					<string>CPTemplateApplicationScene</string>
					<key>UISceneConfigurationName</key>
					<string>CarPlay</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_MODULE_NAME).CarSceneDelegate</string>
				</dict>
			</array>
			<key>UIWindowSceneSessionRoleApplication</key>
			<array>
				<dict>
					<key>UISceneClassName</key>
					<string>UIWindowScene</string>
					<key>UISceneConfigurationName</key>
					<string>Phone</string>
					<key>UISceneDelegateClassName</key>
					<string>$(PRODUCT_MODULE_NAME).PhoneSceneDelegate</string>
				</dict>
			</array>
		</dict>
	</dict>

Snack, code example, screenshot, or link to a repository

Implement all delegates in AppDelegate and PhoneScene to capture URLs in the following cases:

  1. uninstall app -> click deep link -> install app -> open app
  2. terminate app -> click deep link. -> open app
  3. background mode -> click deep link -> open app

PhoneScene.swift

// ios/PhoneScene.swift

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    //...
    
    // <---- either one will work ---->
    print("connectionOptions.urlContexts.first?.url", connectionOptions.urlContexts.first?.url)
    print("connectionOptions.userActivities.first?.webpageURL", connectionOptions.userActivities.first?.webpageURL)
  }
  
  func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    // <---- first launch after install ---->
    print("URLContexts", URLContexts.first?.url)
  }

  func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    // <---- when in background mode ---->
    print("userActivity", userActivity.webpageURL)
  }

AppDelegate.swift

// ios/AppDelegate.swift
import Foundation
import UIKit
import CarPlay
import React
#if DEBUG
#if FB_SONARKIT_ENABLED
import FlipperKit
#endif
#endif

@main
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
 // ...

  func application(_ application: UIApplication, continue userActivity: NSUserActivity,
   restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    print("userActivity.webpageURL", userActivity.webpageURL)
    return true;
  }
  func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool{
    // <---- first launch after install ---->
    print("first launch after install", url)
    return true;
  }
  func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool{
    // <---- first launch after install for older iOS version ---->
    print("first launch after install for older iOS version", url)
    return true;
  }

}

N.B: I have already tried using RCTLinkingManager but these functions in AppDelegate are not invoked, everything is captured by PhoneScene. I cannot understand how to fix this from a UIScene.


// ios/AppDelegate.swift

import Foundation
import UIKit
import CarPlay
import React
#if DEBUG
#if FB_SONARKIT_ENABLED
import FlipperKit
#endif
#endif

@main
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
 // ...

  func application(_ application: UIApplication, continue userActivity: NSUserActivity,
   restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    print("userActivity.webpageURL", userActivity.webpageURL)
   return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler);
  }
  func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool{
    // <---- first launch after install ---->
    print("first launch after install", url)
     return RCTLinkingManager.application(application, open: url, options: options);
  }
  func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool{
    // <---- first launch after install for older iOS version ---->
     return RCTLinkingManager.application(application, open: url, sourceApplication: sourceApplication, annotation: annotation)
  }

}

mikeleruo avatar Nov 03 '22 14:11 mikeleruo

Did you already find a solution for this? We are also using a SceneDelegate and we currently also don't know how to use RCTLinkingManager with a UIScene.

JensDeTaey27 avatar Nov 22 '22 09:11 JensDeTaey27

This worked for us.

UIWindowSceneDelegate:

  func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    URLContexts.forEach({ context in
      RCTLinkingManager.application(UIApplication.shared, open: context.url)
    })
  }

tayfunmavzer avatar Dec 01 '22 10:12 tayfunmavzer

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] avatar May 31 '23 05:05 github-actions[bot]

hi, is there a solution to this problem?

alex-vasylchenko avatar Jul 11 '23 15:07 alex-vasylchenko

any one got any solution ?

harrymash2006 avatar Oct 12 '23 11:10 harrymash2006

Any solution?

MatheusLima7 avatar Mar 12 '24 00:03 MatheusLima7

Did anybody find solution to this bug? Even my project has same case, I have moved all life-cycle methods from AppDelegate to SceneDelegate.swift and

Linking.getInitialURL() always return "null" when app is launched from terminated state. It still works when reverted back to AppDelegate but that's not the solution for me unfortunately.

//willConnectTo method from SceneDelegate

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
   var deeplink: URL?
   if let userActivity = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }),
              let webpageURL = userActivity.webpageURL {
              // get universal link
              deeplink = webpageURL
          } else if let urlContext = connectionOptions.urlContexts.first {
              // get app scheme deep link
              deeplink = urlContext.url
          }
          handleDeepLink(deeplink)
  }

//handels app scheme myapp:// in active and inactive foreground mode

  func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
      if let url = URLContexts.first?.url {
        handleDeepLink(url)
      }
  }

//handels universal links https:// in active and inactive foreground mode

  func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
    if let url = userActivity.webpageURL {               
        handleDeepLink(url)
      }
    }
  }

//function to pass deelink value to react native

  func handleDeepLink(_ deeplink: URL?) {
      guard let deeplink = deeplink else {
          os_log("No deeplink found", log: OSLog.default, type: .debug)
          return
      }
      os_log("Deeplink URL FOUND: %@", log: OSLog.default, type: .debug, deeplink.absoluteString) //Prints Deeplink value
      RCTLinkingManager.application(UIApplication.shared, open: deeplink, options: [:]) // -->Works in background mode but doesn't in terminated / app killed state 
  }

os_log prints the deeplink value when app is launched from terminated state by pressing the deeplink scheme "myapp://" But the value is always "null" when it;s received here Linking.getInitialURL()

Same code works as expected in background and deeplink value is received from Linking.addEventListener

I have tried everything from the internet and all the sources nothing has worked until now.

akash-rouniyar avatar Apr 02 '24 07:04 akash-rouniyar

hi, check my example with solution https://github.com/alex-vasylchenko/react-native-carpaly-example he problem was solved using the connectionOptionsToLaunchOptions function

alex-vasylchenko avatar Apr 02 '24 07:04 alex-vasylchenko

hi, check my example with solution https://github.com/alex-vasylchenko/react-native-carpaly-example he problem was solved using the connectionOptionsToLaunchOptions function

@alex-vasylchenko can you please provide entire source code? I'd like to run the code locally to test it and apply on my project.

It's that true that lifecycle methods from AppDelegate's connectionOptionsToLaunchOptions need to be moved to SceneDelegate when Scene are introduced?

I'll really appreciate your advise.

akash-rouniyar avatar Apr 02 '24 07:04 akash-rouniyar

@akash-rouniyar this code is working, I've been using it in production for several months now. You don't need to change anything there because you have appDelegate.initAppFromScene(connectionOptions: connectionOptions) in PhoneScene

alex-vasylchenko avatar Apr 02 '24 07:04 alex-vasylchenko

@alex-vasylchenko is it possible to get in touch with you google meets or anywhere you prefer? I'm getting bunch of other errors when I apply your solution. I'm stuck on this bug for couple of weeks now and I cannot find any solutions anywhere. I really appreciate your willingness to help.

akash-rouniyar avatar Apr 02 '24 08:04 akash-rouniyar

@akash-rouniyar are you using react-native-carplay library? if so, join the Discord channel https://discord.com/invite/b235pv6QHM

alex-vasylchenko avatar Apr 02 '24 08:04 alex-vasylchenko