iosswiftreact-nativedeep-linking

Deeplinking not navigating to expected location if app is removed from memory in swift iOS


I am new to ReactNative. In my current app, deeplinking was coded and working well when it was done through appDelegate. later, the app support Carplay as well, so to handle multiple scenes sceneDelegate was introduced.

After that, whenever deeplinking is invoked then

What is the issue here, what things I need to change / add to make app launch at desired location even if app is removed from memory.

Here is my appDelegate.

func appDelegate() -> AppDelegate {
    return UIApplication.shared.delegate as! AppDelegate
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var appCenter: AppCenterReactNative!
  var appCenterAnaltics: AppCenterReactNativeAnalytics!
  var appCenterCrashes: AppCenterReactNativeCrashes!
  let mParticleKey: String = ReactNativeConfig.env(for: "MPARTICLE_IOS_KEY");
  let mParticleSecret: String = ReactNativeConfig.env(for: "MPARTICLE_IOS_SECRET");
  // var mParticleEmail: String = ReactNativeConfig.env(for: "MPARTICLE_EMAIL");
  let mParticleEnv: String = ReactNativeConfig.env(for: "MPARTICLE_ENV");
  let mParticleDataPlanName: String = ReactNativeConfig.env(for: "MPARTICLE_DATAPLAN");
  let mParticleDataPlanVersion: String = ReactNativeConfig.env(for: "MPARTICLE_DATAPLAN_VERSION");
  let moEngageAppID: String = ReactNativeConfig.env(for: "MOENGAGE_APP_ID");
  
  /* CarPlay setup */
  var playableContentManager: MPPlayableContentManager?
  var remoteCommandCenter: MPRemoteCommandCenter?
  let carplayPlaylist = CarPlayPlaylist()
  let carplayArtworkCache = NSCache<AnyObject, UIImage>()
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    initializeFlipper(with: application)

    /* Moengage */
    let sdkConfig = MoEngageSDKConfig(appId: moEngageAppID, dataCenter: .data_center_01);
    MoEngageInitializer.sharedInstance().initializeDefaultSDKConfig(sdkConfig, andLaunchOptions: launchOptions ?? [:])
    
    AppCenterReactNative.register()
    AppCenterReactNativeAnalytics.register(withInitiallyEnabled: true);
    AppCenterReactNativeCrashes.registerWithAutomaticProcessing();

    FirebaseApp.configure()
        
    /* ChromeCast activate */
    let receiverAppID:String = "CC1AD845"; // or @"ABCD1234"
    let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)
    
    let bridge = RCTBridge(delegate: self, launchOptions: launchOptions)!
    let rootView = RCTRootView(bridge: bridge, moduleName: "nova", initialProperties: nil)
    
    let rootViewController = UIViewController()
    rootViewController.view = rootView

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

    /* MPNowPlayingInfoCenter */
    UIApplication.shared.beginReceivingRemoteControlEvents()

    setupCarPlay();

    RNSplashScreen.show()

    /* Setup MParticle */
    var dPlanVersion:NSNumber = 0
    if let versionInt = Int(mParticleDataPlanVersion) {
     dPlanVersion = NSNumber(value:versionInt)
    }

    var mParticleEnvMode: MPEnvironment = MPEnvironment.development
    if(mParticleEnv == "PROD") {
      mParticleEnvMode =  MPEnvironment.production
    }
    let mParticleOptions = MParticleOptions(key: mParticleKey, secret: mParticleSecret)
    mParticleOptions.environment = mParticleEnvMode
    mParticleOptions.dataPlanId = mParticleDataPlanName
    mParticleOptions.dataPlanVersion = dPlanVersion
    mParticleOptions.proxyAppDelegate = false
    if #available(iOS 14, *) {
       mParticleOptions.attStatus = NSNumber.init(value: ATTrackingManager.trackingAuthorizationStatus.rawValue)
    }
    
    // Remove AST Events
    mParticleOptions.onCreateBatch = { (batch: [AnyHashable: Any]) -> [AnyHashable: Any]? in
        var modifiedBatch = batch
        guard var modifiedMessages = batch["msgs"] as? [AnyHashable] else { return batch }
        var index = 0
        for message in modifiedMessages {
            // the following removes Application State Transition (AST) events, except for those uploaded on installs and upgrades
            // Install AST events are used by many server-side integrations and are used by
            // mParticle to ensure there is a user profile created
            guard let messageAsDictionary = message as? [AnyHashable: Any] else { continue }
            guard let type = messageAsDictionary["dt"] as? String else { continue }
            let isFirstRun = messageAsDictionary["ifr"] as? Bool ?? false
            let isUpgrade = messageAsDictionary["iu"] as? Bool ?? false
            if type == "ast" && !isFirstRun && !isUpgrade {
                modifiedMessages.remove(at: index)
                index -= 1
            }
            index += 1
        }
        modifiedBatch["msgs"] = modifiedMessages
        return modifiedBatch
    }
    // Start the SDK
    MParticle.sharedInstance().start(with: mParticleOptions)
    
    return true
  } 

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

  /* Allow for orientation change */
  func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return Orientation.getOrientation()
  }

  /* Allow Link back URLs ('nova://') */
  public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    return RCTLinkingManager.application(app, open: url, options: options)
  }
  
  public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
  }
}

extension AppDelegate: RCTBridgeDelegate {
    func sourceURL(for bridge: RCTBridge!) -> URL! {
        #if DEBUG
        return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
        #else
        return CodePush.bundleURL()
        #endif
    }
}

