Get device location (only country) in iOS
Asked Answered
E

16

81

I need to get the country location of a iOS device.

I've been trying to use CoreLocation with MKReverseGeocoder. However this seems to return erraneous quite frequently. And I only need the country, no need for streets and such.

How can this be done in a more stable way?

Exult answered 16/12, 2011 at 12:58 Comment(0)
S
51

NSLocale is just a setting about currently used regional settings, it doesn't mean the actual country you're in.

Use CLLocationManager to get current location & CLGeocoder to perform reverse-geocoding. You can get country name from there.

Scintilla answered 16/12, 2011 at 13:8 Comment(4)
Do you have any solution for iOS 4?Exult
for iOS4 you can get your current coordinates lat&long and use some Google web-service for reverse geocoding from here code.google.com/apis/maps/documentation/geocodingScintilla
Here's a good tutorial: jonathanfield.me/legacy/jons-blog/clgeocoder-example.htmlLamina
This is a good answer and is the way I ended up doing it when I had to solve this problem. FYI, I posted an answer of my own below with some sample code for the actual implementation, to help save people a little time reading the documentation to figure out how to implement this solution...Fulgurate
T
122
NSString *countryCode = [[NSLocale currentLocale] objectForKey: NSLocaleCountryCode];

will get you an identifier like e.g. "US" (United States), "ES" (Spain), etc.


In Swift 3:

let countryCode = NSLocale.current.regionCode

In Swift 2.2:

let countryCode = NSLocale.currentLocale().objectForKey(NSLocaleCountryCode) as String

Compared to a solution based on CLLocationManager this approach has pros and cons. The primary con is that it doesn't guarantee that this is where the device is physically if the user configures it differently. This can however also be seen as a pro since it instead shows which country a user is mentally/culturally aligned with - so if e.g. I go abroad on vacation then the locale is still set to my home country. However a pretty big pro is that this API doesn't require user permission like CLLocationManager does. So if you haven't already gotten permission to use the user's location, and you can't really justify throwing a popup dialog in the user's face (or they already rejected that popup and you need a fallback) then this is probably the API you want to use. Some typical use cases for this could be personalization (e.g. culturally relevant content, default formats, etc.) and analytics.

Transpontine answered 16/12, 2011 at 13:49 Comment(4)
Hi Martin. Will this retrieve the actual location the device is in, or the language the device is set to. Or is there indeed a 'region' of the device which is separate to the 'language'?Loxodrome
On your iPhone go to Settings->General->International. There you can change the Language, Region Format (a more user friendly term for locale) and Calendar (Gregorian, Japanese or Buddhist) separately. Different combinations are possible - for example I as a Dane still prefer an English interface, but a Danish locale (so the week starts on monday and I want dd/mm/yy because mm/dd/yy is just weird ;) This is not influenced by the physical location of the device (I don't want it to suddenly be Italian, just because I'm on vacation...)Transpontine
This has nothing to do with the location, only language.Exult
To be clear, the identifier returned by NSLocaleCountryCode will be the user's "Region Format". The Language is independent of this. Ultimately, your preferred date/time/phone number format, i.e. Region Format, is an indicator of where you might be from, but it is certainly not definitive.Gym
S
51

NSLocale is just a setting about currently used regional settings, it doesn't mean the actual country you're in.

Use CLLocationManager to get current location & CLGeocoder to perform reverse-geocoding. You can get country name from there.

Scintilla answered 16/12, 2011 at 13:8 Comment(4)
Do you have any solution for iOS 4?Exult
for iOS4 you can get your current coordinates lat&long and use some Google web-service for reverse geocoding from here code.google.com/apis/maps/documentation/geocodingScintilla
Here's a good tutorial: jonathanfield.me/legacy/jons-blog/clgeocoder-example.htmlLamina
This is a good answer and is the way I ended up doing it when I had to solve this problem. FYI, I posted an answer of my own below with some sample code for the actual implementation, to help save people a little time reading the documentation to figure out how to implement this solution...Fulgurate
F
15

@Denis's answer is good -- here is some code putting his answer into practice. This is for a custom class that you have set to conform to the CLLocationManagerDelegate protocol. It's a little simplified (e.g. if the location manager returns multiple locations, it just goes with the first one) but should give folks a decent start...

