iOS Deferred Deep Link Implementation|Swift Guide to Seamless App Navigation
Discover how to implement iOS deferred deep links in Swift to create uninterrupted app navigation across all scenarios, enhancing user experience and retention with reliable link handling.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
iOS Deferred Deep Link Implementation (Swift)
Create a Seamless App Redirect Flow That Adapts to All Scenarios
[2022/07/22] Update on iOS 16 Upcoming Changes
Starting from iOS 16, when an app reads the clipboard without user-initiated paste action, a prompt will appear asking for permission. The user must allow it for the app to access the clipboard data.
UIPasteBoard’s privacy change in iOS 16
[2020/07/02] Update
Unrelated
After graduating and completing my military service, I have been working mediocrely for almost three years. My growth has plateaued, and I started to settle into my comfort zone. Fortunately, I made a firm decision to resign, take a break, and start anew.
While reading Design Your Own Life and rethinking my life plan, I reviewed my work and life. Although my technical skills are not very strong, writing on Medium and sharing with others helps me enter a state of “flow” and gain a lot of energy. Recently, a friend asked me about Deep Link issues, so I took the chance to organize my research methods and also recharge my energy!
Scenario
First, the actual application scenario must be explained.
- When the user has the app installed and clicks a URL link (from Google search, FB post, Line link, etc.), the app should open directly to the target screen. If not installed, redirect to the App Store to install the app; after installation and opening the app, it should restore the previously intended screen.
APP Download and Launch Data Tracking: We want to know how many people actually download and open the APP through the promotion link.
Special event entry, such as obtaining rewards by downloading and opening the app through a specific URL.
Support Level:
iOS ≥ 9
What is the Difference Between Deferred Deep Link and Deep Link?
Pure Deep Link itself:
You can see that the iOS Deep Link mechanism only checks whether the app is installed; if it is, it opens the app, otherwise it does nothing.
First, we need to add a “Redirect to App Store if not installed” prompt to guide users to install the app:
URL Scheme is controlled by the system and is generally used for internal app calls, rarely made public; if the trigger point is outside your control (e.g., Line links), it cannot be handled.
If the trigger point is on your own webpage, you can use some tricks to handle it. Please refer to here:
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 the 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, then set a timeout. If the page hasn’t redirected by the time expires, assume the app is not installed and cannot call the Scheme, then redirect to the App Store page (but the experience is still poor as an error message may appear, just with automatic redirection added).
Universal Link is essentially its own webpage. If no redirection occurs, it defaults to being displayed in a web browser. Here, if a web service is available, you can choose to redirect directly to the webpage; if not, it will redirect to the App Store page.
Web service websites can add the following inside the <head></head>
:
1
<meta name="apple-itunes-app" content="app-id=APPID, app-argument=page-parameter">
When browsing the web version on iPhone Safari, an APP installation prompt and a button to open the current page with the APP will appear at the top; the app-argument
parameter is used to pass page values and transmit them to the APP.
Add a flowchart for “If none, redirect to the APP Store”
Improve Deep Link Handling on the APP Side:
What we want is not just “open the app if the user has it installed,” but also to link the source information with the app so that the app automatically displays the target page when opened.
URL Scheme can be handled in AppDelegate within the func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
method:
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 the AppDelegate within the func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool
method:
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
}
}
First, add an extension method queryParameters
to URL for easily converting 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
}
Completed!
So what’s still missing?
So far, it looks perfect. We’ve handled all possible scenarios. What else is missing?
As shown in the image, if the flow is Not Installed -> Install from APP Store -> Open from APP Store, the source data will be lost. The app won’t know the source and will only show the home page. The user must go back to the previous webpage and click to open again for the app to trigger the page jump.
Although this is not impossible, considering the dropout rate, each extra step adds another layer of loss and disrupts the user experience; moreover, users may not be that smart.
Getting to the Point of This Article
What is a Deferred Deep Link? A deferred deep link allows our Deep Link to retain source information even after the app is installed from the App Store.
According to Android engineers, Android natively supports this feature, but iOS does not. Achieving this function on iOS is also not user-friendly. Please read on.
Deferred Deep Link
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 common methods online to achieve the effect of Deferred Deep Link:
One method is to calculate a hash value using parameters such as the user’s device, IP, environment, etc., and store the data on the server from the web end; when the app is installed and opened, it calculates the value in the same way, and if the values match, it retrieves the data to restore it (the approach used by branch.io).
Another method introduced in this article is similar to the Firebase approach; it uses the iPhone clipboard and the shared cookie mechanism between Safari and the app. This means storing data in the clipboard or cookies, which the app reads and uses after installation.
1
2
After clicking "Open," your clipboard will be automatically overwritten and copied by JavaScript with the redirect information: https://XXX.app.goo.gl/?link=https://XXX.net/topicID=1&type=topic
Anyone who has used Firebase Dynamic Links is surely familiar with this redirect page. Once you understand the principle, you’ll know this page cannot be removed from the flow!
Additionally, Firebase does not provide options for style customization.
Support Level
First, let’s talk about a pitfall: support issues; as mentioned before, it is “unfriendly”!
If the app only supports iOS 10 or above, it becomes much easier. The app can implement clipboard access, the web can use JavaScript to overwrite information to the clipboard, and then redirect to the App Store for download.
iOS 9 does not support JavaScript automatic clipboard but supports Safari and APP SFSafariViewController “Cookie Sharing Method”
Additionally, the app needs to secretly add SFSafariViewController in the background to load the web page, then retrieve the cookie information stored when the link was clicked.
The steps are complicated & link clicks are limited to Safari browser.
According to official documentation, iOS 11 no longer allows access to users’ Safari cookies. If this is needed, SFAuthenticationSession can be used, but this method cannot run stealthily in the background. Each time it loads, the following prompt will appear:
SFAuthenticationSession prompt window
Also, app review does not allow placing SFSafariViewController where users cannot see it. (Triggering it programmatically and then adding it as a subview is not easily detected)
Hands-on Practice
Simply put, only consider users with iOS ≥ 10, using the iPhone clipboard to transfer information.
Web version:
We customized our own page by imitating Firebase Dynamic Links. Using the clipboard.js
library, when users click “Go Now,” the information we want to pass to the app (marry://topicID=1&type=topic
) is first copied to the clipboard, then location.href
is used to redirect to the App Store page.
APP Side:
Read the clipboard value in AppDelegate or the main UIViewController:
let pasteData = UIPasteboard.general.string
// Get the string content from the general pasteboard
It is recommended to package the information using URL Scheme for easy identification and data parsing:
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 actions, use UIPasteboard.general.string = ""
to clear the information in the clipboard.
Hands-on — Support for iOS 9 Version
Troubles arise with iOS 9 support. As mentioned earlier, since clipboard sharing is not supported, the Cookie sharing method must be used.
Web version:
The web side is also easy to handle. Just save the information we want to pass to the APP into a cookie when the user clicks “Go Now” (marry://topicID=1&type=topic
), then use location.href
to redirect to the APP Store page.
Here are two encapsulated 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 side:
The most troublesome part of this article is coming.
The principle was mentioned earlier: we need to secretly load an SFSafariViewController programmatically in the background of the main UIViewController without the user noticing.
Another pitfall: For iOS ≥ 10, if the SFSafariViewController’s view size is set smaller than 1, opacity less than 0.05, or isHidden is true, the SFSafariViewController will not load.
p.s iOS = 10 supports both Cookies and Clipboard simultaneously.
My approach is to place a UIView with any height above the main UIViewController on the homepage, aligning its bottom to the top of the main UIView. Then, I connect an IBOutlet (sharedCookieView
) to the class. In viewDidLoad()
, I initialize an SFSafariViewController and add its view to sharedCookieView
. So it actually loads and displays, but the screen pops out and the user can’t see it 🌝.
What URL should SFSafariViewController point to?
Like the web sharing page, we need to create a 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
You need to add this Delegate to capture the callback after the loading is complete.
We can at:
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
Capture the load completion event within the method.
At this point, you might think the next step is to read the cookies inside the webpage within didCompleteInitialLoad
to complete the process!
I couldn’t find a method to read cookies from SFSafariViewController here; using network methods to read them always returns empty.
May need to use JavaScript to interact with page content, calling JavaScript to read cookies and return them to the UIViewController.
Tricky’s URL Scheme Method
Since iOS doesn’t know how to access shared Cookies, we’ll let the “page that reads Cookies” handle the “reading of Cookies” for us directly.
The getCookie() method from the previously mentioned JavaScript cookie handling is used here. Our “cookie reading page” is a blank page (since the user won’t see it), but in the JavaScript part, the cookie must be read after 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 summary is: In HomeViewController viewDidLoad
, add SFSafariViewController
to stealthily load the loadCookie.html
page. The loadCookie.html
page reads and checks previously stored cookies; if any exist, it reads and clears them, then uses window.location.href
to call and trigger the URL Scheme
mechanism.
So the corresponding callback handling will return to func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
in the AppDelegate
for processing.
Completed! Summary:
If it feels cumbersome, you can directly use branch.io or Firebase Dynamic. There’s no need to reinvent the wheel. Here, we built our own solution due to interface customization and some complex requirements.
iOS 9 users are now very rare, so you can safely ignore them unless necessary; using the clipboard method is fast and efficient, and it doesn’t limit you to opening links only in Safari!
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.