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 toN
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:
- Xcode.
- An app created at Back4App.
- Follow the New Parse App tutorial to learn how to create a Parse app at Back4App.
- Note: Follow the Install Parse SDK (Swift) Tutorial to create an Xcode Project connected to Back4App.
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 yourBack4App
application in theAppDelegate
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.