Core Telephony based location: Your Dual-SIM iPhone is… not fully supported?


Nowadays a lot of apps are changing their strategy to use of mobile numbers instead of usernames. Revolut is one example, but I believe that even WhatsApp is asking for your mobile number instead of a username to log in. It is reasonable, since the mobile text service (SMS) is a good candidate for building a Two-Factor authentication system around it. Getting the user’s mobile number at the first step making it really easy to use it with a one-time password.

From user experience it is really nice if you can immediately offer your user the (possible) country code, when the user opens the app. Even better if we can offer it without asking for the user the share her location (we all know how those privacy pop-ups are annoying, but that’s our life nowadays ;)). 

Marko has already made a blog post, where he showed an easy and popular technique how to read out the SIM card country identifier (ISO Country Code) with the help of the Core Telephony framework. Believe it or not, you can even use this framework on macOS, since Yosemite (version 10.10). Currently we are using almost the same solution on the project I am currently working on

Welcome to the world of Dual SIM (from iOS 12)

Before iPhone Xs (which was released with iOS 12), everything was nice and easy. All of the iPhones supported only one SIM provider or carrier. However from iPhone Xs all iPhone models are capable of Dual-SIM. You might ask how the heck, Peter? There is only one SIM slot on those devices! Yep, but with those devices Apple added the eSIM support. So there is one physical slot for the SIM card, and you can also use the eSIM (obviously if your cellular provide supports it). Since the introduction of iPhone 13 you can even use 2 eSIMs for the Dual-SIM mode. Here is the official Apple documentation about it:

The problem

Let see what issues we are facing:

  1. If we want to detect the user’s (possible) country or preferred country without requesting authorization to access to the user location, we couldn’t rely only on the telephony, since some iPad models, iPods or Macs running macOS probably doesn’t have SIM cards built in. So here we need some form of fallback.
  2. Handling Dual-(Multi-)SIM capabilities, and the lack of support from API point of view from Apple :/. Will point out for those things at the end of the article.

Initial solution with a fallback

Based on Marko’s solution I came up with my own modification. I made just one small change, that now the CTTelephonyNetworkInfo() is not instantiated with each function call, but we have a reference to it. Based on my experience, instantiating a this class is really heavy job, and if you need to use this function running on a bigger dataset, you will be definitely hit performance issues. I had already experienced it, where instantiating this class roughly 1000 times gave use 3-4 seconds (!) of delay in the app.

It is also worth mention that this API only available on iOS 12 and later, that’s why I added the @available directives.  Implementing a fallback solution is only one line of code, thanks to twostraws, there is a nice and easy solution, using the Locale

Things can go wrong with dual SIM

As you might already noticed, the code above is using the first element from the serviceSubscriberCellularProvider, and since the type of this property is a Dictionary. Dictionaries in Swift are not ordered, which means that you can have different first element when you have a different instance of the dictionary. 

Apart from the Swift technicalities, we also have some iOS related uncertainty. Since now you can have more than one provider, the Mobile data screen in the Settings are giving you the opportunity to set which provider do you want to use for mobile data, and which one do you want to use as a Default Voice Line. That allows a flexible configuration, since you can now use basically any eSIM based data provider (for example while you are traveling), while still have your good old number ringing on your Apple device when someone needs to reach you over the phone.

iOS Settings screen, showing the Mobile plans with a Hungarian and Maltese number, and the possibility to select the Mobile Data and the Default Voice Line

Having said that, I was wondering how to distinguish between the different carriers from the dictionary of serviceSubscriberCellularProvider to figure out which one can be the default one? My assumption was that I would select the carrier, which is set as the default phone line, since in my opinion the probability that your voice line probably more tied to the actual country where the user lives. 

My confusion was even growing bigger when I realized that the Core Telephony framework provides little to nothing help identifying the default voice line. Actually at the time of writing there is no way to query that setting via a public Core Telephony API.

The closest we can use is the dataServiceIdentifier, but that’s only identifying the current data provider, not the voice provider, and only available from iOS 13. So there is still a gap for devices with iOS 12 (although those should have been already upgraded to iOS 13 in theory).

The Silver Bullet

While I was writing this article and trying to came up with some alternative solution, I wrote down a note to check the Contacts framework, since in the iPhones with multiple carrier you are able to select on each contact individually the provider you want to use (the default value is the last used one for that particular contact). 

Carrier selection on a contact
Selecting a preferred line for the contact, with the the option of always using one of the existing carrier, or using the one, which was used the last time for this contact.

Although I couldn’t find a public API to query this property, my curiosity (and my luck) helped me to find the CNContactsUserDefaults class, which has the countryCode, to the rescue! Worth mentioning that it is supported back to the iOS 9 and macOS 10.11, and (unlike the Locale) it is returning a String, not an Optional String! The only caveat that you need to import the Contacts framework instead of the CoreTelephony.

So next time, if you need to figure out the user current country code, you can do it like that:

PS.: Dear Apple, I think it would be still great if we could have public APIs for:

  • Reading out the Default Voice Line from the Core Telephony framework
  • Reading out the preferred line (probably as part of the CNContact struct) in the Contacts framework.

Thanks for reading (and Apple engineers for their good work), sincerely:

If you like my articles, or you think that you can add more value to them, feel free to say “Hi!” to me on Twitter, or get in touch on LinkedIn.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.