iOS

Basic Queries

Introduction

In most use cases, we require to fetch data from a database with certain conditions. These conditions may include complex comparisons and ordering requirements. Thus, in any application, it is fundamental to construct efficient queries and, at the same time, the database has to be able to execute them as fast as possible.

The ParseSwift SDK does provide the necessary tools for you to construct any query according to the application requirements. In this tutorial, we explore these tools and use them in a real-world application.

This tutorial uses a basic app created in Xcode 12 and iOS 14.

At any time, you can access the complete Project via our GitHub repositories.

Goal

  • To understand how to create basic queries to retrieve data from a Back4App Database.

Prerequisites

To complete this quickstart, you need:

Understanding our Constacts App

The project template is a Contacts App where the user adds a contact’s information to save it on a Back4App Database

On the app’s homescreen you will find a set of buttons for different types of queries. Using the + button located on the top-right side of the navigation bar, we can add as many Contacts as needed.

Quick reference of commands we are going to use

For this example, we use the object Contact

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import Foundation
import ParseSwift

struct Contact: ParseObject {
    // Required properties from ParseObject protocol
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    
    // Custom fields for the contact's information
    var name: String?
    var birthday: Date?
    var numberOfFriends: Int?
    var favoriteFoods: [String]?
    
    ...
}

The following methods will allows us to save and query Contact objects:

When creating and saving a new instance of Contact we can use

1
2
3
4
5
6
7
8
9
var newContact: Contact = Contact(name: "John Doe", birthday: Date(), numberOfFriends: 5, favoriteFoods: ["Bread", "Pizza"])

// Saves newContact on your Back4App Database synchronously and returns the new saved Item. It throws and error if something went wrong.
let savedContact = try? newContact.save()

// Saves newContact on your Back4App Database asynchronously, and passes a Result<Contact, ParseError> object to the completion block to handle the save process.
newContact.save { result in
    // Handle the result to check wether the save process was successfull or not
}

For retrieving all the Contact items saved on a Back4App Database, we construct a Query<Contact> object and call the find() method on it

1
2
3
4
5
6
7
8
9
let contactsQuery = Contact.query() // A query to fetch all Contact items on your Back4App Database.

// Fetches the items synchronously or throws an error if found.
let fetchedContacts = try? query.find()

// Fetches the items asynchronously and calls a completion block passing a result object containing the result of the operation.
query.find { result in
    // Handle the result
}

In order to create a query with a specific condition, we use the static method query(_:) provided by the ParseObject protocol. We pass a QueryConstraint object to the method as a parameter. This QueryConstraint object represents the type of constraint we are imposing on the query. For queries involving comparison constraints, the ParseSwift SDK provides the following methods to create them

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import ParseSwift

// A constraint to retreive all Contact items that have exactly the string 'Jhon Doe' in their 'name' field
let constraint1 = try? equalTo(key: "name", value: "John Doe")

// An operator-like implementation for the equalTo(key:value:) method
let constraint2: QueryConstraint = "name" == "Jhon Doe"

// A constraint to retrieve all Contact items that have the string 'John' in their 'name' field (only workd with String-type fields)
let constraint3: QueryConstraint = containsString(key: "name", substring: "Jhon")

let query = Contact.Query(constrint1) // Depending on your use case, you can send any of the above constraints as parameter

// Executes the query synchronously. It throws an error if something happened
let fetchedContacts = try? query.find()

// Executes que query asynchronously and returns a Result<[Contact], ParseError> object with the result
query.find() { result in
    // Handle the result
}

When we want to query contacts which have a certain amount of friends or more, we do it in the following way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import ParseSwift

// A constraint to retrieve all Contact items that have 30 or more number of friends
let constraint1: QueryConstraint = "numberOfFriends" >= 30

// A constraint to retrieve all Contact items that have more than 30 number of friends
let constraint2: QueryConstraint = "numberOfFriends" > 30

let query = Contact.query(constraint1) // Depending on your use case, you can send any of the above constraints as parameter

// Executes the query synchronously. It throws an error if something happened
let fetchedContacts = try? query.find()

// Executes que query asynchronously and returns a Result<[Contact], ParseError> object with the result
query.find() { result in
    // Handle the result
}

where we used the operator-like methods provided by ParseSwift SDK

1
2
3
4
5
// Contents of QueryConstraint.swift (located in ParseSwift SDK)

public func >= <T>(key: String, value: T) -> QueryConstraint where T: Encodable

