Post

iOS Deferred Deep Link Implementation (Swift)

Hands-on creation of an uninterrupted app transition process adaptable to all scenarios

iOS Deferred Deep Link Implementation (Swift)

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


Hands-on creation of an uninterrupted app transition process adaptable to all scenarios

[2022/07/22] Update on Upcoming Changes in iOS 16

Starting from iOS ≥ 16, when a user does not actively paste, the app’s attempt to read the clipboard will trigger a prompt asking for permission. The user must grant permission for the app to access clipboard information.

[UIPasteBoard’s privacy change in iOS 16](https://sarunw.com/posts/uipasteboard-privacy-change-ios16/){:target="_blank"}

UIPasteBoard’s privacy change in iOS 16

[2020/07/02] Update

Irrelevant

After graduating and completing military service, I’ve been working for nearly three years. My growth has plateaued, and I’ve started to settle into my comfort zone. Fortunately, I made the bold decision to resign and take a step back to reassess my path.

While reading Designing Your Life, I reflected on my career and life. Although my technical skills aren’t exceptional, writing on Medium and sharing with others allows me to enter a “flow state” and gain a lot of energy. Recently, a friend asked about Deep Link issues, so I organized my research on the topic and replenished my energy in the process!

Scenarios

First, let’s clarify the actual application scenarios.

1. When a user clicks a URL link (from Google search, FB post, Line link, etc.) and has the app installed, the app should open directly to the target screen. If not installed, it should redirect to the App Store to install the app; after installation, opening the app should recreate the previously intended screen.

iOS Deferred Deep Link Demo

2. Tracking app downloads and openings. We want to know how many users actually downloaded and opened the app from the promotional link.

3. Special event entries, for example, downloading and opening through a specific URL to receive rewards.

Support:

iOS ≥ 9

As you can see, the mechanism of iOS Deep Link only determines whether the app is installed. If it is, the app opens; if not, nothing happens.

First, we need to add a prompt for “redirect to the App Store if not installed” to inform users to install the app:

The URL Scheme part is controlled by the system and is generally used for internal app calls, which are rarely made public; if the trigger point is in an area that cannot be controlled (e.g., Line link), it cannot be processed.

If the trigger point is on your own webpage, you can use some tricks to handle it. Please refer to this link:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
<head>
  <title>Redirect...</title>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <script>
    var appurl = 'marry://open';
    var appstore = 'https://apps.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E6%9C%80%E5%A4%A7%E5%A9%9A%E7%A6%AE%E7%B1%8C%E5%82%99app/id1356057329';

    var timeout;
    function start() {
      window.location = appurl;
      timeout = setTimeout(function(){
        if(confirm('Would you like to install the Marriage App now?')){
          document.location = appstore;
        }
      }, 1000);
    }

    window.onload = function() {
      start()
    }
  </script>
</head>
<body>

</body>
</html>

The basic logic is to call the URL Scheme, then set a timeout. If the page hasn’t redirected after the timeout, we assume the scheme cannot be called because the app isn’t installed, and we redirect to the App Store page (though the experience is still poor and may show an error message, it at least adds automatic redirection).

Universal Link is essentially a webpage of its own. If there is no redirection, it defaults to being displayed in a web browser. If you have web services, you can choose to directly open the webpage; if not, it redirects to the App Store page.

Websites with web services can add the following in <head></head>:

1
<meta name="apple-itunes-app" content="app-id=APPID, app-argument=page-parameter">

When browsing the webpage version in iPhone Safari, a prompt to install the app and a button to open this page in the app will appear at the top; the parameter app-argument is used to pass the page value to the app.

Flowchart for adding "redirect to the App Store if not installed"

Flowchart for adding “redirect to the App Store if not installed”

What we want is not just “open the app if the user has it installed,” but also to connect the source information with the app, allowing the app to automatically present the target page after opening.

The URL Scheme method can be handled in func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool in AppDelegate:

1
2
3
4
5
6
7
8
9
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    if url.scheme == "marry", let params = url.queryParameters {
      if params["type"] == "topic" {
        let VC = TopicViewController(topicID: params["id"])
        UIApplication.shared.keyWindow?.rootViewController?.present(VC, animated: true)
      }    
    }
    return true
}

Universal Link is handled in func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool in AppDelegate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension URL {
    /// test=1&a=b&c=d => ["test":"1","a":"b","c":"d"]
    /// Parses the URL query into a [String: String] dictionary
    public var queryParameters: [String: String]? {
        guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true), let queryItems = components.queryItems else {
            return nil
        }
        
        var parameters = [String: String]()
        for item in queryItems {
            parameters[item.name] = item.value
        }
        
        return parameters
    }
    
}

Here’s a URL extension method for queryParameters, which conveniently converts the URL query into a Swift Dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
        
  if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let webpageURL = userActivity.webpageURL {
    /// If the source is a universal link URL...
    let params = webpageURL.queryParameters
    
    if params["type"] == "topic" {
      let VC = TopicViewController(topicID: params["id"])
      UIApplication.shared.keyWindow?.rootViewController?.present(VC, animated: true)
    }
  }
  
  return true  
}

