Adding iOS Home Shortcuts to Specific Parts of An App in Swift - SwiftRocks

Adding iOS Home Shortcuts to Specific Parts of An App in Swift

Deeplinking is widely used to provide easy access to certain portions of an iOS app. While most of the uses for a deeplink are marketing purposes such as providing shortcuts inside an e-mail or push notification, they can also be used for communicating between different apps and pretty much anything that involves routing to different screens.

iOS already allows you to add "shortcuts" to an app in the shape of 3D Touch Quick Actions, but they are very limited, hidden from the user, and let's be honest - it's one of these features that nobody remembers. You can also use Siri Extensions, but then you have to deal with Siri, and the regular Siri Shortcuts relies on the Shortcuts app, so it's not something that you can provide to your users. Wouldn't it be great if you could give your users the ability to add shortcuts to things like ordering their favorite coffee in the shape of a home icon - just like the app itself?

Well, after reverse engineering Apple's Shortcuts app, it turns out that you can! That's exactly what it does when you add a system shortcut to the home screen, and we're going to replicate it in order to give users the ability to add home icons that directly open a specific part of an app.

Full project available here.

Safari, Raw Data Rendering and PWAs

If you've never added an Apple shortcut to your home screen, here's the Shortcuts app equivalent that we're going to replicate:

One might think that this is the result of an internal API since Apple has never shared anything in that aspect, but this is actually just a clever use of Safari's PWA features.

To make it short, before the Safari screen is shown, the Shortcuts app locally generates a HTML page that pretends to be a PWA. If the page was opened inside Safari, the instructions are shown - but when the page is opened in PWA mode (a.k.a when you open it from the home screen), it instantly redirects to the relevant shortcuts:// deeplink. That's very sneaky, and you can see by yourself exactly how it achieves that by 3D Touching the generated home icon and decoding the base64 string in the URL. Here's an example.

Replicating this requires a bit of tinkering since that's not an official iOS feature, but the end result is exactly the same and easier than it sounds. With this, you can make your app create home shortcuts to virtually anything that's bootable via deeplinks.

The reason this is possible comes from the fact that Safari doesn't need an URL to display a page - it can render raw encoded data of many kinds, including HTML and images.

Have your Safari open this, for example. You should see a SwiftRocks! page:

data:text/html;base64,PGJvZHk+PHA+U3dpZnRSb2NrcyE8L3A+PC9ib2R5Pg==

There's no page hosting this content, it's simply Safari directly rendering the base 64 encoding of <body><p>SwiftRocks!</p></body>. As expected, you can render entires pages using this format, including their scripts.

Routing to a deeplink can be achieved with a simple JavaScript snippet - if you've been around during the iOS days before Apple added Universal Links, you probably had to deal with making an arbitrary page redirect to its deeplink equivalent if the user happened to have the app installed. We can use a similar approach here to achieve this effect:

<html>
  <body>
    <p>SwiftRocks!</p>
    <a id="redirect" href="https://apple.com/"></a>
  </body>
</html>
<script type="text/javascript">
  var element = document.getElementById('redirect');
  var event = document.createEvent('MouseEvents');
  event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
  setTimeout(function() { element.dispatchEvent(event); }, 25);
</script>

This adds an invisible button link to the Apple website and immediatly runs a script that touches it after a small amount of time. You can try it out here:

data:text/html;base64,PGh0bWw+CiAgPGJvZHk+CiAgICA8cD5Td2lmdFJvY2tzITwvcD4KICAgIDxhIGlkPSJyZWRpcmVjdCIgaHJlZj0iaHR0cHM6Ly9hcHBsZS5jb20vIj48L2E+CiAgPC9ib2R5Pgo8L2h0bWw+CjxzY3JpcHQgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij4KICB2YXIgZWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdyZWRpcmVjdCcpOwogIHZhciBldmVudCA9IGRvY3VtZW50LmNyZWF0ZUV2ZW50KCdNb3VzZUV2ZW50cycpOwogIGV2ZW50LmluaXRFdmVudCgnY2xpY2snLCB0cnVlLCB0cnVlLCBkb2N1bWVudC5kZWZhdWx0VmlldywgMSwgMCwgMCwgMCwgMCwgZmFsc2UsIGZhbHNlLCBmYWxzZSwgZmFsc2UsIDAsIG51bGwpOwogIHNldFRpbWVvdXQoZnVuY3Rpb24oKSB7IGVsZW1lbnQuZGlzcGF0Y2hFdmVudChldmVudCk7IH0sIDI1KTsKPC9zY3JpcHQ+

