How-to: Creating Custom Smart Banners for iOS Apps Using Universal Links

Smart Banners are an iOS native method of promoting App Store apps by displaying a banner on a website simply by adding a meta tag.

A screenshot of an example smart banner

Using this method has quite a few distinct advantages over implementing your own solution:

  1. Simple implementation: A single meta tag is all you need
  2. Consistent look: You can expect the banner to look the same on all devices
  3. No JS/CSS/HTML injection: The banner isn’t actually injected into the website, but instead displayed as an overlay by Safari. This avoids potentially messy interactions with the website
  4. Knows if the app is already installed: Since Safari is part of iOS, it can display either a “View” or “Open” link depending on whether you have the app installed
  5. Affiliate program built-in: Promote your app by incentivizing publishers to show your smart banner on their websites (more info at
  6. Deep link support: You can have the banner’s “Open” button deep link into a particular part of the app

All in all, a very powerful tool compared the small amount of implementation work. A full implementation guide can be found here.


For brand-conscious folks, advantage number 2) can actually be a downside. You can’t customize the look of the smart banner at all. Your only alternative is to style your own banner and conditionally display with with some javascript and cookies. I’m not going to cover that here since it’s not my lane. Rather, I’d like to talk about how build a link for your custom smart banner that will open the app if it is installed (with optional deep linking) or the app store if it isn’t, using a feature called Universal Links.

If you’re already familar with Universal Links, feel free to skip to the next section. Otherwise, here’s a little bit of background and history.

Apple has supported Inter-App Communication using custom URL schemes for a long time. You can use them to deep link into your app from push notifications or other apps. Originally, you could even figure what apps were installed on a user’s device by querying if a particular scheme could be opened. Apple eventually had to bring the boom down after some developers started exploiting this feature, and custom schemes are more limited now. Another downside of schemes is that, unlike with App IDs for example, there’s no infrastructure in place to make sure only you are using your custom scheme. And there’s no procedure in place for what happens if two apps register the same scheme. And finally, if no app is registered to handle a particular scheme, iOS just shows you an ugly error message.

To work around all this and more, Apple introduced Universal Links in iOS 9. Rather than using custom schemes now, you can use regular https links that are backed by a real website. Universal Links improve upon schemes in a few ways:

  1. Security: Association between an app and a web site has to be established bi-directionally, from app to site and vice-versa
  2. User Experience: If the app is not installed, the user will be able to interact with the website instead
  3. Customizability: The app can be associated with just a subset of pages on a domain, and more than just one domain

There are plenty of guides on how to get started with Universal Links, for example at Apple, or here. even has a validator for the Apple App Site Association file you have to create. I’d recommend checking these out if you’re unfamiliar with Universal Links.

There are four steps to creating the link for your custom smart banner. I’ll cover each in turn and explain the how and why.

Step 1: Create a separate (sub)domain

Let’s say your website is at If you want to send the user to your app, redirecting to a URL on the same domain is not enough. For usability reasons, the user is required to initiate the interaction (e.g. tap a link), and the target URL must be in a different context (i.e. a different domain or subdomain). We will use

Step 2: Create a page that redirects the user to the app store

We need a page that will guide the user to the app store if the they don’t have your app installed. Ideally, this page should have some relevant content and a link to the app store (like a marketing page for your app) in case the user declines to be sent to the App Store, but it can also just serve the redirect via HTTP header. In my testing, the latter method caused the browser to stay on the page with the smart banner, but your mileage may vary.

As the URL for this page, we will use Important: This page must be served via HTTPS. Thanks to, that can be done for free now.

Step 3: Create Apple App Site Association file

This is a special JSON file that allows a web site to declare which URLs can be opened in an associated app (and a few other things). This file must be uploaded to Just like the page in step 2, HTTPS is required.