Done!

What else is missing?

It seems perfect so far; we’ve handled all possible scenarios, but what else is missing?

As shown in the diagram, if the user is not installed -> installs from the App Store -> opens from the App Store, the source data will be lost, and the app won’t know the source, so it will only display the homepage. The user will have to go back to the previous webpage and click to open again for the app to navigate to the intended page.

While this approach is not impossible, considering the drop-off rate, adding an extra step increases the likelihood of losing users, and the user experience becomes less smooth; moreover, users may not be that savvy.

Entering the Main Point of the Article

What is Deferred Deep Link? It is a delayed deep link that allows our Deep Link to retain source data even after the app is installed from the App Store.

According to Android engineers, Android has this functionality built-in, but iOS does not support this setting, and achieving this functionality is not user-friendly. Please continue reading.

If you don’t want to spend time implementing this yourself, you can directly use branch.io or Firebase Dynamic Links The method introduced in this article is the one used by Firebase.

To achieve the effect of Deferred Deep Link, there are two methods available online:

One method calculates a hash value based on user device, IP, environment, etc., and stores data on the server when the webpage is accessed; when the app is installed and opened, it calculates the value in the same way. If the values match, it retrieves the data (this is the approach used by branch.io).

The other method, which this article will introduce, is similar to Firebase’s approach; it uses the iPhone clipboard and the shared mechanism between Safari and the app’s cookies, effectively storing data in the clipboard or cookies, which the app can read after installation.

1
After clicking "Open," your clipboard will be automatically overwritten with JavaScript, copying the relevant redirect information: https://XXX.app.goo.gl/?link=https://XXX.net/topicID=1&type=topic

Those who have used Firebase Dynamic Links will be familiar with this redirect page; once you understand the principle, you’ll realize this page cannot be removed from the flow!

Additionally, Firebase does not allow for style modifications.

Support

First, let’s talk about the pitfalls regarding support; as mentioned earlier, it is “not user-friendly”!

If the app only considers iOS ≥ 10, it becomes much easier. The app can implement clipboard access, and the web can use JavaScript to overwrite the clipboard information, then redirect to the App Store for download.

iOS = 9 does not support automatic clipboard access via JavaScript but does support Cookie sharing between Safari and the app’s SFSafariViewController.

Additionally, the app needs to secretly load an SFSafariViewController in the background to retrieve the cookie information stored when the link was clicked.

The steps are cumbersome, and link clicks are limited to the Safari browser.

[SFSafariViewController](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller){:target="_blank"}

SFSafariViewController

According to official documentation, starting from iOS 11, it is no longer possible to access the user’s Safari cookies. If you need this functionality, you can use SFAuthenticationSession, but this method cannot run in the background and will always prompt the following dialog before loading:

_SFAuthenticationSession Prompt_

SFAuthenticationSession Prompt

Moreover, app review does not allow placing SFSafariViewController in areas where users cannot see it. (Triggering it programmatically and then adding it as a subview is not easily detectable)

Hands-On

Let’s start with the simpler part, considering only users on iOS ≥ 10, using the iPhone clipboard to transfer information.

Web Side:

