
TL;DR Alla fine dell’articolo trovate un progetto completo con una classe per risolvere i più comuni problemi con CLLocationManager e iOS8 🙂
Generalmente le nuove versioni del sistema di iOS sono retro-compatibili, potete prendere del codice scritto per iOS(x) compilarlo con iOS(y) con x < y e tutto generalmente funziona senza problemi.
In qualche caso purtroppo non è così, e il nuovo CLLocationManager ne è un esempio. Molti infatti riscontrano un problema con le loro app passando ad Xcode 6 (e quindi all’sdk di ios8), nell’utilizzo del CLLocationManager che sembra apparentemente non funzionare.
Andando a dare un’occhiata all’API-diff tra iOS 7.1 e iOS 8.0 (qui) si notano due nuovi metodi:
- [CLLocationManager requestAlwaysAuthorization] - [CLLocationManager requestWhenInUseAuthorization]
e due nuovi valori per l’enum CLAuthorizationStatus:
Added kCLAuthorizationStatusAuthorizedAlways Added kCLAuthorizationStatusAuthorizedWhenInUse Deprecated kCLAuthorizationStatusAuthorized
iOS 8 infatti prevede due nuovi tipi di autorizzazione: possiamo autorizzare un’app per ricevere informazioni sulla nostra posizione soltanto quando l’app è attiva (foreground) oppure sempre (anche in backgroud) e il vecchio modo di usare il CLLocationManager richiamando semplicemente il metodo startUpdatingLocation non è più sufficiente.
La prima cosa da fare quindi è decidere se la vostra app richiede il primo o il secondo tipo di autorizzazione, se state progettando un’app per tracciare il percorso fatto in auto probabilmente vorrete avere l’accesso alla posizione anche in background, viceversa è sufficiente ottenere questa informazione in foreground.
Fatta questa scelta il primo step è quello di aggiungere nel file .plist dell’applicazione una stringa di testo per spiegare all’utente *perché* volete avere accesso alla sua posizione. Questa stringa verrà mostrata dal sistema operativo al momento della richiesta di accesso e nei settings dell’applicazione
Le chiavi da aggiungere al file plist sono due, a seconda del tipo di permesso che vi serve
NSLocationWhenInUseUsageDescription //Oppure NSLocationAlwaysUsageDescription
Questo è un esempio di plist:
E questo è il risultato visibile nei settings.
ATTENZIONE: Se dimenticate di aggiungere la relativa chiave, anche se tutto il codice è corretto, la richiesta di accesso non verrà mai mostrata all’utente e la vostra app non riceverà mai la sua posizione.
Specificata quindi la ragione per cui vogliamo ottenere la posizione dell’utente, possiamo passare a modificare il codice per ottenerla. Per richiedere la posizione siamo abituati a scrivere più o meno questo tipo di codice
locationManager = [[CLLocationManager alloc] init]; locationManager.distanceFilter = kCLDistanceFilterNone; locationManager.desiredAccuracy = kCLLocationAccuracyBest; locationManager.delegate = self; [locationManager startUpdatingLocation];
Ma su iOS8 questo codice non farà scattare la richiesta di permesso verso l’utente, quindi la nostra app non riceverà nessuna notifica.
Bisogna infatti attivamente richiedere il permesso all’utente tramite uno dei due metodi:
-requestAlwaysAuthorization -requestWhenInUseAuthorization
E finalmente l’utente potrà accettare o rifiutare la richiesta (notare la stringa di descrizione all’interno dell’alert view)
Questi metodi sono stati definiti nell’skd di iOS 8, quindi se la vostra app è compabile anche con iOS 7 dovrete prima di tutto verificare la versione del sistema operativo, viceversa l’invocazione di questo metodo causerà un bel crash a runtime.
È possibile (e consigliato) richiedere sempre il permesso, in ogni caso se l’utente ha già approvato o rifiutato non vedrà apparire il messaggio.
Un secondo aspetto da tenere in considerazione è il cambio di stato dei permessi, l’utente potrebbe aver rifiutato il permesso ma poi grazie ad un nostro ulteriore messaggio (“caro utente se non attivi la localizzazione come faccio a consigliarti i bar qui vicino??”) potrebbe aver deciso di concederci la grazia, quindi la nostra app deve essere reattiva e aggiornarsi di conseguenza.
Questo può essere ottenuto implementando il metodo locationManager:didChangeAuthorizationStatus e richiamando il metodo startUpdatingLocation all’occorrenza.
Una classe da poter riutilizzare
Questa parte di codice è spesso molto simile in tutte le applicazioni, quindi si presta bene ad essere copia&incollata oppure, ancora meglio, ad essere inglobata in una classe che possa essere riutilizzata in più progetti.
Ho infatti scritto un piccolo esempio di wrapper attorno alla classe CLLocationManager per gestire i casi più comuni, trovate il progetto su github a questo indirizzo: https://github.com/ignazioc/DevAppLocationManager
L’idea di base è quella di poter usare questo wrapper al posto del normale CLLocationManager per evitare di dover scrivere codice diverso per iOS 7 / iOS 8 e soprattutto per evitare di dimenticare tutti quei casi un po’ particolari (cambio di stato, etc) che si possono verificare con la gestione dei permessi.
Ho inoltre preferito semplificare un po’ la gestione, usando i block al posto del delegate e gestendo due soli tipi di errori (al posto dei 17 di CoreLocation), gli unici due errori ai quali sono realmente interessato sono “L’utente ha rifiutato il permesso” e “L’utente non può darti il permesso”.
La classe si chiama CLLocationManagerWrapper, non è un singleton ed ha semplicemente due metodi:
- (id)initWithAuthType:(CLLocationManageraAuthType)authType filterDistance:(CLLocationDistance)distanceFilter accuracy:(CLLocationAccuracy)accuracy completionBlock:(void(^)(CLLocation *newLocation, CLLocationManagerError error))compltionBlock; - (void)stopUpdate;
Il primo va utilizzato per inizializzare la classe e passare il block che verrà eseguito per ogni nuova posizione rilevata, il secondo per interrompere la localizzazione.
Questo è un esempio di utilizzo:
locationManagerW = [[CLLocationManagerWrapper alloc] initWithAuthType:CLLocationManageraAuthTypeWhenInUse filterDistance:kCLDistanceFilterNone accuracy:kCLLocationAccuracyHundredMeters completionBlock:^(CLLocation *newLocation, CLLocationManagerError error) { if (newLocation != nil) { NSLog(@"Ricevuta posizione!"); } else { NSLog(@"Found error: %@", NSStringFromCLLocationManagerError(error)); } }];
Una piccola chiccha… per evitare di dimenticare di aggiungere la stringa al plist, in attesa che apple sollevi un warnin o un errore, nell’inizializzazione della classe trovate questa riga, così l’app non compila fintanto che tutto non sarà corretto 😉
NSAssert([[[NSBundle mainBundle] infoDictionary] valueForKey:@"NSLocationWhenInUseUsageDescription"] != nil, @"Aggiungi la descrizione nel file plist");
Alla prossima and “happy coding”!
No Responses to “CLLocationManager e iOS8”