iOS

Relationships

Introduction

Relations are a fundamental feature for organizing data objects stored on a database. ParseSwift does provide the necessary tools and methods to establish relations between classes in your Back4App Database. Depending on the use case, we can identify the following type of relations

  • 1:1: A relation that only connects two data objects.
  • 1:N: A relation between one data object and N data objects.
  • N:N: A relation beween N data objects to N data objects.

As we will see below, implementing 1:1 relations are relatively straightforward. For 1:N and N:N relations, the implementation involves the ParseRelation object provided by ParseSwift SDK. There are additional alternatives for implementing 1:N and N:N relations. However, due to efficiency and performance, we recommend following our approach

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 relations are implemented in a Back4App Database.

Prerequisites

To complete this quickstart, you need:

Understanding our Books App

The project template is a Book App where the user enters a book details to save it on a Back4App Database. On the app’s homescreen you will find the form to do so

Using the + button located on the top-right side of the navigation bar, we can add as many Publishers, Genres and Authors as needed. Once the user enters a book, they can use the Add book button to save the book on their Back4App Database. Additionally, the List books button will allow us to show all the books the user added and also to see their relationship with the publishers and authors.

Quick reference of commands we are going to use

We make use of the objects Author, Publisher, ISBN and Book:

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

struct Genre: ParseObject {
    ...
    
    var name: String?
    
    ...
}
1
2
3
4
5
6
7
8
9
10
import Foundation
import ParseSwift

struct Author: ParseObject {
    ...
    
    var name: String?
    
    ...
}
1
2
3
4
5
6
7
8
9
10
import Foundation
import ParseSwift

struct Publisher: ParseObject {
    ...
    
    var name: String?
    
    ...
}
1
2
3
4
5
6
7
8
9
10
import Foundation
import ParseSwift

struct ISBN: ParseObject {
    ...
    
    var value: String?
    
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import Foundation
import ParseSwift

struct Book: ParseObject {
    ...

    var title: String?
    var publishingYear: Int?
    var genre: Genre?
    var isbn: ISBN?
    
    ...
}

Before storing instances of these objects in a Back4App Database, all their properties must conform to the Codable and Hashable protocols.

We make use of the following methods for managing these objects on the Back4App Database:

When creating a new instance of Author we use

1
2
3
4
5
6
7
8
9
var newAuthor: Author = Author(name: "John Doe")

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

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

For the remaining objects Genre and Publisher, the implementation is similar.

For reading any of the objects introduced above, we construct the corresponding queries for each of them. For Author we have

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

// Fetches the items synchronously or throws an error if found.
let fetchedAuthors = 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 1:1 relations, it is sufficient to have the child object as a property of the parent object. Back4App automatically saves the child object when the parent object is saved.

For the remaining relations, we use the add(_:objects:) method via the relation property available in the parent ParseObject. Adding a relation called authors on a Book object would look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let someBook: Book
let authors: [Author]

// Adds the relation between someBook and authors under the name 'authors'
let bookToAuthorsRelation = try? someBook.relation?.add("authors", objects: authors)

// Saves the relation synchronously
let updatedSomeBook = try? bookToAuthorsRelation.save()

// Saves the relation asynchronously
bookToAuthorsRelation.save { result in
    // Handle the result
}

For 1:1 relations, it is enough to append the include() method in the query. For instance, to query all the books together with their ISBN relation, we use

1
2
3
4
5
6
7
8
9
var query = Book.query().include("isbn")

// Fetches all books synchronously
let books = try? query.find()

// Fetches all books asynchronously
query.find { result in
    // Handle the result
}

For the remaining relations, we create a query by using the static method queryRelation(_,parent:) provided by the ParseObject protocol. Querying the authors related to a book can be implemented in the following way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let someBook: Book
let authors: [Author]
...

// We create a relation (identified by the name 'authors') betwee someBook and a set of authors
let bookToAuthorsRelation = 
guard let bookToAuthorsRelation = try someBook.relation?.add("authors", objects: authors) // Book -> Author 
else {
    fatalError("Failed to add relation")
}

let savedRelation = try bookToAuthorsRelation.save() // Saves the relation synchronously

bookToAuthorsRelation.save { result in // Saves the relation asynchronously
    // Handle the result
}

Step 1 - Download the Books 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 relationships and the ParseSwift SDK.

Step 2 - Additional CRUD flow

Before going further, it is necessary to implement some CRUD functions for us to be able to save the Author, Publisher and Genre objects. In the MainController+ParseSwift.swift file, under an extension for the MainController class, we implemented the following methods

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// MainController+ParseSwift.swift file
extension MainController {
    /// Collects the data to save an instance of Book on your Back4App database.
    func saveBook() {
        view.endEditing(true)
        
        // 1. First retrieve all the information for the Book (title, isbn, etc)
        guard let bookTitle = bookTitleTextField.text else {
            return presentAlert(title: "Error", message: "Invalid book title")
        }
        
        guard let isbnValue = isbnTextField.text else {
            return presentAlert(title: "Error", message: "Invalid ISBN value.")
        }
        
        let query = ISBN.query("value" == isbnValue)
        
        guard (try? query.first()) == nil else {
            return presentAlert(title: "Error", message: "The entered ISBN already exists.")
        }
        
        guard let genreObjectId = genreOptionsView.selectedOptionIds.first,
              let genre = genres.first(where: { $0.objectId == genreObjectId})
        else {
            return presentAlert(title: "Error", message: "Invalid genre.")
        }
        
        guard let publishingYearString = publishingYearTextField.text, let publishingYear = Int(publishingYearString) else {
            return presentAlert(title: "Error", message: "Invalid publishing year.")
        }
        
        let authors: [Author] = self.authorOptionsView.selectedOptionIds.compactMap { [weak self] objectId in
            self?.authors.first(where: { objectId == $0.objectId })
        }
        
        let publishers: [Publisher] = self.publisherOptionsView.selectedOptionIds.compactMap { [weak self] objectId in
            self?.publishers.first(where: { objectId == $0.objectId })
        }
        
        // Since we are making multiple requests to Back4App, it is better to use synchronous methods and dispatch them on the background queue
        DispatchQueue.global(qos: .background).async {
            do {
                let isbn = ISBN(value: isbnValue) // 2. Instantiate a new ISBN object
                
                let savedBook = try Book( // 3. Instantiate a new Book object with the corresponding input fields
                    title: bookTitle,
                    publishingYear: publishingYear,
                    genre: genre,
                    isbn: isbn
                ).save() // 4. Save the new Book object
                
                ... // Here we will implement the relations
                
                DispatchQueue.main.async {
                    self.presentAlert(title: "Success", message: "Book saved successfully.")
                }
            } catch {
                DispatchQueue.main.async {
                    self.presentAlert(title: "Error", message: "Failed to save book: \((error as! ParseError).message)")
                }
            }
        }
    }
    
