iOS

Geoqueries

Introduction

We use the term Geoqueries to refer to the type of queries where their conditions involve ParseGeoPoint type fields. It is recommended to use the ParseGeoPoint struct to store geographic location data in a Back4App Database. The ParseSwift SDK provides a set of methods that allows us to query data according to conditions applied on ParseGeoPoint data type.

Prerequisites

To complete this tutorial, you will need:

Goal

To understand how to query data using conditions on geographic location data.

Step 1 - Quick review about the Query<U> class

Any query performed on a Back4App Database is done via the generic class Query<U>. The generic parameter U (conforming to the ParseObject protocol) is the data type of the objects we are trying to retrieve from the database.

Given a data type like MyObject, we retrieve these objects from a Back4App Database in the following way

1
2
3
4
5
6
7
8
9
10
11
12
import ParseSwift

struct MyObject: ParseObject {
  ...
}

let query = MyObject.query()

// Executes the query asynchronously
query.find { result in
  // Handle the result (of type Result<[MyObject], ParseError>)
}

You can read more about the Query<U> class here at the official documentation.

Step 2 - Save some data on Back4App

Before we begin to execute queries, we should store some sample data on a Back4App Database. By following the Quickstart guide, you can configure and link your sample iOS App to your Back4App database. For this guide, we will store information about cities. We use the following struct to organize a city’s information:

1
2
3
4
5
6
7
8
import ParseSwift

struct City: ParseObject {
    ...
    
    var name: String? // City's name
    var location: ParseGeoPoint? // It will store the city's coordinate on Earth
}

Now, we proceed to store the sample data. This step can be implemented using Swift or directly from your app’s console on the Back4App platform.

After setting up your XCode project, calling the following method should save some sample data on your Back4App Database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import ParseSwift

func setupSampleData() {
  do {
    // Montevideo - Uruguay
    _ = try City(
      name: "Montevideo",
      location: ParseGeoPoint(coordinate: CLLocationCoordinate2D(latitude: -34.85553195363169, longitude: -56.207280375137955))
    ).save()
            
    // Brasília - Brazil
    _ = try City(
      name: "Brasília",
      location: ParseGeoPoint(coordinate: CLLocationCoordinate2D(latitude: -15.79485821477289, longitude: -47.88391074690196))
    ).save()
            
    // Bogotá - Colombia
    _ = try City(
      name: "Bogotá",
      location: ParseGeoPoint(coordinate: CLLocationCoordinate2D(latitude: 4.69139880891712, longitude: -74.06936691331047))
    ).save()
            
    // Mexico City - Mexico
    _ = try City(
      name: "Mexico City",
      location: ParseGeoPoint(coordinate: CLLocationCoordinate2D(latitude: 19.400977162618933, longitude: -99.13311378164776))
    ).save()
            
    // Washington, D.C. - United States
      _ = try City(
        name: "Washington, D.C.",
        location: ParseGeoPoint(coordinate: CLLocationCoordinate2D(latitude: 38.930727220189944, longitude: -77.04626261880388))
      ).save()
            
    // Ottawa - Canada
    _ = try City(
      name: "Ottawa",
      location: ParseGeoPoint(latitude: 45.41102167733425, longitude: -75.695414598736)
    ).save()
  } catch let error as ParseError {
    print("[ParseSwift ERROR]", error.message)
  } catch {
    print("[ERROR]", error.localizedDescription)
  }
}

A quick way to insert elements on your Back4App Database is via the console located in your App’s API section. Once you are there, you can start running Javascript code to save the sample data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Add City objects and create table
// Note how GeoPoints are created, passing latitude and longitude as arguments
// Montevideo
City = new Parse.Object('City');
City.set('name', 'Montevideo - Uruguay');
City.set('location', new Parse.GeoPoint(-34.85553195363169, -56.207280375137955));
await City.save();

// Brasília
City = new Parse.Object('City');
City.set('name', 'Brasília - Brazil');
City.set('location', new Parse.GeoPoint(-15.79485821477289, -47.88391074690196));
await City.save();

// Bogotá
City = new Parse.Object('City');
City.set('name', 'Bogotá - Colombia');
City.set('location', new Parse.GeoPoint(4.69139880891712, -74.06936691331047));
await City.save();

// Mexico City
City = new Parse.Object('City');
City.set('name', 'Mexico City - Mexico');
City.set('location', new Parse.GeoPoint(19.400977162618933, -99.13311378164776));
await City.save();

// Washington, D.C.
City = new Parse.Object('City');
City.set('name', 'Washington, D.C. - USA');
City.set('location', new Parse.GeoPoint(38.930727220189944, -77.04626261880388));
await City.save();

// Ottawa
City = new Parse.Object('City');
City.set('name', 'Ottawa - Canada');
City.set('location', new Parse.GeoPoint(45.41102167733425, -75.695414598736));
await City.save();