- (id) init //designated initializer
{
    if (self)
    {
        self.locationManager = [[CLLocationManager alloc] init];
        self.geocoder = [[CLGeocoder alloc] init];
        self.locationManager.delegate = self;
        [self.locationManager startMonitoringSignificantLocationChanges];
    }
    return self;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    if (locations == nil)
        return;

    self.currentLocation = [locations objectAtIndex:0];
    [self.geocoder reverseGeocodeLocation:self.currentLocation completionHandler:^(NSArray *placemarks, NSError *error)
    {
        if (placemarks == nil)
            return;

        self.currentLocPlacemark = [placemarks objectAtIndex:0];
        NSLog(@"Current country: %@", [self.currentLocPlacemark country]);
        NSLog(@"Current country code: %@", [self.currentLocPlacemark ISOcountryCode]);
    }];
}
Fulgurate answered 30/5, 2014 at 0:32 Comment(0)
E
13

Here is @Denis's and @Matt's answers put together for a Swift 3 solution:

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    let locationManager = CLLocationManager()
    let geoCoder = CLGeocoder()

    override func viewDidLoad() {
        super.viewDidLoad()

        locationManager.requestAlwaysAuthorization()
        if CLLocationManager.locationServicesEnabled() {
            locationManager.delegate = self
            locationManager.startMonitoringSignificantLocationChanges()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let currentLocation = locations.first else { return }

        geoCoder.reverseGeocodeLocation(currentLocation) { (placemarks, error) in
            guard let currentLocPlacemark = placemarks?.first else { return }
            print(currentLocPlacemark.country ?? "No country found")
            print(currentLocPlacemark.isoCountryCode ?? "No country code found")
        }
    }
}

Don't forget to set the NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription in Info.plist as well!

Embroideress answered 28/12, 2016 at 21:11 Comment(5)
I am trying to do this, but it does't seem to be calling this funt locationManger, and therefore I am not getting anything in the console to print out. Do i need to call this method somewhere?Bloodstone
It should be called automatically every time the location is updated in the device. Make sure that the locationManager.delegate is set to self as I did above and that you add the CLLocationManagerDelegate to the class on the top.Embroideress
Sorry, if you are working with swift 3 there should be an underscore in that function func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) I will update the original post to clarify thisEmbroideress
@Embroideress Sorry, I know this is a stupid question, but is there a way to create a variable to store the value of « currentLocPlacemark » ? I am trying to access it's value to do different things if the user is in a different country but it won't let me. Thank you!Baler
I’m pretty sure “currentLocPlacemark.isoCountryCode” and “currentLocPlacemark.country” are stored as strings which you could store in variables and do different things with depending on what you need to do. What is the error you get?Embroideress
H
11

As mentioned by @Denis Locale is just a setting about currently used regional settings, it doesn't mean the actual country you're in.

However, suggested use of CLLocationManager to get current location & CLGeocoder to perform reverse-geocoding, means prompting user access to Location Services.

How about getting country code from mobile carrier?

import CoreTelephony

guard carrier = CTTelephonyNetworkInfo().subscriberCellularProvider else {
    //iPad
    return
}

let countryST = carrier.isoCountryCode!
Henhouse answered 12/5, 2020 at 8:3 Comment(2)
subscriberCellularProvider has been deprecated, use CTTelephonyNetworkInfo().serviceSubscriberCellularProviders dictionary insteadDight
serviceSubscriberCellularProviders is also deprecated :(Asoka
H
10

Here's an alternative, perhaps overly circuitous method. The other solutions are based on manual settings (NSLocale) or on requesting for permission to use location services which can be denied (CLLocationManager), so they have drawbacks.

You can get the current country based on the local timezone. My app is interfacing with a server running Python with pytz installed, and that module provides a dictionary of country codes to timezone strings. I only really need to have the server know the country so I don't have to set it up entirely on iOS. On the Python side:

>>> import pytz
>>> for country, timezones in pytz.country_timezones.items():
...     print country, timezones
... 
BD ['Asia/Dhaka']
BE ['Europe/Brussels']
BF ['Africa/Ouagadougou']
BG ['Europe/Sofia']
BA ['Europe/Sarajevo']
BB ['America/Barbados']
WF ['Pacific/Wallis']
...

On the iOS side:

NSTimeZone *tz = [NSTimeZone localTimeZone];
DLog(@"Local timezone: %@", tz.name); // prints "America/Los_Angeles"

I have my server send in the local timezone name and look it up in the pytz country_timezones dictionary.

If you make an iOS version of the dictionary available in pytz or some other source, you can use it to immediately look up the country code without the help of a server, based on timezone settings, which are often up to date.

I may be misunderstanding NSLocale though. Does it give you the country code through regional formatting preferences or timezone settings? If the latter, then this is just a more complicated way of getting the same result...

Harmattan answered 19/7, 2012 at 23:23 Comment(1)
Sounds good. But I justed checked my iPad Time Zone (set automatically) and it shows Singapore, while I stay in Malaysia (300 miles from Singapore).Erotomania
T
6

Use the CoreTelephony method as a fallback if Locale.current.regionCode doesn't work for some reasons.

import CoreTelephony

if let carriers = CTTelephonyNetworkInfo().serviceSubscriberCellularProviders?.values, let countryCode = Array(carriers).compactMap { $0.isoCountryCode }.first {
    print("❤️ \(countryCode)")
}
Tartarus answered 16/11, 2021 at 18:18 Comment(1)
isoCountryCode: Deprecated: 16.0, message: "Deprecated; returns '--' at some point in the future")Reichel
P
5
NSLocale *countryLocale = [NSLocale currentLocale];  
NSString *countryCode = [countryLocale objectForKey:NSLocaleCountryCode];
NSString *country = [countryLocale displayNameForKey:NSLocaleCountryCode value:countryCode];
NSLog(@"Country Locale:%@  Code:%@ Name:%@", countryLocale, countryCode, country);
//Country Locale:<__NSCFLocale: 0x7fd4b343ed40>  Code:US   Name:United States
Periapt answered 17/3, 2015 at 10:49 Comment(0)
H
5