    /// Retrieves all the data saved under the Genre class in your Back4App Database
    func fetchGenres() {
        let query = Genre.query()
        
        query.find { [weak self] result in
            switch result {
            case .success(let genres):
                self?.genres = genres // When setting self?.genres, it triggers the corresponding UI update
            case .failure(let error):
                self?.presentAlert(title: "Error", message: error.message)
            }
        }
    }
    
    /// Presents a simple alert where the user can enter the name of a genre to save it on your Back4App Database
    func handleAddGenre() {
        // Displays a form with a single input and executes the completion block when the user presses the submit button
        presentForm(
            title: "Add genre",
            description: "Enter a description for the genre",
            placeholder: nil
        ) { [weak self] name in
            guard let name = name else { return }
            let genre = Genre(name: name)
            
            let query = Genre.query("name" == name)
            
            guard ((try? query.first()) == nil) else {
                self?.presentAlert(title: "Error", message: "This genre already exists.")
                return
            }
            
            genre.save { [weak self] result in
                switch result {
                case .success(let addedGenre):
                    self?.presentAlert(title: "Success", message: "Genre added!")
                    self?.genres.append(addedGenre)
                case .failure(let error):
                    self?.presentAlert(title: "Error", message: "Failed to save genre: \(error.message)")
                }
            }
        }
    }
    
    /// Retrieves all the data saved under the Publisher class in your Back4App Database
    func fetchPublishers() {
        let query = Publisher.query()
        
        query.find { [weak self] result in
            switch result {
            case .success(let publishers):
                self?.publishers = publishers
            case .failure(let error):
                self?.presentAlert(title: "Error", message: error.message)
            }
        }
    }
    
    /// Presents a simple alert where the user can enter the name of a publisher to save it on your Back4App Database
    func handleAddPublisher() {
        // Displays a form with a single input and executes the completion block when the user presses the submit button
        presentForm(
            title: "Add publisher",
            description: "Enter the name of the publisher",
            placeholder: nil
        ) { [weak self] name in
            guard let name = name else { return }
            
            let query = Publisher.query("name" == name)
            
            guard ((try? query.first()) == nil) else {
                self?.presentAlert(title: "Error", message: "This publisher already exists.")
                return
            }
            
            let publisher = Publisher(name: name)
            
            publisher.save { [weak self] result in
                switch result {
                case .success(let addedPublisher):
                    self?.presentAlert(title: "Success", message: "Publisher added!")
                    self?.publishers.append(addedPublisher)
                case .failure(let error):
                    self?.presentAlert(title: "Error", message: "Failed to save publisher: \(error.message)")
                }
            }
        }
    }
    
