# Using Maps and Location Data in Your SwiftUI (+Realm) App

## Introduction
Embedding Apple Maps and location functionality in SwiftUI apps used to be a bit of a pain. It required writing your own SwiftUI wrapper around UIKit code—see these examples from the O-FISH app:

* Location helper
* Map views

If you only need to support iOS14 and later, then you can **forget most of that messy code 😊**. If you need to support iOS13—sorry, you need to go the O-FISH route!

iOS14 introduced the Map SwiftUI view (part of Mapkit) allowing you to embed maps directly into your SwiftUI apps without messy wrapper code.

This article shows you how to embed Apple Maps into your app views using Mapkit's Map view. We'll then look at how you can fetch the user's current location—with their permission, of course!

Finally, we'll see how to store the location data in Realm in a format that lets MongoDB Atlas Device Sync it to MongoDB Atlas. Once in Atlas, you can add a geospatial index and use MongoDB Charts to plot the data on a map—we'll look at that too.

Most of the code snippets have been extracted from the RChat app. That app is a good place to see maps and location data in action. Building a Mobile Chat App Using Realm – The New and Easier Way is a good place to learn more about the RChat app—including how to enable MongoDB Atlas Device Sync.

## Prerequisites

* Realm-Cocoa 10.8.0+ (may work with some 10.7.X versions)
* iOS 14.5+ (Mapkit was introduced in iOS 14.0 and so most features should work with earlier iOS 14.X versions)
* XCode12+

## How to Add an Apple Map to Your SwiftUI App

To begin, let's create a simple view that displays a map, the coordinates of the center of that map, and the zoom level:

With Mapkit and SwiftUI, this only takes a few lines of code:

``` swift
import MapKit
import SwiftUI

struct MyMapView: View {
    @State private var region: MKCoordinateRegion = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: MapDefaults.latitude, longitude: MapDefaults.longitude),
        span: MKCoordinateSpan(latitudeDelta: MapDefaults.zoom, longitudeDelta: MapDefaults.zoom))
        
    private enum MapDefaults {
        static let latitude = 45.872
        static let longitude = -1.248
        static let zoom = 0.5
    }

    var body: some View {
        VStack {
            Text("lat: \(region.center.latitude), long: \(region.center.longitude). Zoom: \(region.span.latitudeDelta)")
            .font(.caption)
            .padding()
            Map(coordinateRegion: $region,
                interactionModes: .all,
                showsUserLocation: true)
        }
    }
}
```

Note that `showsUserLocation` won't work unless the user has already given the app permission to use their location—we'll get to that.

`region` is initialized to a starting location, but it's updated by the `Map` view as the user scrolls and zooms in and out.

### Adding Bells and Whistles to Your Maps (Pins at Least)

Pins can be added to a map in the form of "annotations." Let's start with a single pin:

Annotations are provided as an array of structs where each instance must contain the coordinates of the pin. The struct must also conform to the Identifiable protocol:

``` swift
struct MyAnnotationItem: Identifiable {
    var coordinate: CLLocationCoordinate2D
    let id = UUID()
}
```

We can now create an array of `MyAnnotationItem` structs:

``` swift
let annotationItems = 
    MyAnnotationItem(coordinate: CLLocationCoordinate2D(
        latitude: MapDefaults.latitude,
        longitude: MapDefaults.longitude))]
```

We then pass `annotationItems` to the `MapView` and indicate that we want a `MapMarker` at the contained coordinates:

``` swift
Map(coordinateRegion: $region,
    interactionModes: .all,
    showsUserLocation: true,
    annotationItems: annotationItems) { item in
        MapMarker(coordinate: item.coordinate)
    }
```

That gives us the result we wanted.

What if we want multiple pins? Not a problem. Just add more `MyAnnotationItem` instances to the array.

All of the pins will be the same default color. But, what if we want different colored pins? It's simple to extend our code to produce this:

![Embedded Apple Map showing red, yellow, and plue pins at different locations

Firstly, we need to extend `MyAnnotationItem` to include an optional `color` and a `tint` that returns `color` if it's been defined and "red" if not:

``` swift
struct MyAnnotationItem: Identifiable {
    var coordinate: CLLocationCoordinate2D
    var color: Color?
    var tint: Color { color ?? .red }
    let id = UUID()
}
```

In our sample data, we can now choose to provide a color for each annotation:

``` swift
let annotationItems = 
    MyAnnotationItem(
        coordinate: CLLocationCoordinate2D(
            latitude: MapDefaults.latitude,
            longitude: MapDefaults.longitude)),
    MyAnnotationItem(
        coordinate: CLLocationCoordinate2D(
            latitude: 45.8827419,
            longitude: -1.1932383),
        color: .yellow),
    MyAnnotationItem(
        coordinate: CLLocationCoordinate2D(
            latitude: 45.915737,
            longitude: -1.3300991),
        color: .blue)
]
```

The `MapView` can then use the `tint`:

``` swift
Map(coordinateRegion: $region,
    interactionModes: .all,
    showsUserLocation: true,
    annotationItems: annotationItems) { item in
    MapMarker(
        coordinate: item.coordinate,
        tint: item.tint)
}
```

If you get bored of pins, you can use `MapAnnotation` to use any view you like for your annotations:

``` swift
Map(coordinateRegion: $region,
    interactionModes: .all,
    showsUserLocation: true,
    annotationItems: annotationItems) { item in
    MapAnnotation(coordinate: item.coordinate) {
        Image(systemName: "gamecontroller.fill")
            .foregroundColor(item.tint)
    }
}
```

This is the result:

![Apple Map showing red, yellow and blue game controller icons at different locations on the map

You could also include the name of the system image to use with each annotation.

This gist contains the final code for the view.

## Finding Your User's Location

### Asking for Permission

Apple is pretty vocal about respecting the privacy of their users, and so it shouldn't be a shock that your app will have to request permission before being able to access a user's location.

The first step is to add a key-value pair to your Xcode project to indicate that the app may request permission to access the user's location, and what text should be displayed in the alert. You can add the pair to the "Info.plist" file:

```
Privacy - Location When In Use Usage Description : We'll only use your location when you ask to include it in a message
```

Once that setting has been added, the user should see an alert the first time that the app attempts to access their current location:

### Accessing Current Location

While Mapkit has made maps simple and native in SwiftUI, the same can't be said for location data.

You need to create a SwiftUI wrapper for Apple's Core Location functionality. There's not a lot of value in explaining this boilerplate code—just copy this code from RChat's LocationHelper.swift file, and paste it into your app:

``` swift
import CoreLocation

class LocationHelper: NSObject, ObservableObject {

    static let shared = LocationHelper()
    static let DefaultLocation = CLLocationCoordinate2D(latitude: 45.8827419, longitude: -1.1932383)

    static var currentLocation: CLLocationCoordinate2D {
        guard let location = shared.locationManager.location else {
            return DefaultLocation
        }
        return location.coordinate
    }

    private let locationManager = CLLocationManager()

    private override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
}

extension LocationHelper: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: CLLocation]) { }

    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Location manager failed with error: \(error.localizedDescription)")
    }

    public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        print("Location manager changed the status: \(status)")
    }
}
```

Once added, you can access the user's location with this simple call:

``` swift
let location = LocationHelper.currentLocation
```

### Store Location Data in Your Realm Database

#### The Location Format Expected by MongoDB

Realm doesn't have a native type for a geographic location, and so it's up to us how we choose to store it in a Realm Object. That is, unless we want to synchronize the data to MongoDB Atlas using Device Sync, and go on to use MongoDB's geospatial functionality.

To make the best use of the location data in Atlas, we need to add a [geospatial index to the field (which we’ll see how to do soon.) That means storing the location in a supported format. Not all options will work with Atlas Device Sync (e.g., it's not guaranteed that attributes will appear in the same order in your Realm Object and the synced Atlas document). The most robust approach is to use an array where the first element is longitude and the second is latitude:

``` json
location: , ]
```

#### Your Realm Object

The RChat app gives users the option to include their location in a chat message—this means that we need to include the location in the [ChatMessage Object:

``` swift
class ChatMessage: Object, ObjectKeyIdentifiable {
  …
    @Persisted let location = List()
  …
    convenience init(author: String, text: String, image: Photo?, location: Double] = []) {
        ...
location.forEach { coord in
            self.location.append(coord)
      }
        ...
        }
    }
   ….
}
```

The `location` array that's passed to that initializer is formed like this:

``` swift
let location = LocationHelper.currentLocation
self.location = [location.longitude, location.latitude]
```

## Location Data in Your Backend MongoDB Atlas Application Services App

The easiest way to create your backend MongoDB Atlas Application Services schema is to enable [Development Mode—that way, the schema is automatically generated from your Swift Realm Objects.

This is the generated schema for our "ChatMessage" collection:

``` swift
{
    "bsonType": "object",
    "properties": {
      "_id": {
        "bsonType": "string"
      },
      ...
      "location": {
        "bsonType": "array",
        "items": {
          "bsonType": "double"
        }
      }
    },
    "required": 
      "_id",
      ...
    ],
    "title": "ChatMessage"
}
```

This is a document that's been created from a synced Realm `ChatMessage` object:

![Screen capture of an Atlas document, which includes an array named location

### Adding a Geospatial Index in Atlas

Now that you have location data stored in Atlas, it would be nice to be able to work with it—e.g., running geospatial queries. To enable this, you need to add a geospatial index to the `location` field.

From the Atlas UI, select the "Indexes" tab for your collection and click "CREATE INDEX":

You should then configure a `2dsphere` index:

Most chat messages won't include the user's location and so I set the `sparse` option for efficiency.

Note that you'll get an error message if your ChatMessage collection contains any documents where the value in the location attribute isn't in a valid geospatial format.

Atlas will then build the index. This will be very quick, unless you already have a huge number of documents containing the location field. Once complete, you can move onto the next section.

### Plotting Your Location Data in MongoDB Charts

MongoDB Charts is a simple way to visualize MongoDB data. You can access it through the same UI as Application Services and Atlas. Just click on the "Charts" button:

The first step is to click the "Add Data Source" button:

Select your Atlas cluster:

Select the `RChat.ChatMessage` collection:

Click “Finish.” You’ll be taken to the default Dashboards view, which is empty for now. Click "Add Dashboard":

In your new dashboard, click "ADD CHART":

Configure your chart as shown here by:
- Setting the chart type to "Geospatial" and the sub-type to "Scatter."
- Dragging the "location" attribute to the coordinates box.
- Dragging the "author" field to the "Color" box.

Once you've created your chart, you can embed it in web apps, etc. That's beyond the scope of this article, but check out the MongoDB Charts docs if you're interested.

## Conclusion

SwiftUI makes it easy to embed Apple Maps in your SwiftUI apps. As with most Apple frameworks, there are extra maps features available if you break out from SwiftUI, but I'd suggest that the simplicity of working with SwiftUI is enough incentive for you to avoid that unless you have a compelling reason.

Accessing location information from within SwiftUI still feels a bit of a hack, but in reality, you cut and paste the helper code once, and then you're good to go.

By storing the location as a `longitude, latitude]` array (`List`) in your Realm database, it's simple to sync it with MongoDB Atlas. Once in Atlas, you have the full power of MongoDB's geospatial functionality to work your location data.

If you have questions, please head to our [developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.