public func > <T>(key: String, value: T) -> QueryConstraint where T: Encodable

Adding an ordering option to queries is straightforward. Any Query<Contact> object has the order(_:) method to do so. A simple query using the birthday as descending order can be implemented in the folowing way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import ParseSwift

// A query without order to retrieve all the Contact items
let unorderedQuery = Contact.query()

// Sorts the result by the brithday field. The parameter in the enumeration is the key of the field used to order the results
let descendingOrder = Query<Contact>.Order.descending("birthday")

let orderedQuery = unorderedQuery.order([descendingOrder]) // Returns a new query with the requested (descending) ordering option

// Executes the query synchronously. It throws an error if something happened
let orderedContacts = try? orderedQuery.find()

// Executes que query asynchronously and returns a Result<[Contact], ParseError> object with the result
orderedContacts.find() { result in
    // Handle the result
}

The Query<Contact>.Order eumeration provided by the ParseSwift SDK allows us to order the results from a query, for each enumeration we pass the key (birthday in this case) of the field used to order the results.

Step 1 - Download the Contacts App Template

The XCode project has the following structure

At any time, you can access the complete Project via our GitHub repositories.

To focus on the main objective of this guide, we will only detail the sections strictly related to queries and the ParseSwift SDK.

Step 2 - Additional CRUD flow

Before getting started with queries, it is necessary to have some contacts already saved on your Back4App Database. In the NewContactController class, we implement a basic form to add a Contact. To save an instance of a Contact object, we use the handleAddContact() method implemented in the NewContactController class

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
// NewContactController.swift file
...

extension NewContactController {
    /// Retrieves the info the user entered for a new contact and stores it on your Back4App Database
    @objc fileprivate func handleAddContact() {
        view.endEditing(true)
        
        // Collect the contact's information from the form
        guard let name = nameTextField.text,
              let numberOfFriendsString = numberOfFriendsTextField.text,
              let numberOfFriends = Int(numberOfFriendsString),
              let favoriteFoods = favoriteFoodsTextField.text?.split(separator: ",") else {
            return showAlert(title: "Error", message: "The data you entered is con valid.")
        }
        
        // Once the contact's information is collected, instantiate a Contact object to save it on your Back4App Database
        let contact = Contact(
            name: name,
            birthday: birthdayDatePicker.date,
            numberOfFriends: numberOfFriends,
            favoriteFoods: favoriteFoods.compactMap { String($0).trimmingCharacters(in: .whitespaces) }
        )
        
        // Save the new Contact
        contact.save { [weak self] result in
            switch result {
            case .success(_):
                self?.showAlert(title: "Success", message: "Contact saved.") {
                    self?.dismiss(animated: true, completion: nil)
                }
            case .failure(let error):
                self?.showAlert(title: "Error", message: "Failed to save contact: \(error.message)")
            }
        }
    }
}

For more details about this step, you can go to the basic operations guide.

Step 3 - Performing basic queries

- By name

The first example we look at is a query that allows us to retrieve contacts which have a specific substring in their name field. In order to do this, we first create a QueryConstraint object. This object will contain the constraint we want. The ParseSwift SDK provides the following methods to (indirectly) create a QueryConstraint

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
// QueryConstraint.swift file

/**
  Add a constraint for finding string values that contain a provided substring.
  - warning: This will be slow for large datasets.
  - parameter key: The key that the string to match is stored in.
  - parameter substring: The substring that the value must contain.
  - parameter modifiers: Any of the following supported PCRE modifiers (defaults to nil):
    - `i` - Case insensitive search
    - `m` - Search across multiple lines of input
  - returns: The resulting `QueryConstraint`.
 */
public func containsString(key: String, substring: String, modifiers: String? = nil) -> QueryConstraint

/**
 Add a constraint that requires that a key is equal to a value.
 - parameter key: The key that the value is stored in.
 - parameter value: The value to compare.
 - returns: The same instance of `QueryConstraint` as the receiver.
 - warning: See `equalTo` for more information.
 Behavior changes based on `ParseSwift.configuration.isUsingEqualQueryConstraint`
 where isUsingEqualQueryConstraint == true is known not to work for LiveQuery on
 Parse Servers  <= 5.0.0.
 */
public func == <T>(key: String, value: T) -> QueryConstraint where T: Encodable

For instance, a query that allows us to retrieve all the Contact’s with John in their name field can be created with

