react-native
react-native copied to clipboard
SceneDelegate - Linking getInitialURL() and addEventListener() don't work on iOS
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.
- I have already tried running the program without debug mode enabled.
- I am sure that the link method is called when the application is loaded
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
andAppDelegate.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:
- uninstall app -> click deep link -> install app -> open app
- terminate app -> click deep link. -> open app
- 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)
}
}
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.
This worked for us.
UIWindowSceneDelegate
:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
URLContexts.forEach({ context in
RCTLinkingManager.application(UIApplication.shared, open: context.url)
})
}
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.
hi, is there a solution to this problem?
any one got any solution ?
Any solution?
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.
hi, check my example with solution https://github.com/alex-vasylchenko/react-native-carpaly-example
he problem was solved using the connectionOptionsToLaunchOptions
function
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 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 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 are you using react-native-carplay library? if so, join the Discord channel https://discord.com/invite/b235pv6QHM