I managed to get the country without asking for location permissions using the following approach:

import MapKit

class CountryDectectorViewController: UIViewController {
    var didDetectCountryCode: ((String?) -> Void)?

    override func viewDidLoad() {
        super.viewDidLoad()
        // Map view setup
        let mapView = MKMapView()
        view.addSubview(mapView)
        mapView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        mapView.layoutIfNeeded()
        // Reverse geocoding map region center
        let location = CLLocation(
            latitude: mapView.region.center.latitude,
            longitude: mapView.region.center.longitude
        )
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, _ in
            self.didDetectCountryCode?(placemarks?.first?.isoCountryCode)
        }
    }
}

To ISO Country Code can be then obtained using:

let controller = CountryDectectorViewController()
controller.loadViewIfNeeded()
controller.didDetectCountryCode = { countryCode in
    print(countryCode)
}

Some context

I realised that UIKit has this information already because everytime the MKMapView is shown, the region is automatically set to fit the current user's country. Using this hypothesis I needed to find a solution to load the map without presenting it and then to reverse geocode the center coordinates to identify the country.

I implemented this solution taking into consideration the following limitations I found:

  • Don't ask for user permissions ideally
  • Device locale was not considered a reliable alternative
  • Detecting the sim carrier actually returns the original carrier country and not the current connected carrier (via roaming)
  • Retrieving the country by IP can be easily faked using VPNs which are becoming more popular lately
Hypocrite answered 5/7, 2022 at 21:41 Comment(1)
Best solution. Even VPN can't override this value.Royo
F
4

For Swift 3 it's even simpler:

let countryCode = Locale.current.regionCode
Fairyfairyland answered 6/12, 2016 at 13:45 Comment(0)
P
4

Swift 4.0 code for getting the Country name as per region set:

    let countryLocale = NSLocale.current
    let countryCode = countryLocale.regionCode
    let country = (countryLocale as NSLocale).displayName(forKey: NSLocale.Key.countryCode, value: countryCode)
    print(countryCode, country)

prints: Optional("NG") Optional("Nigeria"). //for nigeria region set

Pyriphlegethon answered 18/9, 2018 at 6:23 Comment(0)
C
1

You can get NSTimeZone from CLLocation: https://github.com/Alterplay/APTimeZones and works locally.

Cheerly answered 23/10, 2013 at 14:19 Comment(0)
V
0

If you are only interested in telephone devices, then the technique mentioned here might be useful to you: Determine iPhone user's country

Verboten answered 7/3, 2012 at 9:31 Comment(0)
L
-1

Here's a quick loop in Swift 3 that returns a complete list of country codes.

let countryCode = NSLocale.isoCountryCodes
    for country in countryCode {
        print(country)
    }
Lottielotto answered 2/5, 2017 at 9:26 Comment(0)
D
-1

@Rob

let locale = Locale.current
let code = (locale as NSLocale).object(forKey: NSLocale.Key.countryCode) as! String?

using these code you will get your region country code and if you didn't get still then change it just go in Phone setting->general->language & region and set your region you want

Diaconicon answered 21/5, 2018 at 12:2 Comment(0)
H
-1
    if let countryCode = Locale.current.regionCode {
        let country =   Locale.current.localizedString(forRegionCode: countryCode)
    }
Hamal answered 13/4, 2021 at 7:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.