Branch.io link for iOS 9+ issues - ios

I used Branch.io for my project to launch app from my website. What I want is if app is off, when I click the universal link on web it will open HomePage first. In contrast, it opens another. It works well with iOS 8. But in iOS 9+, it always open the LaunchScreen.
Please, take a look at my code:
AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
let branch = Branch.getInstance()
branch.initSessionWithLaunchOptions(launchOptions) { (params, error) -> Void in
DHIndicator.hide()
if let _ = params {
print("kdlkasdlf: \(params.debugDescription)")
if let str = params["$deeplink_path"], url = NSURL(string: str as! String) {
NSLog("link: \(url)")
self.path = url.path
self.query = url.query
LaunchAppFlowManager.shareInstance.displayLaunchDetails()
} else {
// load your normal view
}
}
}
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.makeKeyAndVisible()
window?.backgroundColor = UIColor.whiteColor()
AccountFlowManager.shareInstance.start()
}
func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool {
Branch.getInstance().handleDeepLink(url)
if AppDelegate.shareInstance().window?.rootViewController?.nibName != "LaunchScreenVC" {
DHIndicator.show()
}
return true
}
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
print(userActivity.webpageURL?.absoluteString)
if AppDelegate.shareInstance().window?.rootViewController?.nibName != "LaunchScreenVC" {
DHIndicator.show()
}
return Branch.getInstance().continueUserActivity(userActivity)
}
And, func to start app:
if let _ = AppDelegate.shareInstance().path, _ = AppDelegate.shareInstance().query {
let navi = UINavigationController(rootViewController: HomePageVC())
self.navi = navi
AppDelegate.shareInstance().window?.rootViewController = navi
} else {
let vc = LaunchScreenVC()
AppDelegate.shareInstance().window?.rootViewController = vc
}

To summarize the issue:
On iOS 8, when a Branch Link is clicked the app opens and shows
the correct ViewController
On iOS 9, when a Branch Link is
clicked the app opens but the default ViewController is shown
The issue in this case is related to Universal Linking. Previously, using URI Schemes on iOS 8, you would route within your app using $deeplink_path. Rather than use $deeplink_path, with Universal Links Branch now recommends using link data stored in custom fields to inform in-app routing decisions, as described here: https://dev.branch.io/getting-started/deep-link-routing/advanced/ios/#building-a-custom-deep-link-routing-method
Additionally, it is important to locate the code used for in-app routing within the Branch init completion block, as this completion block is only run after the response from Branch's servers have been received and the link parameters are available. If the code for in-app routing is located outside the completion block (and not called by the completion block), it is likely that the routing decision will be made before link parameters have actually been returned by Branch.

Related

How to wait for didFinishLaunchingWithOptions to finish before handling universal link?

In my Swift iOS app I asynchronously set up the initial view controller in the AppDelegate inside didFinishLaunchingWithOptions. Now I'm adding a Universal Link such that if you go to the corresponding URL on your phone it will automatically open the right page in the app (in this case it's a password reset URL that, if correct, will open the password reset form inside the app).
I am handling the linking inside application(_:continue:restorationHandler:) (continueUserActivity) in my AppDelegate.
It works if the app is already running but otherwise the link handling happens before my app has finished setting up the initial view controller and the view controller for the deep link gets dismissed when didFinishLaunchingWithOptions returns.
How can I ensure the app is finished setting up before I present view controllers from continueUserActivity? I can't/shouldn't prevent didFinishLaunching from returning until the view controllers are set up, so the method returns before the initial view controller is presented.
I know I can check the user activity dictionary inside didFinishLaunchingWithOptions but continueUserActivity is still called (it would be much simpler if it was only called if the app was running, like how didReceiveRemoteNotification works). Is there a condition to check inside this method to defer the link handling to didFinishLaunching?
Some simplified UIApplicationDelegate code to help explain:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
doSomeAsyncSetup(
completion: {
presentSomeViewController()
}
)
return true
}
func application(_: UIApplication, continue userActivity: NSUserActivity, restorationHandler _: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems,
components.path == pathThatIExpect
{
handleUniversalLink(params)
}
return true
}
Note there seems to be a slightly different distinction when the app has opted into Scenes but my app is not using that (https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app).
A DispatchGroup should be able to handle this for you.
private let dispatchGroup = DispatchGroup()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.dispatchGroup.enter()
doSomeAsyncSetup(
completion: {
presentSomeViewController()
self.dispatchGroup.leave()
}
)
return true
}
func application(_: UIApplication, continue userActivity: NSUserActivity, restorationHandler _: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems,
components.path == pathThatIExpect
{
self.dispatchGroup.notify {
handleUniversalLink(params)
}
}
return true
}
In the case where your app is being launched, the dispatch group is entered in didFinishLaunching and it is left once the view controller is presented.
Once the dispatch group is empty, the notify will execute the user activity code; so only after your view controller is presented.
In the case where your app is already launched, the dispatch group will be empty since didFinishLaunching isn't called and so the notify will execute immediately
You can use serial queue for that. For example:
serialQueue.async {
self.presentSomeViewController()
}
And
serialQueue.async {
self.handleUniversalLink(params)
}

