/GeoFirestore-iOS

GeoFirestore for iOS - Realtime location queries with Firestore

Primary LanguageMakefileMIT LicenseMIT

GeoFirestore for iOS — Realtime location queries with Firestore

GeoFirestore is an open-source library for Swift that allows you to store and query a set of documents based on their geographic location.

At its heart, GeoFirestore simply stores locations with string keys. Its main benefit however, is the possibility of querying documents within a given geographic area - all in realtime.

GeoFirestore uses the Firestore database for data storage, allowing query results to be updated in realtime as they change. GeoFirestore selectively loads only the data near certain locations, keeping your applications light and responsive, even with extremely large datasets.

A compatible GeoFirestore client is also available for Android.

Integrating GeoFirestore with your data

GeoFirestore is designed as a lightweight add-on to Firestore. However, to keep things simple, GeoFirestore stores data in its own format and its own location within your Firestore database. This allows your existing data format and security rules to remain unchanged and for you to add GeoFirestore as an easy solution for geo queries without modifying your existing data.

Example usage

Assume you are building an app to rate bars, and you store all information for a bar (e.g. name, business hours and price range) at collection(bars).document(bar-id). Later, you want to add the possibility for users to search for bars in their vicinity. This is where GeoFirestore comes in. You can store the location for each bar document using GeoFirestore. GeoFirestore then allows you to easily query which bar are nearby.

Example

To run the example project, clone the repo, and run pod repo update and pod install from the Example directory first.

Downloading GeoFirestore for iOS

If you're using CocoaPods add the following to your Podfile:

pod ‘Geofirestore'

Then run the following in terminal:

pod repo update
pod install

Getting Started with Firestore

GeoFirestore requires the Firestore database in order to store location data. You can learn more about Firestore here.

Using GeoFirestore

GeoFirestore

A GeoFirestore object is used to read and write geo location data to your Firestore database and to create queries. To create a new GeoFirestore instance you need to attach it to a Firestore collection reference:

let geoFirestoreRef = Firestore.firestore().collection("my-collection")
let geoFirestore = GeoFirestore(collectionRef: geoFirestoreRef)

Setting location data

To set the location of a document simply call the setLocation method:

geoFirestore.setLocation(location: CLLocation(latitude: 37.7853889, longitude: -122.4056973), forDocumentWithID: "que8B9fxxjcvbC81h32VRjeBSUW2") { (error) in
    if (error != nil) {
        print("An error occured: \(error)")
    } else {
        print("Saved location successfully!")
    }
}

Alternatively set the location using a GeoPoint :

geoFirestore.setLocation(geopoint: GeoPoint(latitude: 37.7853889, longitude: -122.4056973), forDocumentWithID: "que8B9fxxjcvbC81h32VRjeBSUW2") { (error) in
    if (error != nil) {
        print("An error occured: \(error)")
    } else {
        print("Saved location successfully!")
    }
}

To remove a location and delete the location from your database simply call:

geoFirestore.removeLocation(forDocumentWithID: "que8B9fxxjcvbC81h32VRjeBSUW2") 

Retrieving a location

Retrieving locations happens with callbacks. If the document is not present in GeoFirestore, the callback will be called with nil. If an error occurred, the callback is passed the error and the location will be nil.

geoFirestore.getLocation(forDocumentWithID: "que8B9fxxjcvbC81h32VRjeBSUW2") { (location: CLLocation?, error) in
    if (error != nil) {
        print("An error occurred: \(error)")
    } else if (location != nil) {
        print("Location: [\(location!.coordinate.latitude), \(location!.coordinate.longitude)]")
    } else {
        print("GeoFirestore does not contain a location for this document")
    }
}

Alternatively get the location as a GeoPoint :

geoFirestore.getLocation(forDocumentWithID: "que8B9fxxjcvbC81h32VRjeBSUW2") { (location: GeoPoint?, error) in
    if (error != nil) {
        print("An error occurred: \(error)")
    } else if (location != nil) {
        print("Location: [\(location!.latitude), \(location!.longitude)]")
    } else {
        print("GeoFirestore does not contain a location for this document")
    }
}

GeoFirestore Queries

GeoFirestore allows you to query all documents within a geographic area using GFSQuery objects. As the locations for documents change, the query is updated in realtime and fires events letting you know if any relevant documents have moved. GFSQuery parameters can be updated later to change the size and center of the queried area.

// Query using CLLocation
let center = CLLocation(latitude: 37.7832889, longitude: -122.4056973)
// Query locations at [37.7832889, -122.4056973] with a radius of 600 meters
var circleQuery = geoFirestore.query(withCenter: center, radius: 0.6)

// Query using GeoPoint
let center2 = GeoPoint(latitude: 37.7832889, longitude: -122.4056973)
// Query locations at [37.7832889, -122.4056973] with a radius of 600 meters
var circleQuery2 = geoFirestore.query(withCenter: center2, radius: 0.6)

// Query location by region
let span = MKCoordinateSpanMake(0.001, 0.001)
let region = MKCoordinateRegionMake(center.coordinate, span)
var regionQuery = geoFirestore.query(inRegion: region)

Receiving events for geo queries

There are three kinds of events that can occur with a geo query:

  1. Document Entered: The location of a document now matches the query criteria.
  2. Document Exited: The location of a document no longer matches the query criteria.
  3. Document Moved: The location of a document changed but the location still matches the query criteria.

Document entered events will be fired for all documents initially matching the query as well as any time afterwards that a document enters the query. Document moved and document exited events are guaranteed to be preceded by a document entered event.

To observe events for a geo query you can register a callback with observe:with::

let queryHandle = query.observe(.documentEntered, with: { (key, location) in
    print("The document with documentID '\(key)' entered the search area and is at location '\(location)'")
})

To cancel one or all callbacks for a geo query, call removeObserver:withHandle: or removeAllObservers:, respectively.

Waiting for queries to be "ready"

Sometimes you want to know when the data for all the initial documents has been loaded from the server and the corresponding events for those documents have been fired. For example, you may want to hide a loading animation after your data has fully loaded. GFSQuery offers a method to listen for these ready events:

query.observeReady {
    print("All initial data has been loaded and events have been fired!")
}

Note that locations might change while initially loading the data and document moved and document exited events might therefore still occur before the ready event was fired.

When the query criteria is updated, the existing locations are re-queried and the ready event is fired again once all events for the updated query have been fired. This includes document exited events for documents that no longer match the query.

Updating the query criteria

To update the query criteria you can use the center and radius properties on the GFSQuery object. Document exited and document entered events will be fired for documents moving in and out of the old and new search area, respectively. No document moved events will be fired as a result of the query criteria changing; however, document moved events might occur independently.

Convenient extensions

To make it easier to convert between a GeoPoint and a CLLocation we have provided some useful extensions:

let cllocation = CLLocation(latitude: 37.7832889, longitude: -122.4056973)
let geopoint = GeoPoint(latitude: 37.7832889, longitude: -122.4056973)

// Converting from CLLocation to Geopoint
let loc1: GeoPoint = cllocation.geopointValue()
let loc2: GeoPoint = GeoPoint.geopointWithLocation(location: cllocation)

// Converting from Geopoint to CLLocation
let loc3: CLLocation = geopoint.locationValue()
let loc4: CLLocation = CLLocation.locationWithGeopoint(geopoint: geopoint)

API Reference & Documentation

Full API reference and documentation is available here

License

GeoFirestore is available under the MIT license. See the LICENSE file for more info.

Copyright (c) 2018 Imperium Labs