That looks great! But if you take a look at the Shortcuts's app behavior, you'll see that the redirecting starts only after the icon is generated. How does that work?

When Apple first added PWA support for Safari, a "standalone" or "app mode" function was added in order to make the web app experience better for users by hiding normal Safari functions such as the URL bar and navigation arrows. This mode is initiated when a PWA is opened from the home screen, and its usage can be detected by the page. That's exactly what the Shortcuts app uses to make sure the redirecting only happens when the page is opened from the home screen!

In the following example, the SwiftRocks! text is only added if the page was opened inside Safari. If the page was opened in PWA mode, the user will be redirected instead. Metadata was also added to indicate Safari that this page can be opened in PWA mode, as well as a simple title tag so a default name is provided inside the share sheet.

<html>
<head>
  <title>Apple Page</title>
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="#ffffff">
  <meta name="apple-mobile-web-app-title" content="Apple Page">
</head>
  <body>
    <a id="redirect" href="https://apple.com/"></a>
  </body>
</html>
<script type="text/javascript">
  if (window.navigator.standalone) {
    var element = document.getElementById('redirect');
    var event = document.createEvent('MouseEvents');
    event.initEvent('click', true, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
    setTimeout(function() { element.dispatchEvent(event); }, 25);
  } else {
    var p = document.createElement('p');
    var node = document.createTextNode('SwiftRocks!');
    p.appendChild(node);
    document.body.appendChild(p);
  }
</script>

Try it out! Open this in Safari, press the Share button, add it to your home screen and touch it to see how it works. For a real example, change the link to a deeplink to your app and encode it to base64 to see it in action. The page still looks terrible, but we'll see how to add icons and splash screens later on.

data:text/html;base64,PGh0bWw+CjxoZWFkPgogIDx0aXRsZT5BcHBsZSBQYWdlPC90aXRsZT4KICA8bWV0YSBuYW1lPSJhcHBsZS1tb2JpbGUtd2ViLWFwcC1jYXBhYmxlIiBjb250ZW50PSJ5ZXMiPgogIDxtZXRhIG5hbWU9ImFwcGxlLW1vYmlsZS13ZWItYXBwLXN0YXR1cy1iYXItc3R5bGUiIGNvbnRlbnQ9IiNmZmZmZmYiPgogIDxtZXRhIG5hbWU9ImFwcGxlLW1vYmlsZS13ZWItYXBwLXRpdGxlIiBjb250ZW50PSJBcHBsZSBQYWdlIj4KPC9oZWFkPgogIDxib2R5PgogICAgPGEgaWQ9InJlZGlyZWN0IiBocmVmPSJodHRwczovL2FwcGxlLmNvbS8iPjwvYT4KICA8L2JvZHk+CjwvaHRtbD4KPHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcHQiPgogIGlmICh3aW5kb3cubmF2aWdhdG9yLnN0YW5kYWxvbmUpIHsKICAgIHZhciBlbGVtZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3JlZGlyZWN0Jyk7CiAgICB2YXIgZXZlbnQgPSBkb2N1bWVudC5jcmVhdGVFdmVudCgnTW91c2VFdmVudHMnKTsKICAgIGV2ZW50LmluaXRFdmVudCgnY2xpY2snLCB0cnVlLCB0cnVlLCBkb2N1bWVudC5kZWZhdWx0VmlldywgMSwgMCwgMCwgMCwgMCwgZmFsc2UsIGZhbHNlLCBmYWxzZSwgZmFsc2UsIDAsIG51bGwpOwogICAgc2V0VGltZW91dChmdW5jdGlvbigpIHsgZWxlbWVudC5kaXNwYXRjaEV2ZW50KGV2ZW50KTsgfSwgMjUpOwogIH0gZWxzZSB7CiAgICB2YXIgcCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3AnKTsKICAgIHZhciBub2RlID0gZG9jdW1lbnQuY3JlYXRlVGV4dE5vZGUoJ1N3aWZ0Um9ja3MhJyk7CiAgICBwLmFwcGVuZENoaWxkKG5vZGUpOwogICAgZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChwKTsKICB9Cjwvc2NyaXB0Pg==

iOS: Opening the raw page from Swift

Now that we've seen how to make a shortcut PWA, we can make an app generate and trigger it.

I'm assuming you already know how to create and process deeplinks in iOS, so I'll go straight to the point. In this example, I'm going to make a simple UIViewController that represents a "profile" screen trigger a shortcut page that opens an shortcutTestApp://profile deeplink that my app listens to in order to route the user to this same view controller:

final class ProfileViewController: UIViewController {
    @IBAction func shortcutButtonTouched(_ sender: Any) {
    	showShortcutScreen(forDeepLink: "shortcutTestApp://profile")
    }
}

Inside showShortcutScreen, I'm going to generate the HTML for that shortcut, encode it and attempt to open it:

func showShortcutScreen(forDeepLink deepLink: String) {
    guard let deepLinkUrl = URL(string: deepLink) else {
        return
    }
    let html = getHtml(title: "Profile", urlToRedirect: deepLinkUrl)
    guard let data = html.data(using: .utf8) else {
        return
    }
    let base64 = data.base64EncodedString()
    guard let shortcutPage = URL(string: "data:text/html;base64,\(base64)") else {
        return
    }
    UIApplication.shared.open(shortcutPage)
}

func getHtml(title: String, urlToRedirect: URL) -> String {
	return
	"""
	    <head>
	      <body>
	        .. //Rest of the HTML, replacing the relevant data with the properties.
	"""
}

This looks straightforward, but there's a big problem: If you try to run this, you'll see that it will simply not work.

If you try to open a data: page from Swift using open(url), nothing will happen, and if you try to use SFSafariViewController, it will crash stating that it doesn't understand the URL. There's no clear reason why this happens since Safari itself can open these links - it honestly just sounds like a bug in Apple's side. However, it does mean that we need to do a small workaround to make it work, but the good news is that even the Shortcuts app needs to do this, so you're not going to be alone.

A solution for this is that instead of trying to directly open the raw content, you can create a localhost server that redirects to it. To make things easy, I'm going to use the Swifter library to create this server:

var localServer: HttpServer?

func showShortcutScreen(forDeepLink deepLink: String) {
    localServer = HttpServer()
    guard let deepLinkUrl = URL(string: deepLink) else {
        return
    }
    let html = getHtml(title: "Profile", urlToRedirect: deepLinkUrl)
    guard let data = html.data(using: .utf8) else {
        return
    }
    let base64 = data.base64EncodedString()
    guard let shortcutPage = URL(string: "http://localhost:8245/shortcut") else {
        return
    }
    localServer?["/shortcut"] = { request in
        return .movedPermanently("data:text/html;base64,\(base64)")
    }
    try? localServer?.start(8245)
    UIApplication.shared.open(shortcutPage)
}

deinit {
    localServer?.stop()
    localServer = nil
}

Compared to the previous example, this will boot a localhost server that listens to /shortcut and redirects to the original data URL. The result of this is the gif at the beginning of the article, and you can add this to different screens of your app in order to add shortcuts to pre-defined deeplinks that your or other's apps supports.

Full project available here.

Pimp my PWA: Icons, Splash Screens and Conclusion

Now, while this works, it certainly doesn't look anything like the Shortcuts example! I didn't go too far in the looks department since I'm not a designer, but because this is essentially a PWA page you can infuse it with PWA properties in order to add things like home icons and splash screens.

You can find this info in the Apple docs, but you can use apple-touch-icon and apple-touch-startup-image tags to link to images in order to add this sort of content, as well as using some html tricks to make your pages look neater. You can see the example project or the decoded Shortcut to see how to achieve this, but the Shortcuts app uses the localhost server to provide images while the example project encodes an asset and provides the raw data.

To display the pretty instructions, the Shortcuts app generates an image on the fly. When it boots in PWA mode, it fetches the image from the local server and adds it to the body just like the previous SwiftRocks! example. It also has a fake version of the splash screen encoded into the html to make the transition between the page and the app smoother. In the end you're dealing with a HTML page, so anything goes!

In conclusion, if you have an app that contains important features, you can use this trick as an alternative to allow your users to create icons that go straight to them directly from their home screen.

Follow me on my Twitter - @rockthebruno, and let me know of any suggestions and corrections you want to share.

References and Good reads

Apple's PWA docs

Contact Info

Bruno Rocha works as Senior iOS Software Engineer at iFood and is the developer of several open sources libraries like SwiftInfo and SwiftShield.
bruno@swiftrocks.com

Newsletter

Click here to subscribe to my newsletter to get notified of new posts by e-mail.

RSS / Social

Info

This website's static HTML pages are generated by WriteIt.