3D Touch quick actions not working when called from didFinishLaunchingWithOptions

I'm implementing 3D touch quick actions in my app and I have the following problem:
When the app is already running and so the quick action is initiated through perform Action For Shortcut Item, it works perfectly fine. However, when the app is killed and then launched through a quick action (so didFinishLaunchingWithOptions) it does not take me to the desired view controller, but rather to the home screen of the app.
Here is my code:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//... other stuff I'm doing
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
shortcutItemToProcess = shortcutItem
}
return true
NOTE: I've read previous SO answers where they said that I need to return false in didFinishLaunchingWithOptions when the app was launched through a quick action, so that performAction won't get called. I need to always return true however in my didFinishLaunching method because of the other things I'm handling there. I tried however to return false just to see if that causes the problem and the app still behaved the same way.
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
shortcutItemToProcess = shortcutItem
}
Here is how I present the view controller:
func applicationDidBecomeActive(_ application: UIApplication) {
if let shortcutItem = shortcutItemToProcess {
if shortcutItem.type == "com.myName.MyApp.myQuickAction" {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myViewController = storyboard.instantiateViewController(withIdentifier: "myViewController") as! MyViewController
if let navVC = window?.rootViewController as! UINavigationController? {
navVC.pushViewController(myViewController, animated: true)
}
}
So this works fine when app is already running, but it lands me on the home page of my app when the app is killed. What am I doing wrong and how can I solve this?

How do I know if app was launched via Firebase-Deeplink (Dynamic Links) at AppDelegate's didFinishLaunchingWithOptions

We are adding Firebase-Deeplinks to our IOS-project, so that the app can be started via deeplink.
The deeplink-feature itself work fine so far, and so does the default app launch routine. But making both startRoutines work side by side gives me some headache.
What I am trying to achieve get's obvious looking at this code snippet.
func application(_:didFinishLaunchingWithOptions:) {
FirebaseApp.configure()
if "deeplink" {
return true
}
defaultAppLaunch() // no deeplink
return true
}
If there is a deeplink one of these appDelegate-functions is called:
func application(:continueUserActivity:restorationHandler:) {
handleDeeplink()
return true
}
func application(:openURL:options:) {
handleDeeplink()
return true
}
So how do I know at application(_:didFinishLaunchingWithOptions:) if I can call defaultAppLaunch()?
I know there is the launchOptions-Argument in but in my case it is always nil, at least when running the app via XCode. And also the Firebase-Documentation says nothing about launchOptions to be set by Firebase-Deeplinks.
Help is highly appreciated.
TL;DR
You can't know that your app was opened using deeplinks through App Delegate DidFinishLaunching.
Explaination:
App delegate did finish launch is always called, regardless if app was opened normally or via deeplinks. so you can't know through app delegate
Instead, you can know that app was opened through deeplinks if the following delegate function is called.
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) {
// Handle the deep link. For example, show the deep-linked content or
// apply a promotional offer to the user's account.
// ...
return true
}
return false
}
and you should handle the deeplinks functionality in the same function
I'm referencing the Firebase docs in handling dynamic links for iOS:
Firebase docs for receiving dynamic links
Next, in the application:continueUserActivity:restorationHandler:
method, handle links received as Universal Links when the app is
already installed (on iOS 9 and newer):
func application(_ application: UIApplication, continue userActivity: NSUserActivity,
restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
let handled = DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { (dynamiclink, error) in
// ...
}
return handled
}
Finally, in the application:openURL:sourceApplication:annotation: (iOS
8 and older) and application:openURL:options: (iOS 9 and up) methods,
handle links received through your app's custom URL scheme. These
methods are called when your app receives a link on iOS 8 and older,
and when your app is opened for the first time after installation on
any version of iOS.
#available(iOS 9.0, *)
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool {
return application(app, open: url,
sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String,
annotation: "")
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) {
// Handle the deep link. For example, show the deep-linked content or
// apply a promotional offer to the user's account.
// ...
return true
}
return false
}
But you did mention that the app is currently only being run on Xcode (and I'm guessing iOS Simulator, maybe you can try it on a test device too!)

Firebase Deeplink not calling application:continueUserActivity:restorationHandler function of AppDelegate in Swift 3

I am using firebase Deeplink URL to open my app's specific section. It is working well when app running in background but when I killed the app and click deeplink url from outside than I don't know how to handle that case, I mean where I should write my condition to get the parameters of url.
This method of app delegate called when app in background
#available(iOS 8.0, *)
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
guard let dynamicLinks = DynamicLinks.dynamicLinks() else {
return false
}
let handled = dynamicLinks.handleUniversalLink(userActivity.webpageURL!) { (dynamiclink, error) in
if let dynamicLink = dynamiclink, let _ = dynamicLink.url
{
var path = dynamiclink?.url?.path
path?.remove(at: (path?.startIndex)!)
let delimiter = "/"
var fullNameArr = path?.components(separatedBy: delimiter)
let type: String = fullNameArr![0]
let Id: String? = (fullNameArr?.count)! > 1 ? fullNameArr?[1] : nil
if(type == "games")
{
self.callGameDetailView(gameId: Id! , gameType: "created", notificationId: "NIL" )
}else{
var paths = dynamicLink.url?.path
paths?.remove(at: (path?.startIndex)!)
self.setRewardViewController(paths!)
}
} else {
// Check for errors
}
}
return handled
}
but it will not call open url method when I killed the app and hit the dynamic link url:
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any])
-> Bool {
return self.application(application, open: url, sourceApplication: nil, annotation: [:])
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
let dynamicLink = DynamicLinks.dynamicLinks()?.dynamicLink(fromCustomSchemeURL: url)
if let dynamicLink = dynamicLink
{
var path = dynamicLink.url?.path
path?.remove(at: (path?.startIndex)!)
let delimiter = "/"
var fullNameArr = path?.components(separatedBy: delimiter)
let type: String = fullNameArr![0]
let Id: String? = (fullNameArr?.count)! > 1 ? fullNameArr?[1] : nil
if(type == "games")
{
self.callGameDetailView(gameId: Id! , gameType: "created", notificationId: "NIL" )
}else{
var paths = dynamicLink.url?.path
paths?.remove(at: (path?.startIndex)!)
self.setRewardViewController(paths!)
return true
}
}
return FBSDKApplicationDelegate.sharedInstance().application(
application,
open: url,
sourceApplication: sourceApplication,
annotation: annotation)
}
In short I have no Idea how to handle the dynamic link when app runs first time or run after killing the app?
I also have same issue, you need to get NSUserActivity from launchOptions
var launchURL :URL? = nil
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
....
// this code should have worked but not working for me with Xcode 9
// if let userActivityDictionary = launchOptions?[.userActivityDictionary] as? [UIApplicationLaunchOptionsKey : Any],
// let auserActivity = userActivityDictionary[.userActivityType] as? NSUserActivity {
// launchURL = auserActivity.webpageURL
// }
if let userActDic = launchOptions?[UIApplicationLaunchOptionsKey.userActivityDictionary] as? [String: Any],
let auserActivity = userActDic["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity{
NSLog("type \(userActDic.self),\(userActDic)") // using NSLog for logging as print did not log to my 'Device and Simulator' logs
launchURL = auserActivity.webpageURL
}
...
}
func applicationDidBecomeActive(_ application: UIApplication) {
if let url = self.launchURL {
self.launchURL = nil
DispatchQueue.main.asyncAfter(deadline: .now()+2.0, execute: {
// wait to initalize notifications
if let dynamicLinks = DynamicLinks.dynamicLinks() {
NSLog("handling \(dynamicLinks)")
dynamicLinks.handleUniversalLink(url) { (dynamiclink, error) in
if let link = dynamiclink?.url {
NSLog("proceessing \(dynamicLinks)")
let strongMatch = dynamiclink?.matchConfidence == .strong
// process dynamic link here
self.processDynamicLink(withUrl: link, andStrongMatch: strongMatch)
}
}
}
})
}
}
I also facing this same issue all day. Firebase SDK does not handle this edge case and didn't mention anywhere in their documentation. Very frustrating!
Found a solution after the investigation for iOS 13 and later OS.
Firebase Dynamic link is working and also able to redirect to a specific page even when the app runs the first time or run after killing the app.
Have the below code in the SceneDelegate instead of AppDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let firstUrlContext = connectionOptions.userActivities.first,let url = firstUrlContext.webpageURL {
DynamicLinks.dynamicLinks().handleUniversalLink(url) { (dynamiclink, error) in
if let dynamiclink = dynamiclink {
self.handleDynamicLink(dynamiclink)
}
}
}
}
The two openURL AppDelegate methods that you are referencing are called when the application is opened by use of a URI scheme. URI scheme functionality will only work in very specific scenarios. In iOS 9, Apple switched to Universal Links, which actually call the function application:continueUserActivity:restorationHandler:.
This function does not call the openURL methods so all of your Universal links handling will have to be done in the continueUserActivity that you mentioned first.
For the sake of saving yourself a lot of trouble in handling other edgecases, I'd suggest moving to another 3rd party provider like Branch (Full disclosure, I work there) since they bundle all of this handling into one callback and provide more functionality and attribution related services that are not available to Firebase users.
In application:didFinishLaunchingWithOptions: method, you need to register your customURLScheme.
FirebaseOptions.defaultOptions()?.deepLinkURLScheme = YOUR_URL_SCHEME