Step 3 - Query the data

With the sample data saved, we can start performing different query types.

Sorting the results

For our first example, we will select all the cities and sort them depending on how far they are from a reference geopoint. We implement this query by passing a constraint to the Query<City> object. The method near(key:geoPoint:) available via the ParseSwift SDK allows us to construct such constraint. As arguments, we pass the field’s name (usually referred to as key) containing the reference geoPoint.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// The reference geopoint will be Kingston city - Jamaica
guard let kingstonGeoPoint = try? ParseGeoPoint(latitude: 18.018086950599134, longitude: -76.79894232253473) else { return }

let query = City.query(near(key: "location", geoPoint: kingstonGeoPoint)) // The method near(key:geoPoint:) returns the constraint needed to sort the query

let sortedCities: [City]? = try? query.find() // Executes the query synchronosuly and returns an array containing the cities properly sorted

query.find { result in // Executes the query asynchronosuly and returns a Result<[City], ParseError> type object to handle the results
  switch result {
  case .success(let cities):
    // cities = [Bogotá, Washington DC, Mexico City, Ottawa, Brasília, Montevideo]
  case .failure(let error):
    // Handle the error if something happened
  }
}

Selecting results within a given region

Suppose we want to select cities within a certain region. We can achieve this with a constraint created by the method withinKilometers(key:geoPoint:distance:). As arguments, we pass the field’s name containing the city’s location, the region’s center (a ParseGeoPoint data type) and the maximum distance (in km) a city can be from this region’s center. To select all cities that are at most 3000km away from Kingston - Jamaica, we can do it in the following way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let distance: Double = 3000 // km
guard let kingstonGeoPoint = try? ParseGeoPoint(latitude: 18.018086950599134, longitude: -76.79894232253473) else { return }

let query = City.query(withinKilometers(key: "location", geoPoint: kingstonGeoPoint, distance: distance))
// The method withinKilometers(key:geoPoint:distance:) returns the constraint we need for this case

let sortedCities: [City]? = try? query.find() // Executes the query synchronosuly and returns an array containing the cities we are looking for (Bogotá, Washington DC and Mexico city in this case)

query.find { result in // Executes the query asynchronosuly and returns a Result<[City], ParseError> type object to handle the results
  switch result {
  case .success(let cities):
    // cities = [Bogotá, Washington DC, Mexico City]
  case .failure(let error):
    // Handle the error if something happened
  }
}

Additionally, when the distance is given in miles instead of kilomenters, we can use the withinMiles(key:geoPoint:distance:sorted:) method.

A less common method, withinRadians(key:geoPoint:distance:sorted:), is also available if the distance is given in radians. Its use is very similar to the previous methods.

Selecting results within a given polygon

In the previous example, we selected cities within a region represented by a circular region. In case we require to have a non-circular shape for the region, the ParseSwift SDK does allow us to construct such regions from their vertices.

Now, the goal for this example is to select cities within a five vertex polygon. These vertices are expressed using the ParseGeoPoint struct. Once we have created the vertices, we instantiate a ParsePolygon. This polygon is then passed to the withinPolygon(key:polygon:) method (provided by the ParseSwift SDK) to construct the constraint that will allow us to select cities within this polygon.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// The polygon where the selected cities are
let polygon: ParsePolygon? = {
  do {
    // We first instantiate the polygon vertices
    let geoPoint1 = try ParseGeoPoint(latitude: 15.822238344514378, longitude: -72.42845934415942)
    let geoPoint2 = try ParseGeoPoint(latitude: -0.7433770196268968, longitude: -97.44765968406668)
    let geoPoint3 = try ParseGeoPoint(latitude: -59.997149373299166, longitude: -76.52969196322749)
    let geoPoint4 = try ParseGeoPoint(latitude: -9.488786415007201, longitude: -18.346101586021952)
    let geoPoint5 = try ParseGeoPoint(latitude: 15.414859532811047, longitude: -60.00625459569375)
                
    // Next we compose the polygon
    return try ParsePolygon([geoPoint1, geoPoint2, geoPoint3, geoPoint4, geoPoint5])
  } catch let error as ParseError {
    print("Failed to instantiate vertices: \(error.message)")
    return nil
  } catch {
    print("Failed to instantiate vertices: \(error.localizedDescription)")
    return nil
  }
}()
        
guard let safePolygon = polygon else { return }
        
let query = City.query(withinPolygon(key: "location", polygon: safePolygon))
// withinPolygon(key:polygon:) returns the required constraint to apply on the query
        
let cities = try? query.find() // Executes the query synchronously
        
query.find { result in // Executes the query asynchronously and returns a result of type Result<[], ParseError>
  // Handle the result
}

Conclusion

Nowadays doing operations on location data to offer custom services is very important. Back4App together with the ParseSwift SDK makes it easy to implement those kinds of operations.