This is my sceneDelegate

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate, RCTBridgeDelegate {

  let mParticleKey: String = ReactNativeConfig.env(for: "MPARTICLE_IOS_KEY");
  let mParticleSecret: String = ReactNativeConfig.env(for: "MPARTICLE_IOS_SECRET");
  // var mParticleEmail: String = ReactNativeConfig.env(for: "MPARTICLE_EMAIL");
  let mParticleEnv: String = ReactNativeConfig.env(for: "MPARTICLE_ENV");
  let mParticleDataPlanName: String = ReactNativeConfig.env(for: "MPARTICLE_DATAPLAN");
  let mParticleDataPlanVersion: String = ReactNativeConfig.env(for: "MPARTICLE_DATAPLAN_VERSION");


  func sourceURL(for bridge: RCTBridge!) -> URL! {
    let jsCodeLocation: URL
    jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
    return jsCodeLocation
  }
  
  var window: UIWindow?
  
  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)
    
    let bridge = RCTBridge.init(delegate: self, launchOptions: nil)
    let rootView = RCTRootView.init(bridge: bridge!, moduleName: "nova", initialProperties: nil)
      
    let rootViewController = UIViewController()
    rootViewController.view = rootView

    

    AppCenterReactNative.register()
    AppCenterReactNativeAnalytics.register(withInitiallyEnabled: true);
    AppCenterReactNativeCrashes.registerWithAutomaticProcessing();

    FirebaseApp.configure()

    /* ChromeCast activate */
    let receiverAppID:String = "CC1AD845"; // or @"ABCD1234"
    let criteria = GCKDiscoveryCriteria(applicationID: receiverAppID)
    let options = GCKCastOptions(discoveryCriteria: criteria)
    GCKCastContext.setSharedInstanceWith(options)
    
    /* MPNowPlayingInfoCenter */
    UIApplication.shared.beginReceivingRemoteControlEvents()

    RNSplashScreen.show()

    // Instantiate root view here instead of scene to start the bundler on app launch
    RNBridgeInstanceHolder.sharedInstance.bridge = bridge
    RNBridgeInstanceHolder.sharedInstance.rctRootView = rootView
    
    if let windowScene = scene as? UIWindowScene {
       let window = UIWindow(windowScene: windowScene)
      window.rootViewController = rootViewController
       self.window = window
       window.makeKeyAndVisible()
    }

    if #unavailable(iOS 14.0) {
      appDelegate().setupCarPlay()
    }

    /* Setup MParticle */
    var dPlanVersion:NSNumber = 0
    if let versionInt = Int(mParticleDataPlanVersion) {
     dPlanVersion = NSNumber(value:versionInt)
    }

    var mParticleEnvMode: MPEnvironment = MPEnvironment.development
    if(mParticleEnv == "PROD") {
      mParticleEnvMode =  MPEnvironment.production
    }
    let mParticleOptions = MParticleOptions(key: mParticleKey, secret: mParticleSecret)
    mParticleOptions.environment = mParticleEnvMode
    mParticleOptions.dataPlanId = mParticleDataPlanName
    mParticleOptions.dataPlanVersion = dPlanVersion
    mParticleOptions.proxyAppDelegate = false
    if #available(iOS 14, *) {
       mParticleOptions.attStatus = NSNumber.init(value: ATTrackingManager.trackingAuthorizationStatus.rawValue)
    }
    
    // Remove AST Events
    mParticleOptions.onCreateBatch = { (batch: [AnyHashable: Any]) -> [AnyHashable: Any]? in
        var modifiedBatch = batch
        guard var modifiedMessages = batch["msgs"] as? [AnyHashable] else { return batch }
        var index = 0
        for message in modifiedMessages {
            // the following removes Application State Transition (AST) events, except for those uploaded on installs and upgrades
            // Install AST events are used by many server-side integrations and are used by
            // mParticle to ensure there is a user profile created
            guard let messageAsDictionary = message as? [AnyHashable: Any] else { continue }
            guard let type = messageAsDictionary["dt"] as? String else { continue }
            let isFirstRun = messageAsDictionary["ifr"] as? Bool ?? false
            let isUpgrade = messageAsDictionary["iu"] as? Bool ?? false
            if type == "ast" && !isFirstRun && !isUpgrade {
                modifiedMessages.remove(at: index)
                index -= 1
            }
            index += 1
        }
        modifiedBatch["msgs"] = modifiedMessages
        return modifiedBatch
    }
    // Start the SDK
    MParticle.sharedInstance().start(with: mParticleOptions)
      
  }

  //handels app scheme novaplayer:// 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://novaplayer 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)
      RCTLinkingManager.application(UIApplication.shared, open: deeplink, options: [:])
  }

}

Here is my info.plist piece of code.

<key>UIApplicationSceneManifest</key>
    <dict>
        <key>UISceneConfigurations</key>
        <dict>
            <key>CPTemplateApplicationSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneClassName</key>
                    <string>CPTemplateApplicationScene</string>
                    <key>UISceneConfigurationName</key>
                    <string>CarPlay Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
                </dict>
            </array>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                </dict>
            </array>
        </dict>
    </dict>

It is working as expected in Android build, but having issue only with iOS build. If I switch back to appDelegate without scenedelegate then my Carplay app is not getting launched.

Any help will be appreciated.

Thanks.


Solution

  • I sort it out with the help of @sonle

    by adding time to execute the deeplink, sorted the issue.

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                handleDeepLink(deeplink)
            }
    

    Thanks for guidance Sonle.