Launching with UIApplicationShortcutItem

I'm implementing some 3D touch quick actions for my iOS 9 app in swift, and I have a curious issue. When my app is in the background and I launch with the quick action, everything goes as planned. When my app is totally dead (i.e. I killed it from the multitasking menu), and I launch with the quick action, the app crashes. I'm having trouble debugging this as once I kill the app, the debug session in Xcode gets detached. Is there a way for me to connect to the app to debug like normal, or is there something in my code that would be causing it? Thanks in advance.
Code:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
var launchedFromShortCut = false
//Check for ShortCutItem
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem
{
launchedFromShortCut = true
self.handleShortCutItem(shortcutItem)
}
return !launchedFromShortCut
}
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void)
{
self.handleShortCutItem(shortcutItem)
}
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem)
{
//Get type string from shortcutItem
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type)
{
//Get root navigation viewcontroller and its first controller
let rootNavigationViewController = window!.rootViewController as? UINavigationController
if let rootViewController = rootNavigationViewController?.viewControllers.first as! LaunchViewController?
{
//Pop to root view controller so that approperiete segue can be performed
rootNavigationViewController?.popToRootViewControllerAnimated(false)
switch shortcutType
{
case .Compose:
rootViewController.shouldCompose()
break
}
}
}
}
Thanks!
In Xcode, open Product -> Schemes -> Edit Schemes
In your Run Scheme, change the Launch setting to 'Wait for executable to be launched'
Now, if you turn on debugging and run your app, Xcode will wait for you to launch your app from the home screen so you are able to test launching it using a 3D Touch Shortcut Item.
I finally got this working. Here's what my AppDelegate.swift file ended up as;
class AppDelegate: UIResponder, UIApplicationDelegate {
// Properties
var window: UIWindow?
var launchedShortcutItem: UIApplicationShortcutItem?
func applicationDidBecomeActive(application: UIApplication) {
guard let shortcut = launchedShortcutItem else { return }
handleShortcut(shortcut)
launchedShortcutItem = nil
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
var shouldPerformAdditionalDelegateHandling = true
// If a shortcut was launched, display its information and take the appropriate action
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
launchedShortcutItem = shortcutItem
// This will block "performActionForShortcutItem:completionHandler" from being called.
shouldPerformAdditionalDelegateHandling = false
}
return shouldPerformAdditionalDelegateHandling
}
func handleShortcut( shortcutItem:UIApplicationShortcutItem ) -> Bool {
// Construct an alert using the details of the shortcut used to open the application.
let alertController = UIAlertController(title: "Shortcut Handled", message: "\"\(shortcutItem.localizedTitle)\"", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertController.addAction(okAction)
// Display an alert indicating the shortcut selected from the home screen.
window!.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
return handled
}
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
completionHandler(handleShortcut(shortcutItem))
}
Much of this was taken from Apple's sample code for UIApplicationShortcuts, and while I'm having my app launch an alert to prove that it is recognizing the proper shortcut was chosen, this could be adapted to your code to pop the view controller.
I think the func applicationDidBecomeActive was the critical part that I was missing, and removing the self.handleShortCut(shortcutItem) from didFinishLaunchingWithOptions (otherwise it was calling handleShortCut twice, it seemed).
For Swift 4.2
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
var isLaunchedFromQuickAction = false
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
isLaunchedFromQuickAction = true
handleQuickAction(shortcutItem: shortcutItem)
}
return isLaunchedFromQuickAction
}
I tried all the above, and it didn't solve the problem
than I tried handling the shortcut after delay in handleShortcut method:
self.performSelector("action1", withObject: self, afterDelay: 0.5)
and added a method for every action, and it worked like a charm
Replace your didfinishlaunching method with this one.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
if let shortcutItem =
launchOptions?[UIApplicationLaunchOptionsShortcutItemKey]
as? UIApplicationShortcutItem {
handleShortcut(shortcutItem)
return false
}
return true
}
XCode 11.6, Swift 5
We can attach a process at runtime. XCode will wait until the process is started and will get attached to it when an App is launched manually.
XCode -> Debug -> Attach to process by PID or Name -> ("Enter the name of an app in the pop-up")
Directions:
Make sure an application is freshly installed on a device or a simulator.
Kill the application.
In XCode attach the name of the process as mentioned above.
Open the application through the desired shortcut
P.S: If you are using SceneDelegate a shortcutItem can be found in
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
switch connectionOptions.shortcutItem?.localizedTitle {
case "Search":
break
case "DoSomething":
break
default:
break
}
}
Happy Debugging :)

Resources