1
2
3
4
5
6
7
8
// Create the query sending the constraint as parameter
let constraint: QueryConstraint = containsString(key: "name", substring: "John") // The first parameter (key) referres to the name of the field
let query = Contact.query(constrain)

// Retrieve the contacts asynchronously (or sinchronously if needed)
query.find() { result in
    // Handle the result and do the corresponding UI update
}

In case the constraint requires the name field to match exactly a given string, we can use

1
2
3
4
// Create the query sending the constraint as parameter
let value = "John"
let constraint: QueryConstraint = "name" == value
let query = Contact.query(constrain)

- By number of friends

A query with a constraint involving a numerical comparison can be constructed by creating a QueryConstraint with

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
/**
 Add a constraint that requires that a key is greater than a value.
 - parameter key: The key that the value is stored in.
 - parameter value: The value to compare.
 - returns: The same instance of `QueryConstraint` as the receiver.
 */
public func > <T>(key: String, value: T) -> QueryConstraint where T: Encodable

/**
 Add a constraint that requires that a key is greater than or equal to a value.
 - parameter key: The key that the value is stored in.
 - parameter value: The value to compare.
 - returns: The same instance of `QueryConstraint` as the receiver.
 */
public func >= <T>(key: String, value: T) -> QueryConstraint where T: Encodable

/**
 Add a constraint that requires that a key is less than a value.
 - parameter key: The key that the value is stored in.
 - parameter value: The value to compare.
 - returns: The same instance of `QueryConstraint` as the receiver.
 */
public func < <T>(key: String, value: T) -> QueryConstraint where T: Encodable

/**
 Add a constraint that requires that a key is less than or equal to a value.
 - parameter key: The key that the value is stored in.
 - parameter value: The value to compare.
 - returns: The same instance of `QueryConstraint` as the receiver.
 */
public func <= <T>(key: String, value: T) -> QueryConstraint where T: Encodable

To query all contacts with 30 or more friends, we use

1
2
3
4
5
6
let query = Contacts.query("numberOfFriends" >= 30)

// Retrieve the contacts asynchronously (or sinchronously if needed)
query.find() { result in
    // Handle the result and do the corresponding UI update
}

- Ordering query results

For ordering the results from a query, the Query<Contacts> object provides the method order(_:) which returns a new Query<Contact> object considering the requested ordering option. As a parameter, we pass an enumeration (Query<Contact>.Order) to indicate the ordering we want. The following snippet applies a descending order based on the birthday field

1
2
3
4
5
6
7
8
9
10
11
12
// A query without order to retrieve all the Contact items
let unorderedQuery = Contact.query()

// Sorts the contacts based on their brithday. The parameter in the enumeration is the key of the field used to order the results
let descendingOrder = Query<Contact>.Order.descending("birthday")

let orderedQuery = unorderedQuery.order([descendingOrder]) // Returns a new query with the requested (descending) ordering option

// Executes que query asynchronously and returns a Result<[Contact], ParseError> object with the result
orderedContacts.find() { result in
    // Handle the result
}

In the project example, we implemented the queries mentioned above. The ContactsController class has the method fetchContacts() where you will find the following snippet

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
...

class ContactsController {
    let queryType: QueryType

    ...

    private func fetchContacts() {
        // We create a Query<Contact> according to the queryType enumeration
        let query: Query<Contact> = {
            switch queryType {
            case .byName(let value):
                return Contact.query(containsString(key: "name", substring: value))
            case .byNumberOfFriends(let quantity):
                return Contact.query("numberOfFriends" >= quantity)
            case .byOrdering(let order):
                let query = Contact.query()
                switch order {
                case .ascending: return query.order([.ascending("birthday")])
                case .descending: return query.order([.descending("birthday")])
                }
            case .all:
                return Contact.query()
            }
        }()
        
        // Execute the query
        query.find { [weak self] result in
            switch result {
            case .success(let contacts):
                self?.contacts = contacts
                
                // Update the UI
                DispatchQueue.main.async { self?.tableView.reloadData() }
            case .failure(let error):
                // Notify the user about the error that happened during the fetching process
                self?.showAlert(title: "Error", message: "Failed to retrieve contacts: \(error.message)")
                return
            }
        }
    }
}

Step 4 - Run the app!

Before pressing the run button on XCode, do not forget to configure your Back4App application in the AppDelegate class!

Using the + button in the navigation bar, add a counple of contacts and test the different queries.