    /// Retrieves all the data saved under the Genre class in your Back4App Database
    func fetchAuthors() {
        let query = Author.query()
        
        query.find { [weak self] result in
            switch result {
            case .success(let authors):
                self?.authors = authors
            case .failure(let error):
                self?.presentAlert(title: "Error", message: error.message)
            }
        }
    }
    
    /// Presents a simple alert where the user can enter the name of an author to save it on your Back4App Database
    func handleAddAuthor() {
        // Displays a form with a single input and executes the completion block when the user presses the submit button
        presentForm(
            title: "Add author",
            description: "Enter the name of the author",
            placeholder: nil
        ) { [weak self] name in
            guard let name = name else { return }
            
            let query = Author.query("name" == name)
            
            guard ((try? query.first()) == nil) else {
                self?.presentAlert(title: "Error", message: "This author already exists.")
                return
            }
            
            let author = Author(name: name)
            
            author.save { [weak self] result in
                switch result {
                case .success(let addedAuthor):
                    self?.presentAlert(title: "Success", message: "Author added!")
                    self?.authors.append(addedAuthor)
                case .failure(let error):
                    self?.presentAlert(title: "Error", message: "Failed to save author: \(error.message)")
                }
            }
        }
    }
}

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

Step 3 - Setup the relations

Before starting to create relations, take a look at the quick reference section to have an idea about the objects we want to relate to each other. In the figure below we show how these objects are related

As can be seen, the relations are created by putting the Book object in the middle. The arrows show how each object is related to a Book object.

Step 4 - Implementing relations

1:1 case

Adding 1:1 relations can easlity be achieved by adding a property in the Book object, i.e.,

1
2
3
4
5
6
7
struct Book: ParseObject {
    ...

    var isbn: ISBN? // Esablishes a 1:1 relation between Book and ISBN
    
    ...
}

In this case, Book and ISBN share a 1:1 relation where Book is identified as the parent and ISBN as the child. Internally, when Back4App saves an instance of Book, it saves the ISBN object (under the ISBN class name) first. After this process is complete, Back4App continues with the object Book. The new Book object is saved in a way that its isbn property is represented by a Pointer<ISBN> object. A Pointer<> object allows us to store a unique instance of the ISBN object related to its corresponding parent.

1:N case

For 1:N relations, the most efficient way to implement them is via a ParseRelation<Book> object. ParseSwift provides a set of methods to add these types of relationships for any object conforming to the ParseObject protocol. For instance, if we want to create a 1:N relation between Book and Author we can use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let someBook: Book
let authors: [Author]
...

// We create a relation (identified by the name 'authors') between someBook and a set of authors
let bookToAuthorsRelation = 
guard let bookToAuthorsRelation = try someBook.relation?.add("authors", objects: authors) // Book -> Author 
else {
    fatalError("Failed to add relation")
}

let savedRelation = try bookToAuthorsRelation.save() // Saves the relation synchronously

bookToAuthorsRelation.save { result in // Saves the relation asynchronously
    // Handle the result
}

It is straightforward to adapt this snippet for the other relations we have for Book.

- Putting all together

Once we established the basic idea to implement relations, we now complete the saveBook() method. We enumerate the key points to have in mind during this process

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
46
47
extension MainController {
    /// Collects the data to save an instance of Book on your Back4App database.
    func saveBook() {
        ...        
        // 1. First retrieve all the information for the Book (bookTitle, isbnValue, etc)
        ...

        // Since we are making multiple requests to Back4App, it is better to use synchronous methods and dispatch them on the background queue
        DispatchQueue.global(qos: .background).async {
            do {
                let isbn = ISBN(value: isbnValue) // 2. Instantiate a new ISBN object
                
                let savedBook = try Book( // 3. Instantiate a new Book object with the corresponding input fields
                    title: bookTitle,
                    publishingYear: publishingYear,
                    genre: genre,
                    isbn: isbn
                ).save() // 4. Save the new Book object
                
                // 5. Add the corresponding relations for new Book object
                guard let bookToAuthorsRelation = try savedBook.relation?.add("authors", objects: authors), // Book -> Author
                      let bootkToPublishersRelation = try savedBook.relation?.add("publishers", objects: publishers), // Book -> Publisher
                      let genreRelation = try genre.relation?.add("books", objects: [savedBook]) // Genre -> Book
                else {
                    return DispatchQueue.main.async {
                        self.presentAlert(title: "Error", message: "Failed to add relations")
                    }
                }
                                
                // 6. Save the relations
                _ = try bookToAuthorsRelation.save()
                _ = try bootkToPublishersRelation.save()
                _ = try genreRelation.save()
                
                DispatchQueue.main.async {
                    self.presentAlert(title: "Success", message: "Book saved successfully.")
                }
            } catch {
                DispatchQueue.main.async {
                    self.presentAlert(title: "Error", message: "Failed to save book: \((error as! ParseError).message)")
                }
            }
        }
    }

