Home iOS Deferred Deep Link Implementation (Swift)
Post
Cancel

iOS Deferred Deep Link Implementation (Swift)

Build an app transition flow that adapts to all scenarios without interruption

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

Starting from iOS ≥ 16, when an app actively reads the clipboard without user-initiated action, a prompt will appear asking for permission. Users need to allow this for the app to access clipboard information.

UIPasteBoard’s privacy change in iOS 16

UIPasteBoard’s privacy change in iOS 16

[2020/07/02] Update

Irrelevant

From graduating and completing military service to now working aimlessly for nearly three years, my growth has plateaued, and I have settled into a comfort zone. Fortunately, a decision to resign sparked a new beginning.

While reading “Designing Your Life” and reorganizing my life plan, I reflected on my work and life. Despite not having exceptional technical skills, sharing on Medium has allowed me to enter a state of “flow” and gain a lot of energy. Recently, a friend asked me about Deep Link issues, so I organized my research findings and replenished my energy in the process!

Scenarios

First, let’s explain the practical application scenarios.

  1. When a user with the app installed clicks on a URL link (from Google search, Facebook post, Line link, etc.), the app should directly display the target screen. If the app is not installed, it should redirect to the App Store for installation. After installing and opening the app, it should be able to reproduce the desired screen from before.

iOS Deferred Deep Link Demo

  1. Tracking data for app downloads and openings. We want to know how many people actually download and open the app through a promotional link.

  2. Special event entrances, such as being able to receive rewards by downloading and opening through a specific URL.

Support:

iOS ≥ 9

iOS Deep Link Mechanism

As seen, the iOS Deep Link mechanism itself only determines if the app is installed. If it is, the app opens; if not, it does nothing.

First, we need to add a prompt to redirect to the App Store if the app is not installed:

The URL Scheme part is controlled by the system and is generally used for internal app calls and rarely exposed publicly. If the trigger point is in an area you cannot control (e.g., Line link), it cannot be handled.

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('Install Marry App now?')){
          document.location = appstore;
        }
      }, 1000);
    }

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

</body>
</html>

The general logic is to call the URL Scheme, set a Timeout, and if the page has not redirected within the set time, assume that the Scheme cannot be called and redirect to the APP Store page (but the experience is still not good as there will still be a URL error prompt, just with added automatic redirection).

Universal Link itself is a webpage. If there is no redirection, it defaults to being presented in a web browser. Websites with web services can choose to directly jump to the web browser for those services, or directly redirect to the APP Store page.

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

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

When browsing the webpage version on iPhone Safari, an APP installation prompt will appear at the top, along with a button to open the page using the APP; the app-argument parameter is used to pass in page values and transmit them to the APP.

Flowchart of adding "redirect to APP Store if not available"

Flowchart of adding “redirect to APP Store if not available”

Of course, what we want is not just “open the APP when the user has it installed,” but also to link the referral information with the APP, so that the APP automatically displays the target page when opened.

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

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
}

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

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"]
    /// Parse the URL query into a [String: String] array
    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
    }
    
}