Here’s how the file would look in our case. The appID is your team ID (also know as prefix) followed by a dot and the bundle ID. Check Apple’s Universal Links Guide for more information. You can validate the resulting file here.

    "applinks": {
        "apps": [],
        "details": [
                "appID": "ABCD1234.appBundleId",
                "paths": ["/app-store"]

Step 4: Update App capabilities

Finally, the app needs to declare association with the domain from step 1. To do this, open the project in Xcode, go to “Capabilities”, enable “Associated Domains”, and add the following entry based on the domain from step 1: “”

That’s it.

You can now test your custom smart banner by deploying the app to a device and visiting a page with the banner in Safari.

iOS App Distribution Methods Explained

Apple is very particular about how 3rd party software gets on their devices. Basically, the more users you want to distribute the app to, the more hoops you have to jump through. It breaks down like this:


You can build and run any software on your own device, as long as it’s attached to your computer. You don’t even need a paid account.

Development Team

You can build any software and distribute it over the air to members of your development team (everyone registered on the dev portal as a member or admin). Requires a paid account, and the number of people is severely limited. If you want to add people, you have to create a new build. Builds expire after at most a year, usually much faster, depending on how the expiration times of the dev certificate and provisioning profile line up.


You can build any software and distribute it over the air to a set of up to 100 devices (registered on the dev portal by device ID). Requires a paid account, and once a device has been added to the list, it can’t be removed until the development program membership renewal date. Each build contains the list of devices is can be installed on, so adding devices requires creating a new build. Builds expire after at most a year, usually much faster, depending on how the expiration times of the ad-hoc certificate and provisioning profile line up.


You can build software, but it must pass a rudimentary check when uploaded. You can freely distribute it over the air to up to 25 internal users. If you want to distribute to external testers (up to 10,000) it has to pass a beta review. Builds expire after 90 days.

App Store

You can build software, but it must adhere to the App Store Review Guidelines and some other requirements and pass a review. Can then be downloaded by everyone from the App Store. Only the latest version is available though. Requires paid account. Unlimited users and automated distribution.

Dev team and ad-hoc are the only methods here that enable over-the-air side-loading.


All of the above use the regular Developer Program, which open to anyone who can pay $99/year (personal accounts are free). Enterprise accounts work a little different. It costs more money ($299/year), the setup process is more arduous, and you can’t distribute apps through TestFlight nor the App Store. The upside is that you can distribute pretty much anything over the air, once you have a distribution server set up. However, any device you install your app on has to be either managed through MDM or requires explicitly enabling a device management profile before you can run the app.

How-to: Silence individual warnings for select cocoapods

As usual, Xcode 9 introduced new default warnings and tightened existing ones. Some of these changes don’t seem to get applied to existing projects during the upgrade check. For better or worse though, running pod install or pod update has the effect of setting warnings for all pods like it’s a new project. This led to me seeing a lot of warnings in the form of 'RandomCocoaTouchAPI' is only available on iOS 10.0 or newer in certain pods. The build setting responsible is CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE.

Since there’s no telling when these warnings are going to be fixed, if ever, I used the following post_install snippet in my Podfile to manually relax just this setting on the offending pods. It’s a better solution than suppressing all warnings. It also prints out a notice to remind me to check later if the warnings have been fixed.

post_install do |installer|
  # Manually relax CLANG_WARN_UNGUARDED_AVAILABILITY for some pods temporarily
  unguardedAvailabilityTargets = ['FBSDKCoreKit']
  installer.pods_project.targets.each do |target|
    if unguardedAvailabilityTargets.include?
      target.build_configurations.each do |config|
        config.build_settings['CLANG_WARN_UNGUARDED_AVAILABILITY'] = 'YES'
      puts "CLANG_WARN_UNGUARDED_AVAILABILITY was set to YES for #{}"

If you don’t care to enumerate individual pods, you can also change the setting at the pod project level.

post_install do |installer|
  installer.pods_project.build_configurations.each do |config|
    config.build_settings['CLANG_WARN_UNGUARDED_AVAILABILITY'] = 'YES'
    puts "CLANG_WARN_UNGUARDED_AVAILABILITY was set to YES for all pods"

How-to: Integrating Google Analytics into iOS apps

Google recently reorganized their Cocoapods offerings, moving components like their analytics package back into its own pod and deprecating the Google pod in the process.

It would have been a good time to redo their integration docs as well, but unfortunately, they are still outdated and incomplete.

I’d like to quickly go over how I incorporate Google Analyics into iOS app nowadays.

First, add the tracking ID to your info.plist.

A screenshot of Xcode's plist editor


Next, add pod to your Podfile, run pod update, and then add the necessary includes to your bridging header file.

#import <GoogleAnalytics/GAI.h>
#import <GoogleAnalytics/GAIDictionaryBuilder.h>
#import <GoogleAnalytics/GAIFields.h>
#import <GoogleAnalytics/GAIEcommerceFields.h>

The existing docs tell you to guard against misconfiguration like this:

// wrong
guard let gai = GAI.sharedInstance() else {
  assert(false, "Google Analytics not configured correctly")

Unfortunately, this will break as soon as you do a release build, since assertions are removed in release configurations, and a guard block must end execution of the current scope. Here’s a better solution:

if let gai = GAI.sharedInstance() {
    // configure GAI here
} else {
    assertionFailure("Google Analytics not configured correctly")

You still get the assertion helping you out while debugging, without running into problems later on.

Next you’ll want to some basic configuration.

    let gai = GAI.sharedInstance(),
    let gaConfigValues = Bundle.main.infoDictionary?["GoogleAnalytics"] as? [String: String],
    let trackingId = gaConfigValues["TRACKING_ID"]
    gai.logger.logLevel = .error
    gai.trackUncaughtExceptions = false
    gai.tracker(withTrackingId: trackingId)
    // gai.dispatchInterval = 120.0
} else {
    assertionFailure("Google Analytics not configured correctly")

When you first start integration, I recommend setting the log level to verbose. You could even schemes or your build configurations to set it to different values as needed.

Similarly, I wouldn’t change the dispatchInterval from the default, unless you’re actively working on your analytics code and need events to show up quicker in the reporting dashboard.

If you want google analytics to record uncaught exceptions, you can enable this feature here. However, be aware that this will interfere with other crash reporting libraries like Crashlytics. If you use one of them or another library that registers exception handlers, set trackUncaughtExceptions to false or initialize them after Google Analytics so the exception handler can be reset.

That should cover the basics, but I’ve also included an Analytics helper struct below. It’s similar to what I use in my apps. Using enums for actions and screen names helps to prevent typos from creeping in.

struct Analytics {
    static func trackEvent(withScreen screen: Screen, category: String, label: String, action: Actions, value: Int? = nil) {
            let tracker = GAI.sharedInstance().defaultTracker,
            let builder = GAIDictionaryBuilder.createEvent(withCategory: category, action: action.rawValue, label: label, value: NSNumber(integerLiteral: value ?? 0))
        else { return }

        tracker.set(kGAIScreenName, value: screen.rawValue)
        tracker.send( as [NSObject : AnyObject])

    static func trackPageView(withScreen screen: Screen) {
            let tracker = GAI.sharedInstance().defaultTracker,
            let builder = GAIDictionaryBuilder.createScreenView()
        else { return }

        tracker.set(kGAIScreenName, value: screen.rawValue)
        tracker.send( as [NSObject : AnyObject])

    enum Actions: String {
        case search = "Search"
        case tap = "Tap"
        case toggle = "Toggle"

    enum Screen: String {
        case exampleScreenName = "exampleScreenName"

Live scroll notifications from NSScrollView

Prior to OS X 10.9, observing NSScrollView scroll events involved either subscribing to bounds changes on the accompanying NSClipView or subclasses the scrollView and overriding scrollWheel(_:). In 10.9, Apple introduced responsive scrolling and a whole host of related improvements. One of those are LiveScrollNotifications. Live Scroll Events are user-initianted (via touchpad, mouse-wheel, etc) scroll events.

There are 3 available notifications:


The major caveat that’s unfortunately not mentioned in the documentation (only in this WWDC session), is that only NSScrollViewDidLiveScrollNotification is fired universally. The start and end notifications are not triggered if scrolling with a mouse wheel for example. The upside is that there’s no subclassing or messing with unrelated views involved. Just register for the notifcation and pass the scrollView as notificationSender.

    selector: #selector(scrollViewDidScroll(_:)),
    name: NSScrollViewDidLiveScrollNotification,
    object: scrollView

Generic read/write locks in Swift

About 18 months ago, Mike Ash had a detailed look at locks and thread safety in Swift. I’ve been thinking a lot about threading lately because of a side project I’m working on, so I thought I’d try to build a generic version of a dispatch queue lock.

You can find it here. Parse and have good general primers on concurrency in Apple platform development.

Some caveats:

@inline(__always) func with<T>(queue: dispatch_queue_t, @autoclosure(escaping) get block: () -> T) -> T {
    assert(dispatch_queue_get_label(queue) != dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), "Invoked dispatch_sync in a way that will deadlock")
    var result: T!
    dispatch_sync(queue) {
        result = block()

    return result

First, the autoclosure here is marked as escaping even though it clearly ought to be non-escaping. Unfortunately, dispatch_sync’s function signature doesn’t include the @noescape marker, and thus can’t be used in this way. This appears to have been fixed in Swift 3. I think I have to mark result as force-unwrap for the same reason, but I won’t know until I get a chance to play with Swift 3.

Second, since dispatch_sync doesn’t return until the block you pass has been executed on the queue, calling in from the same queue you’re dispatching to will deadlock your program. The assert will catch some, but not all such calls. For example, it doesn’t know about target queues. It’s mainly meant for manually created queues.

Chaotic alignment bug in Metal shaders

All I wanted to do was put some texture on a cube. 😒

This doesn't look right

I ported the Metal tutorial at to OSX and came across this bug in the shader compiler backend, leading to distored textures.

When using a VertexIn struct with packed floats such as

struct VertexIn {
    packed_float3 position;
    packed_float4 color;
    packed_float2 texCoord;

the shader compiler backend will generate invalid offsets for the struct member texCoord. This seems to be related specifically to the combination of packed_float3 and packed_float2 since:

  • using packed_float4 fixes the issue
  • color is unaffected
  • reordering the members produces different invalid output
  • manually calculating the correct texCoord member offset fixes the issue

The issue is specific to Nvidia 750M cards as found in mid 2014 rMBPs. Running the same code on the built-in Intel Iris Pro produces valid output. From reports, the 650M is not affected either, but I was unable to verify that myself.

Big thanks to @warrenm for helping to diagnose and providing the workaround code.

I submitted the bug to Apple at rdar://24249981. You can find the code I used at Github.

Games I've Played In 2015

One of my low-effort side projects this year was writing down which games I’ve played and if I’ve actually completed them. I didn’t write down any thoughts after I played them because I wanted to see how much I actually remember at the end of the year. This is list is roughly in the order of when I started playing the games.

League of Legends (PC)

Hey look, I’m still playing League of Legends.

Secret of Mana (Emulator) (finished)

I’m pretty sure I played this game on the SNES way back when, but I don’t think I finished it before. I enjoyed it overall, despite the sometimes clunky combat mechanics and the grinding necessary for leveling up weapons and spells.

Xenonauts (PC) (finished)

I’m a big fan of the original X-COM: UFO Defense and X-COM: Terror from the Deep. They’re two of the few games I’ve played through more than once. I kickstarted Xenonauts and was pretty happy with the game overall. It hit all the same spots X-COM did, except for somewhat forgettable music (I had to double check whether the game actually had music) and an overall sterile feel.

Asura’s Wrath (PS3) (finished)

Punching Your Problems Until They Go Away: The Game: The Anime: The Movie

The game started entertainingly enough, due to the bombastic opening and generally entertaining gameplay. However, once they novelty wore off, gameplay became quite tedious and repetitive, with the story being low on character development and leaving me ultimately unsatisfied despite 3 consecutive endings.

Fire Emblem: Awakening (3DS)

I really enjoyed this game, but since I was playing on a borrowed 3DS, I’ve haven’t gotten around to finishing it yet.

Shadow of Mordor (PC) (finished)

Orc Murder Simulator of the Year. I found this game to be an enjoyable alternative to the Assassin’s Creed/Arkham Batman kind of games. Story was heavy on the tropes.

Elite: Dangerous (PC) (MMO)

Best space game I’ve played in a long time. I put significant time into combat and exploration. Mining and trading seemed to inaccessible to be enjoyable for me. The biggest problem for me was the lack of mission variety and that’s what kept me from checking out the expansion so far.

Dying Light (PC) (finished)

Pretty enjoyable open world zombie killing game. The story was as dumb as could be excepted and got much dumber than that in the last few hours.

Tales of Xilia (PS3) (finished)

Easily one of the best JRPGs I’ve played. Likeable cast, funny dialogue, fast-paced low-grind combat, and a story that actually makes sense! Visuals and voice work were top notch as well.

Homeworld HD Remaster (PC)

I love all the Homeworld games, so I acutally pre-ordered this HD remaster. For better or worse, that’s all it is though. They faithfully restored every aspect of the original games, including the pacing issues of some of the missions and the lackluster AI. I fully intended on finishing at least the first game. However, I ran into a showstopping bug roughly halfway through the campaign and sort of forgot about it while waiting for a patch. I intend to revisit it next year.

Bastion (PC) (finished)

Somehow, I had to start playing this game three times over the past 3 years, but I finally finished it! I had a great time, although I was unmoved by the ending. (I generally don’t enjoy time travel based plots)

Xenosaga Episode I (PS2)

I actually wish I had taking some proper notes while I was playing this game. I guess I sort of did, on Twitter:

I got about 3/4 through the game before abandoning it. The mecha component of the battle system was pointless to me, but mainly the plot was just a mess of arglebargle. I read quite a few pages on Wikia about the whole thing afterwards, and it just confirmed that I made the right choice.

Final Fantasy Type-0 HD (PS4)

I actually really liked what I played, but felt like I was missing too much stuff without a guide. I’ll probably pick this up for PC at some point and get a strategy guide.

Bloodborne (PS4) (finished)

I’m a sucker for the Souls games, and this one was no different. Interesting new setting and aesthetic. I hope they’ll make another.

Rogue Legacy (PC) (rogue-like)

I put a few hours into it, but the difficulty is pretty unforgiving and I didn’t get very far.

Mortal Kombat X (PC)

I actually enjoyed the over-the-top story antics of the previous installment, but couldn’t get into this one because of technical problems.

Digital: A Love Story (PC) (finished)

I loved it! I’m looking forward to playing more visual novel type games next year.

Rise of the Triad (2013) (PC)


Alien Breed 3 (PC)

Solid top down shoot’em’up, but I just couldn’t get myself to finish the series.

Car Mechanic Simulator 2015 (PC) (sandbox)

I wanted to try one of the endless number of *** Simulator games and actually found this one to be pretty engaging. Interface could have been a lot better though.

GTA V (PC) (finished)

I hate-played this game. I was curious about the triple protagonist mechanic and the heists, so I just tried to finish the story as quickly as possible. Predictably, all characters I encountered were unlikable assholes and there was little I found very enjoyable besides the driving.

Kerbal Space Program (PC) (sandbox)

Spaaaaaaaaaaaaaace. A great, kid-friendly game. I wish it were better explained/tutorialized though, especially when you’re playing with a child. They tend to get bored when you have to read a lengthy guide in order to figure out how to do basic things.

Carmageddon: Reincarnation (PC)

Solid sequel to the oddball original games. Haven’t had a chance yet to check out the major update they put out recently.

Legend of Grimrock (PC) (finished)

Great (difficult) tile-based dungeon crawler. Looking forward to the sequel.

Banished (PC) (sandbox)

Dense, kind of bland village builder. Mechanically solid, but I didn’t find it very engaging.

Witcher 2 (PC)

This was the second time I started playing this game, and once again I didn’t get past the first act. This time, I tried to play with a mod that was supposed to improve the combat, but all it did was fuck up my level progression. Maybe third time’s the charm?

TIS-100 (PC)

Pretty spot on. I need to give this another shot now that it’s out of early access.

Super Mario 3D World (Wii U) (finished)

I’ve hugely enjoyed this game. Art style, music, visuals, level design (mostly), co-op. Super Mario Galaxy was the last one I enjoyed this much.

Captain Toad Treasure Tracker (Wii U)

Very cute, super-fun puzzler. It somehow ended up in a moving box in a storage unit, so I haven’t been able to finish it yet.

System Shock 2 (PC) (finished)

Despite playing it shortly after it was first released, I had never finished it. I played it with a few mods to improve the visuals and it hold up nicely. I still prefer System Shock 1 though and can’t wait for the HD remaster.

Rising Thunder Beta (PC)

This was billed as an effort to make fighting games more accessible. It failed for me, but I might give it another shot once it leaves beta.

Final Fantasy X HD (PS4) (finished)

Despite being generally too grindy, it’s still a great RPG that mostly holds up mechanically. I had played this on PS2 before, and found the story made a lot of sense this time around.

Command & Conquer (PC)

A classic that simply doesn’t hold up. The interface is clunky and the missions take too long as a result.

Command & Conquer: Red Alert (PC)

See above.

Monument Valley (iOS) (finished)

Beautiful puzzler. Too short, if anything.

Steven Universe: Attack the Light (iOS) (finished)

The only game I 100%-ed this year.

World of Warships (PC)

Enjoyable timesink. The biggest advantage over MOBAs is the short game length, IMO.

Guild Wars 2 (PC) (MMO)

I liked what I played, but I just can’t get into MMOs without friends to play with.

Demon’s Souls (PS3)

I wanted to revisit the original Souls game. It’s still great and I might finish it next year.

Metro Last Light Redux (PC)

Not much to say here. It’s a good FPS, but it didn’t hold my attention long enough to finish it.

SOMA (PC) (finished)

Despite an absence of jump scares, this game manages to create such heavy tension that I had to play parts of it during the day. I loved the story and enjoyed the exploration of humanity and identity.

Avadon (PC)

I’ve been following Jeff Vogel’s blog for a while now, so I thought it was time to give one of his indie RPGs a try. I ended up putting about 34 hours into it, but the draw of the story was not strong enough to get me to finish it.

Starcraft 2: Legacy of the Void (PC) (finished)

The missions were mostly great. The story was mostly sterile filled characters that were exeedingly hard to care about.

Her Story (PC) (finished)

My wife and I played this one night and got to the conclusion in about 2 hours. We watched fewer than 50% of the available clips (according to achievements), not we might be missing a significant part of the narrative. Nevertheless, we really enjoyed it.

Fallout 4 (PC)

Without mods, I would probably not be playing this game anymore. I didn’t encounter any gamebreaking bugs, mostly just humorous glitches. However, the interface is atrocious, especially with regards to inventory management and dialogue options. Storywise, they removed a lot of the nuanced interactions in favor of making an action-RPG shooter that is easy to get lost in (often in a good way). It’s good that western RPGs are going through a revival right now and there are richer options to choose from.

Resident Evil Revelations 2 (PS4) (finished-ish)

I only got the bad ending so far (which I only realized when I checked achievements). Resident Evil games occupy a weird spot for me. The villains are universally cartoonish, unsympathetic, poorly motivated, and their actions usually unjustifiable. The clunky interface is often just as dangerous as the monsters, and the boss fights are mostly hit or miss. So I have to ask myself what keeps me coming back. This installment was definitely my favorite out of the last 3 I played (RE6, Rev1, Rev2). I liked the protagonists (especially Moira), the improved interface, inventory management, and the buddy system. Loading times were pretty bad though.

Tagged Pointers in Objective-C

On Tuesday, I gave a tech talk about the use of tagged pointers in Objective-C at the local iOS developer meetup. You can find the slides, demo, and supporting docs at Github.