    ...
}

Step 5 - Querying relations

For 1:1 relations, given the parent Book and its child ISBN, we query the corresponding child by including it in the Book query

1
2
3
4
5
6
7
8
let query = Book.query().include("isbn")

let books = try query.find() // Retrieves synchronously all the books together with its isbn

query.find { result in // Retrieves asynchronously all the books together with its isbn
    // Handle the result
}

With this, all the books from the query will also have the isbn property set properly with the related ISBN object.

On the other hand, in order to retrieve objects with a 1:N relation, the ParseObject protocol provides the static method queryRelation(_,parent:). By providing the name of the relation (as the first parameter) and the parent, this method allows us to create the required query. For instance, to retrieve all the Author’s related to a specific Book we can use 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
let book: Book // Book from which we are trying to retrieve its related authors

do {
    let authorsQuery = try Author.queryRelations("authors", parent: book) // 'authors' is the name of the relation it was saved with
        
    authorsQuery.find { [weak self] result in
        switch result {
        case .success(let authors):
            self?.authors = authors
                    
            DispatchQueue.main.async {
                // Update the UI
            }
            case .failure(let error):
                DispatchQueue.main.async {
                    self?.presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.message)")
                }
            }
        }
    }
} catch {
    if let error = error as? ParseError {
        presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.message)")
    } else {
        presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.localizedDescription)")
    }
}

Similarly, we can query other related objects such as Publisher.

In the BookDetailsController.swift file we implement these queries to display the relation a book has with authors and publishers

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
46
47
48
49
50
51
52
53
54
55
// BookDetailsController.swift file
class BookDetailsController: UITableViewController {
    ...
    
    /// Retrieves the book's details, i.e., its relation with authors and publishers
    private func fetchDetails() {
        do {
            // Constructs the relations you want to query
            let publishersQuery = try Publisher.queryRelations("publishers", parent: book)
            let authorsQuery = try Author.queryRelations("authors", parent: book)
            
            // Obtains the publishers related to book and display them on the tableView, it presents an error if happened.
            publishersQuery.find { [weak self] result in
                switch result {
                case .success(let publishers):
                    self?.publishers = publishers
                    
                    // Update the UI
                    DispatchQueue.main.async {
                        self?.tableView.reloadSections(IndexSet([Section.publisher.rawValue]), with: .none)
                    }
                case .failure(let error):
                    DispatchQueue.main.async {
                        self?.presentAlert(title: "Error", message: "Failed to retrieve publishers: \(error.message)")
                    }
                }
            }
            
            // Obtains the authors related to book and display them on the tableView, it presents an error if happened.
            authorsQuery.find { [weak self] result in
                switch result {
                case .success(let authors):
                    self?.authors = authors
                    
                    // Update the UI
                    DispatchQueue.main.async {
                        self?.tableView.reloadSections(IndexSet([Section.author.rawValue]), with: .none)
                    }
                case .failure(let error):
                    DispatchQueue.main.async {
                        self?.presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.message)")
                    }
                }
            }
        } catch { // If there was an error during the creation of the queries, this block should catch it
            if let error = error as? ParseError {
                presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.message)")
            } else {
                presentAlert(title: "Error", message: "Failed to retrieve authors: \(error.localizedDescription)")
            }
        }
    }
    
    ...
}

Step 6 - Run the app!

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

You have to add a couple of Genre’s, Publisher’s and Author’s before adding a new book. Then, you can start entering a book’s information to save it on your Back4App Database. Once you have saved one Book, open your Back4App dashboard and go to your application linked to the XCode project. In the Database section, you will find the class Book where all the books created by the iOS App are stored.

Additionally, you can see that Back4App automatically created the class ISBN in order to relate it with its corresponding Book object. If you go back to the Book class, you can identify the data types for each type of relation. In the case of ISBN and Genre, the data type is a Pointer<>. On the other hand, for relations like Author and Publisher, the data type is Relation<>. This is a key difference to have in mind when constructing relations.