We created a custom page mimicking Firebase Dynamic Links, using the clipboard.js library to copy the information we want to send to the app to the clipboard (marry://topicID=1&type=topic) when the user clicks “Go Now,” and then using location.href to redirect to the App Store page.

App Side:

In the AppDelegate or main UIViewController, read the clipboard value:

let pasteData = UIPasteboard.general.string

It is recommended to wrap the information using the URL Scheme for easier identification and data resolution:

1
2
3
4
5
6
if let pasteData = UIPasteboard.general.string, let url = URL(string: pasteData), url.scheme == "marry", let params = url.queryParameters {
    if params["type"] == "topic" {
      let VC = TopicViewController(topicID: params["id"])
      UIApplication.shared.keyWindow?.rootViewController?.present(VC, animated: true)
    }
}

Finally, after processing the action, use UIPasteboard.general.string = "" to clear the information from the clipboard.

Hands-On — Supporting iOS 9

Now comes the tricky part, supporting iOS 9. As mentioned earlier, since clipboard access is not supported, we will use the Cookie sharing method.

Web Side:

The web side is relatively easy to handle; we just need to change it so that when the user clicks “Go Now,” we store the information we want to send to the app in a cookie (marry://topicID=1&type=topic), and then use location.href to redirect to the App Store page.

Here are two packaged JavaScript methods for handling cookies to speed up development:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// name: Cookie name
/// val: Cookie value
/// day: Cookie expiration, default is 1 day
/// EX1: setcookie("iosDeepLinkData","marry://topicID=1&type=topic")
/// EX2: setcookie("hey","hi",365) = valid for one year
function setcookie(name, val, day) {
    var exdate = new Date();
    day = day || 1;
    exdate.setDate(exdate.getDate() + day);
    document.cookie = "" + name + "=" + val + ";expires=" + exdate.toGMTString();
}

/// getCookie("iosDeepLinkData") => marry://topicID=1&type=topic
function getCookie(name) {
    var arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)"));
    if (arr != null) return decodeURI(arr[2]);
    return null;
}

App Side:

This is the most complicated part of the article.

As mentioned earlier, we need to secretly load an SFSafariViewController in the background in the main UIViewController without the user noticing.

Another pitfall: The secret loading of SFSafariViewController; if its size is set to less than 1, its opacity is less than 0.05, or it is set to isHidden, SFSafariViewController will not load.

p.s. iOS = 10 supports both Cookie and clipboard.

[https://stackoverflow.com/questions/39019352/ios10-sfsafariviewcontroller-not-working-when-alpha-is-set-to-0/39216788](https://stackoverflow.com/questions/39019352/ios10-sfsafariviewcontroller-not-working-when-alpha-is-set-to-0/39216788){:target="_blank"}

https://stackoverflow.com/questions/39019352/ios10-sfsafariviewcontroller-not-working-when-alpha-is-set-to-0/39216788

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
---
title: "iOS Deferred Deep Link Implementation"
date: 2023-10-01
---

My approach here is to place a UIView at the top of the main page's UIViewController with an arbitrary height, aligning its bottom with the top of the main page's UIView. Then, I connect the IBOutlet `sharedCookieView` to the class. In the `viewDidLoad()` method, I initialize the SFSafariViewController and add its view to `sharedCookieView`. So, it actually shows that it has loaded, but it runs off the screen, making it invisible to the user 🌝.

**What URL should the SFSafariViewController point to?**

Similar to the web sharing page, we need to create another page for reading cookies and place both pages under the same domain to avoid cross-domain cookie issues. I will attach the page content later.
```swift
@IBOutlet weak var SharedCookieView: UIView!

override func viewDidLoad() {
    super.viewDidLoad()
    
    let url = URL(string:"http://app.marry.com.tw/loadCookie.html")
    let sharedCookieViewController = SFSafariViewController(url: url)
    VC.view.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
    sharedCookieViewController.delegate = self
    
    self.addChildViewController(sharedCookieViewController)
    self.SharedCookieView.addSubview(sharedCookieViewController.view)
    
    sharedCookieViewController.beginAppearanceTransition(true, animated: false)
    sharedCookieViewController.didMove(toParentViewController: self)
    sharedCookieViewController.endAppearanceTransition()
}

sharedCookieViewController.delegate = self

class HomeViewController: UIViewController, SFSafariViewControllerDelegate

You need to add this delegate to capture the callback after the loading is complete.

We can capture the loading completion event in:

func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {

At this point, you might think that the next step is to read the cookies from the webpage in didCompleteInitialLoad, and that would be it!

I haven’t found a method to read cookies from SFSafariViewController; the methods found online all return empty results.

Alternatively, you might need to use JavaScript to interact with the page content, asking JavaScript to read the cookies and return them to the UIViewController.

The Tricky URL Scheme Method

Since iOS does not know how to obtain shared cookies, we can directly let the “cookie reading page” help us “read the cookies.”

The getCookie() method from the JavaScript cookie handling approach mentioned earlier is used for this. Our “cookie reading page” is a blank page (since the user won’t see it), but in the JavaScript part, we need to read the cookies after the body onload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html>
<head>
  <title>Load iOS Deep Link Saved Cookie...</title>
  <script>
  function checkCookie() {
    var iOSDeepLinkData = getCookie("iOSDeepLinkData");
    if (iOSDeepLinkData && iOSDeepLinkData != '') {
        setcookie("iOSDeepLinkData", "", -1);
        window.location.href = iOSDeepLinkData; /// marry://topicID=1&type=topic
    }
  }
  </script>
</head>

<body onload="checkCookie();">

</body>

</html>

The actual principle is summarized as follows: when HomeViewController viewDidLoad is called, we add the SFSafariViewController to stealthily load the loadCookie.html page. The loadCookie.html page checks for previously stored cookies, and if found, it reads and clears them, then uses window.location.href to call and trigger the URL Scheme mechanism.

Thus, the corresponding callback handling will return to the AppDelegate in the func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool method for processing.

Done! Summary:

If you find this tedious, you can directly use branch.io or Firebase Dynamic—there’s no need to reinvent the wheel. This is because of the need for interface customization and some complex requirements, so I had to build it myself.

Users on iOS 9 are already very rare, so if it’s not necessary, you can ignore it; using the clipboard method is quick and efficient, and with the clipboard, you don’t have to limit the link to be opened in Safari!

If you have any questions or suggestions, feel free to contact me.

```


This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

20,662 Total Views
Last Statistics Date: 2025-03-25 | 20,432 Views on Medium.
This post is licensed under CC BY 4.0 by the author.