Since iOS 9 has been out for almost 4 months, I realize that this post is a bit overdue, but since I still see a lot of misinformation on the subject bandied about on social media, I figure it is better late than never.
Here are some examples of misconceptions I still hear propagated by developers about the changes Apple made to the canOpenURL call in iOS 9:
“You can now only call up to 50 unique custom URL schemes in an app.”
“Apple made this change to kill app launching apps.”
In this post I will try to explain the changes made in iOS 9 that affect the canOpenURL call as clearly and concisely as I can in order to quell any remaining fear or false information out there on the subject. And then I’ll probably discuss more of the mundane details for developers out there with too much time on their hands.
In case you are unaware, every iOS app can register their own custom URL schemes in their Info.plist. A URL scheme is the first part of a URL and on the web is mostly using either “http” or “https”. However, URL schemes can be whatever you want and iOS uses them to launch one app from another app. Facebook uses “fb” and Twitter uses “twitter”. This means that if you load Safari and instead of the usual http:// or https:// you instead type in fb:// in the URL bar, you will launch the Facebook app. You can also launch the Facebook app from your own app if you use the openURL call and pass in “fb://”.
Now let’s say you want to be able to send a user in your app to your Facebook page and you feel that sending them to that page in the Facebook app, if they have it installed, is a better user experience than sending them to your Facebook web page in Safari. You would then need to know if the Facebook app is installed on the user’s device. That’s where the canOpenURL call comes in. You can call
and if it returns true, that means the user can open the “fb” custom URL scheme because they have the Facebook app installed.
For years there were no limitations on the canOpenURL call, but that changed in iOS 9.
What exactly changed?
In short, here is what changed in iOS 9:
In order to call UIApplication.sharedApplication().canOpenURL() in iOS 9, you must first declare the URL schemes that you want to check in your Info.plist, otherwise the call will return false even if the app is installed. There are some exceptions to this like the URL schemes that are “built-in” to iOS such as http, sms, tel, facetime, etc. So the declaration is needed for checking if third-party apps are installed such as Facebook or Google Maps.
To declare the URL schemes, you just need to add a new entry in your app’s Info.plist with the key “LSApplicationQueriesSchemes” and an Array of URL Schemes as the value. Here is an example:
<key>LSApplicationQueriesSchemes</key> <array> <string>fb</string> <string>comgooglemaps</string> <array>
There is a limit of 50 unique custom URL schemes that can be sent to canOpenURL before it starts failing, but that only applies to apps compiled for iOS 8 (XCode 6 and below) when they are run on iOS 9. If you compile an app for iOS 9 using XCode 7 and declare all of the custom URL schemes you want to check, there is effectively no limit.
Why was canOpenURL changed?
Well, only people at Apple knows for sure, but I think there is only one likely explanation.
I’ve heard some people draw the conclusion that Apple made this change in order to “kill app launching apps.” However, I can provide two good reasons why that isn’t the case.
Firstly, other than a bug in iOS 9 beta 1, this change only affects the call to canOpenURL to check if an app is installed and it does not affect the openUrl call. This means that any app launching app can continue to function without declaring any custom URL schemes or making any calls to canOpenURL at all. The only drawback would be that the app couldn’t check if the apps were installed first, but it doesn’t stop the app from launching other apps.
Secondly, I think this misconception is largely based on the aforementioned 50 unique URL scheme limit. However, as previously stated, the 50 unique URL scheme limit only applies to apps compiled for iOS 8 running on iOS 9.
Therefore, the more likely reason why this change was made was that Twitter and other apps were using canOpenURL to check what apps people had installed on their phone solely in order to better target their ads. With Apple’s focus on user privacy and this unintended use of this call, this explanation makes the most sense.
Requiring the declaration of every custom URL scheme you want to check in Info.plist means that every scheme is now exposed to the App Review team who can then reject apps if they appear to have no valid reason for checking if certain apps are installed. Previously the calls to check which apps are installed were buried in code or internal data files and App Review had no access to them. I haven’t heard of any cases of app rejections based on this, but I wouldn’t be surprised if it has happened. Plus, just the threat of rejection has likely caused the apps that were checking installed apps for ad targeting to cease that questionable behavior.
Although App Review could arbitrarily enforce some sort of limit on the number of URL schemes, I can report that my app, Launcher, declares and checks over 4,000 custom URL schemes and hasn’t yet had any run-ins with App Review (well, at least not with respect to checking URL schemes). So, at least for now, app launching apps and widgets appear to be safe.
Update: As of v3.0, released in May 2017, Launcher had close to 8,000 custom URL schemes listed under LSApplicationQueriesSchemes. When this version was originally submitted to Apple, one of the many things that App Review made me change was that I was no longer able to check all URL schemes to see which apps were installed, which was what the app had been doing upon first launch since v1.0. They allowed me to keep these 8,000 URL schemes in the LSApplicationQueriesSchemes list, but said that I could only check 20 of them upfront without user intervention. So if the user searched for apps for which to add launchers, I could then check the install state of each app in the search result list using canOpenURL. So there is still effectively no limit to the number of URL schemes you can list in LSApplicationQueriesSchemes, but App Review will check and limit the number of different URL schemes you can check using canOpenURL for any given user.
Fallout from this change
There are a few not-so-great side effects of this change that may be interesting to developers. Or not. Feel free to stop reading at this point if minute implementation details are not something you find fascinating.
The biggest side effect of this change is that since all schemes have to be declared in an app’s Info.plist, that means you have to release a new app to add new schemes. For most people this isn’t a big deal. But, for instance, my app, Launcher, was previously able to dynamically update its list of launchable apps which meant that I could add an app to the list and my users would see that app in their “installed launchable app” list immediately. Now I can still update the list dynamically, but the app can’t know if a newly added app is installed or not because the canOpenURL call will return false as its scheme is not declared in Info.plist. It also means that with new apps coming out all the time, if I want to keep the installed app list up to date, I need to update the app frequently on the App Store. And as developers know, pushing out a new version comes with downsides like resetting the app’s current ratings and reviews.
Another important side effect is that the canOpenURL call takes much longer than it did prior to iOS 9. The end result is that calling canOpenURL thousands of times prior to iOS 9 used to complete in less than a second and with iOS 9 it now can take over a minute, depending on hardware. This can have a negative impact in user experience if you need to check thousands of URL schemes when the app starts up because you may have to make the user wait until you have checked them all.
I filed a radar with Apple on this issue and it was closed, saying “This is an intentional behaviour change.” I don’t understand why this delay would be intentional other than the fact that the added checking is for some reason really inefficient and costly.
The last side effect is what I consider to be a bug (which still exists in iOS 9.3 beta 1) where every call to canOpenURL which returns false (because the app isn’t installed) will result in an error message being printed out, even if you have properly declared the URL scheme. The error message looks like this:
-canOpenURL: failed for URL: "fb://" - error: "(null)"
Now I can understand that there should be an error message if you make a call to canOpenURL for an undeclared URL scheme, and there is (in that case the error message is “This app is not allowed to query for scheme fb“). However, it makes no sense to print out an error message when there is no error. We made the call to see if the app was installed and it wasn’t. That’s not an error.
Most people probably aren’t bothered by this because they likely only check one or two URL schemes, but in my app launching app that checks thousands of apps, the console is inundated with error messages everytime I launch it. The worst part about it is that millions of CPU cycles and batteries are being needlessly wasted around the world due to an unnecessary message being logged. OK, so logging a message to the console isn’t that expensive, but it isn’t completely free either. If you feel this should be changed, please dupe my radar on this issue.