First, an extension method queryParameters for URL is provided to easily convert URL Queries 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, webpageURL = userActivity.webpageURL {
    /// If it is a universal link URL source...
    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 looks perfect now, we’ve handled all the scenarios we might encounter, so what else is missing?

Entering the main point of this article

What is a Deferred Deep Link? It is to extend our Deep Link to retain referral data even after installing from the APP Store.

According to Android engineers, Android itself has this feature, but it is not supported on iOS, and the method to achieve this is not user-friendly. Keep reading to find out more.

If you don’t want to spend time doing it yourself, you can directly use branch.io or Firebase Dynamic Links. The method introduced in this article is the way Firebase uses.

There are two ways to achieve the effect of Deferred Deep Link:

One is to calculate a hash value based on user device, IP, environment, etc., store data on the server on the web side; when the APP is opened after installation, calculate in the same way, if the values are the same, retrieve the data (branch.io’s method).

The other is the method introduced in this article, similar to Firebase’s approach; using the iPhone clipboard and Safari and APP Cookie sharing mechanism, which means storing data in the clipboard or Cookie, and then reading it out for use after the APP is installed.

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

Those who have used Firebase Dynamic Links must be familiar with this opening redirect page. Once you understand the principle, you will know that this page cannot be removed from the process!

Additionally, Firebase does not provide style modifications.

Support

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

If the APP only considers iOS ≥ 10, it is much easier. The APP implements clipboard access, the Web uses JavaScript to overwrite information to the clipboard, and then redirects to the APP Store for download.

iOS = 9 does not support JavaScript automatic clipboard but supports Safari and APP SFSafariViewController “Cookie sharing method”

Also, the APP needs to secretly add SFSafariViewController in the background to load the Web, and then obtain the Cookie information stored when clicking the link from the Web.

The process is cumbersome & link clicks are limited to Safari browser.

According to the official documentation, iOS 11 can no longer access the user’s Safari Cookie. If you have such a requirement, you can use SFAuthenticationSession, but this method cannot be executed stealthily in the background, and a confirmation window will pop up each time before loading.

_SFAuthenticationSession Prompt_

SFAuthenticationSession Prompt

Also, App Review does not allow placing SFSafariViewController where users cannot see it. (It’s not easy to be noticed by triggering programmatically and then adding it as a subview.)

Get Started

Let’s start with something simple, considering users with iOS ≥ 10, simply transfer information using the iPhone clipboard.

Web End:

We customized our own page similar to Firebase Dynamic Links, using the clipboard.js package to copy the information we want to bring to the app when users click “Go Now” to the clipboard (marry://topicID=1&type=topic), and then use location.href to redirect to the App Store page.

App End:

Read the clipboard value in AppDelegate or the main UIViewController:

let pasteData = UIPasteboard.general.string

It is recommended to wrap the information using the URL Scheme method here for easy identification and data decryption:

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 completing the action, use UIPasteboard.general.string = “” to clear the information in the clipboard.

Get Started — Support for iOS 9 Version

Here comes the tricky part, supporting the iOS 9 version. As mentioned earlier, due to the lack of clipboard support, we need to use the Cookie Exchange Method.

Web End:

Handling the web end is relatively straightforward, just change it so that when the user clicks “Go Now,” the information we want to bring to the app is stored in a Cookie (marry://topicID=1&type=topic), and then use location.href to redirect to the App Store page.

Here are two pre-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 period, 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 End:

Here comes the most troublesome part of this document.

As mentioned earlier, we need to secretly load an SFSafariViewController in the background in the main UIViewController to implement the principle.

Another pitfall: The issue of secretly loading is that if the size of the View of iOS ≥ 10 SFSafariViewController is set to less than 1, the opacity is less than 0.05, and it is set to isHidden, the SFSafariViewController will not load.

p.s iOS = 10 supports both Cookies and Clipboard simultaneously.

[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

My approach here is to place a UIView above the UIViewController of the main page with any height, align it to the bottom of the main UIView, then drag IBOutlet (sharedCookieView) to the Class; in viewDidLoad(), initialize the SFSafariViewController and add its View to sharedCookieView, so it actually displays and loads, just off-screen where the user can’t see 🌝.

Where should the URL of SFSafariViewController point to?

Similar to sharing a page on the web, we need to create a separate page for reading Cookies, and place both pages under the same domain to avoid cross-domain Cookie issues, the page content will be provided later.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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

This Delegate needs to be added to capture the callback after loading is complete.

We can use:

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

Capture the load completion event in the method.

At this point, you might think that reading the cookies in didCompleteInitialLoad completes the process!

I couldn’t find a method to read SFSafariViewController cookies here, and using internet methods to read them always returns empty.

Or you may need to interact with the page content using JavaScript, have JavaScript read the cookies and return them to the UIViewController.

Tricky URL Scheme Method

Since iOS doesn’t know how to get shared cookies, we can directly let the “cookie-reading page” help us “read the cookies”.

The JavaScript method for handling cookies provided earlier with the getCookie() function is used here. Our “cookie-reading page” is a blank page (users can’t see it anyway), but in the JavaScript part, we need to read the cookies after the body onload event:

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: add an SFSafariViewController to HomeViewController viewDidLoad to secretly load the loadCookie.html page. The loadCookie.html page checks and reads the previously stored cookies, clears them if found, and then uses window.location.href to trigger the URL Scheme mechanism.

So the corresponding callback processing will return to func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) in AppDelegate.

Done! Summary:

If you find it cumbersome, you can directly use branch.io or Firebase Dynamic without reinventing the wheel. Here, it’s because of interface customization and some complex requirements that we have to build it ourselves.

iOS 9 users are already very rare, so you can ignore it if it’s not necessary; using the clipboard method is fast and efficient, and using the clipboard means you don’t have to limit the links to be opened in Safari!

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

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here



This post is licensed under CC BY 4.0 by the author.

Using 'Shortcuts' Automation with Mi Home Smart Home on iOS ≥ 13.1

iOS UIViewController Transition Techniques