MapKit y CoreLocation en SwiftUI (localización en tiempo real)
MapKit en SwiftUI permite mostrar un mapa y CoreLocation en SwiftUI permite saber la localización de un user en tiempo real. Primero tenemos que usar CLLocationManager para que el user autorice recoger la localización. Aprende a crear una app mostrando la localización de un user en tiempo real
Tabla de contenido
Hoy en SwiftBeta vamos a aprender a mostrar la localización de un user en tiempo real en un mapa en SwiftUI. Vamos usar dos frameworks llamados MapKit y CoreLocation. Los dos necesarios para cumplir con los propósitos del post de hoy.
MapKit en código
Lo primero de todo es crear un proyecto de cero en Xcode. Y dentro del ContentView vamos a crear dos struct privadas con la información de unas coordenadas que queremos mostrar en el mapa y también el zoom que queremos que tenga nuestro mapa.
El código del ContentView quedaría de la siguiente manera:
Lo siguiente que vamos hacer es irnos a la parte de arriba del fichero e importar el framework MapKit. Y dentro de la vista ContentView, en la propiedad body vamos a poner Map. Al abrir parétensis podemos ver todos los inicializadores, nos quedamos con el que esperar un coordinateRegion y un showsUserLocation.
El parámetro coordinateRegion es de tipo MKCoordinateRegion, y debemos inicializarlo con las structs privadas que hemos creado al principio del video. Para darle un valor inicial, creamos una propiedad @State y inicializamos MKCoordinateRegion, el código final sería:
Si ahora compilamos veremos que aparece la vista del mapa y sale San José, que son las coordenadas que hemos puesto en la struct DefaultRegion.
Aquí puedes jugar con el Span para ver como la vista del mapa se acerca o se aleja dependiendo de los valores que pongas
Hasta aquí todo muy sencillo, hemos importado un framework que no habíamos visto hasta ahora llamado MapKit, hemos usado la vista Map y hemos usado también un tipo llamado MKCoordinateRegion para mostrar una región de un mapa.
Ahora lo que vamos hacer es mostrar la posición de un usuario en el mapa.
CoreLocation en código
Ahora lo que vamos hacer es crear un fichero nuevo, va a ser una clase y la vamos a llamar LocationViewModel. Esta clase va a ser final y de momento va a conformar el protocolo NSObject. Vamos a crear una propiedad de tipo CLLocationManager y la vamos a inicializar. Vemos que hay un error, para arreglarlo importamos el framework CoreLocation, este framework nos permite acceder a información de la localización de un user, saber si nos ha autorizado saber su localización en nuestra app, etc (todo esto lo vamos a ver a continuación).
Cremos un init y especificamos que queremos la mejor precisión, especificamos que queremos saber la localización mientras se esté usando la app. Especificamos que queremos empezar a recibir la localización del user y asignamos que la clase Location sea el delegado de nuestra propiedad locationManager,
De momento el código nos quedaría así:
¿esto del delegado qué significa? hay "algo" mágico que nos va a ir notificando de actualizaciones en la localización del user, y nos notificará dentro de nuestra clase Location. Para ello debemos conformar un protocolo e implementar un método:
Lo siguiente que vamos hacer es crear una propiedad en la vista ContentView de tipo LocationViewModel y la vamos a instanciar. El código que quedaría:
Ahora si compilamos la app, seguimos viendo la misma vista de antes donde aparece San José, y si te fijas vemos un error por consola que nos dice lo siguiente:
This app has attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an “NSLocationWhenInUseUsageDescription” key with a string value explaining to the user how the app uses this data
Para poder recibir la localización del user, el user préviamente debe autorizarlo dentro de nuestra app y dar su consentimiento. Ya lo habrás visto en otras apps, a continuación te muestro una captura y sabrás exactamente lo que hablo.
Dentro de este alert, aparece un mensaje, esto es lo que debemos especificar en nuestro Info.plist de nuestra app. Para ello vamos al Info.plist
Pulsamos en una key cualquiera y pulsamos la tecla enter, al hacerlo se crea un campo nuevo y añadimos la key NSLocationWhenInUseUsageDescription y ponemos un mensaje que aparecerá en el alert, en mi caso he puesto Quiero acceder a tu localización para mostrarla en un mapa
Una vez hemos añadido esta key en el Info.plist podemos compilar nuestra app y nos aparecerá el alert preguntándonos si queremos autorizar a que nuestra app vaya recibiendo la localización del user.
Fíjate que el mensaje que hemos añadido a la key NSLocationWhenInUseUsageDescription aparece en el alert. Podemos poner el mensaje que queramos siempre que le quede claro al user para qué queremos obtener su localización.
¡Vamos a continuar! si autorizamos, y pulsamos "Allow While Using App" verás que automáticamente se printa algo por consola.
Location <+41.38790000,+2.16992000> +/- 5.00m (speed -1.00 mps / course -1.00) @ 10/1/21, 5:12:58 PM Central European Summer Time
Al autorizar poder mostrar la localización del user, el método func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) del delegado CLLocationManagerDelegate donde teníamos un print, nos muestra nuestra primera coordenada. Ahora, lo que estaría bien es poder mostrar esta localización en el mapa, vamos a ello.
Vamos a crear una nueva propiedad en LocationViewModel y va a ser de tipo @Published, así que cada cambio que recibamos en el método del delegado, actualizará esta propiedad y se refrescará la vista del Map del ContentView con la nueva localización. La clase LocationViewModel quedaría:
fíjate que para poder tener propiedades @Published en LocationViewModel hemos tenido que conformar el protocolo ObservableObject
y por lo tanto en ContentView podemos simplificar el código que teníamos a:
Si compilamos la app, vemos que nuestro mapa ahora mismo nos situa en Barcelona, y aparece la localización del user en Plaza Cataluña.
Hemos avanzado bastante con muy pocas líneas de código. Ahora lo que vamos hacer es ver qué pasa si un user no autoriza a que recibamos su localización en tiempo real.
authorizationStatus y locationManagerDidChangeAuthorization
Lo que vamos hacer ahora es borrar la app del simulador, ¿por qué? hemos hecho el camino fácil, vamos a ver qué pasa cuando el user no nos autoriza a que recibamos su localización.
Al instalar la app de cero, no tenemos localización y por lo tanto si te fijas estamos en medio de un océano.
Spoiler: Esto lo arreglaremos en breves y daremos una localización por defecto. Así en lugar de que nos muestre una posición en medio del Atlántico, nos mostrará una por defecto. Por ejemplo, otra vez San José.
Vamos a ver qué pasa si le damos a "Dont Allow"
Pues lo que pasa es que el alert desaparece, y el mapa nos muestra lo que comentábamos, una porción del Atlántico. Vamos a dar una posición por defecto cuando no tengamos la localización del user.
Para ello nos vamos al LocationViewModel, y creamos la misma struct que teniamos antes con las coordenadas de San José.
private struct DefaultRegion {
static let latitude = 9.9333
static let longitude = -84.0833
}
y también instanciamos la propiedad userLocation con estos parámetros, el código del LocationViewModel debería parecerse a esto:
Ahora si compilamos, en lugar de mostrarnos una zona del Atlántico, nos muestra la zona de San José.
Lo siguiente que vamos hacer, es cambiar la vista, queremos saber si un user ha autorizado a que podamos recibir actualizaciones de su localización, y en caso de no autorizarnos queremos cambiar la vista y mostrar un Link (un Button) para que vaya a la pantalla de Settings de la app y tenga la posibilidad de cambiar lo que escogió por primera vez. Vamos a añadir nuestra última propiedad en LocationViewModel, la vamos a llamar userHasLocation y será de tipo Booleano. La modificaremos a true o false dependiendo de lo que escoja el usuario cuando le aparezca el alert.
Una vez creada la propiedad:
@Published var userHasLocation: Bool = false
Vamos a crear un método en LocationViewModel para saber qué escoge el user cuando le aparece el alert de la autorización de la localización. Es una función muy simple, solo debemos usar la variable authorizationStatus de nuestra propiedad locationManager, y obtendremos qué estado tiene:
Ahora, vamos a llamar en otro método que pertenece al Protocolo CLLocationManagerDelegate (y del que ya conforma LocationViewModel). Dentro de este método, cuando haya un cambio la autorización, llamaremos a la función que acabamos de crear para que actualice la propiedad userHasLocation (y por lo tanto actualice la vista, que es lo que veremos a continuación)
Es decir, nuestro LocationViewModel, nuestra versión final quedaría:
Ahora, como hemos dicho, tenemos que escuchar estos cambios en la vista para poder actualizarla, lo que vamos hacer es mostrar dos vistas diferentes abajo de la vista Map
- Si el user acepta a que recibamos su localización mostramos el Map y abajo un Text indicando que el user ha aceptado.
- Si el user no acepta que recibamos su localización mostramos el Map y abajo un Link, un Button para ir a la pantalla de Settings de la app y pueda modificar el estado de la autorización.
El código de ContentView nos queda así:
Como lo último que teníamos era que el user no había aceptado las notificaciones. (Es lo que hemos escogido y por eso nos aparecía la vista del Atlántico y lo hemos modificado para que apareciese San José.). El user no tiene localización y por lo tanto aparece esta vista
y si pulsamos el Link (el Button) que aparece en la parte inferior, nos lleva a la siguiente vista:
Lo que debemos hacer es que nos lleve a las Settings de la app que estamos creando, no a la Settings generales de iOS. Para poder hacerlo, debemos crear un nuevo fichero.
Pulsamos COMMAND+N y escogemos el que pone "Settings Bundle"
Le damos a Next y lo creamos. Nos aparecerá en el listado de ficheros en Xcode (en la parte izquierda). Ahora, si compilamos otra vez y le damos al Link (al Button) que aparece debajo de la vista Map, nos lleva directamente a la sección de Settings de nuestra app, y aquí podemos cambiar la opción de Location
si te fijas, a mi me aparece con el valor Never, si la modifico y selecciono "While Using the App" y vuelvo abrir la app (sin necesidad de compilar) me muestra directamente que estoy en Barcelona.
De esta manera los users que no aceptaron la localización de tu app, tienen otra oportunidad para aceptarla.
Conclusión
Hoy hemos aprendido a usar MapKit y CoreLocation en SwiftUI. Hemos aprendido también varios tipos nuevos que son usados dentro de estos frameworks. Con muy pocas líneas de código hemos podido mostrar una posición en el mapa en SwiftUI y más tarde hemos aprendido a cómo sacar la localización en tiempo real de un user.