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 andNdata objects
N:N: A relation beweenNdata objects toNdata 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.
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:
Genre
Author
Publisher
ISBN
Book
1 import Foundation
2 import ParseSwift
34 struct Genre: ParseObject {5...67 var name: String?89...10}
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:
Create
Read
Add relations
Query relations
Remaining relations
//When creating a new instance of Author we use1 var newAuthor: Author =Author(name:"John Doe")23// Saves newAuthor on your Back4App Database synchronously and returns the new saved Item. It throws and error if something went wrong.4 let savedAuthor =try? newAuthor.save()56// Saves newAuthor on your Back4App Database asynchronously, and passes a Result<Author, ParseError> object to the completion block to handle the save process.7 newAuthor.save { result in8// Handle the result to check wether the save process was successfull or not9}
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.
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 theMainController+ParseSwift.swift file, under an extension for the MainController class, we implemented the following methods
Swift
1// MainController+ParseSwift.swift file2 extension MainController {3/// Collects the data to save an instance of Book on your Back4App database.4 func saveBook(){5 view.endEditing(true)67// 1. First retrieve all the information for the Book (title, isbn, etc)8 guard let bookTitle = bookTitleTextField.text else{9returnpresentAlert(title:"Error", message:"Invalid book title")10}1112 guard let isbnValue = isbnTextField.text else{13returnpresentAlert(title:"Error", message:"Invalid ISBN value.")14}1516 let query = ISBN.query("value"== isbnValue)1718 guard (try? query.first())== nil else{19returnpresentAlert(title:"Error", message:"The entered ISBN already exists.")20}2122 guard let genreObjectId = genreOptionsView.selectedOptionIds.first,23 let genre = genres.first(where:{ $0.objectId == genreObjectId})24else{25returnpresentAlert(title:"Error", message:"Invalid genre.")26}2728 guard let publishingYearString = publishingYearTextField.text, let publishingYear =Int(publishingYearString)else{29returnpresentAlert(title:"Error", message:"Invalid publishing year.")30}3132 let authors:[Author]= self.authorOptionsView.selectedOptionIds.compactMap {[weak self] objectId in33 self?.authors.first(where:{ objectId == $0.objectId })34}3536 let publishers:[Publisher]= self.publisherOptionsView.selectedOptionIds.compactMap {[weak self] objectId in37 self?.publishers.first(where:{ objectId == $0.objectId })38}3940// Since we are making multiple requests to Back4App, it is better to use synchronous methods and dispatch them on the background queue41 DispatchQueue.global(qos:.background).async {42do{43 let isbn =ISBN(value: isbnValue)// 2. Instantiate a new ISBN object4445 let savedBook =tryBook(// 3. Instantiate a new Book object with the corresponding input fields46 title: bookTitle,47 publishingYear: publishingYear,48 genre: genre,49 isbn: isbn
50).save()// 4. Save the new Book object5152...// Here we will implement the relations5354 DispatchQueue.main.async {55 self.presentAlert(title:"Success", message:"Book saved successfully.")56}57}catch{58 DispatchQueue.main.async {59 self.presentAlert(title:"Error", message:"Failed to save book: \((error as! ParseError).message)")60}61}62}63}6465/// Retrieves all the data saved under the Genre class in your Back4App Database66 func fetchGenres(){67 let query = Genre.query()6869 query.find {[weak self] result in70 switch result {71 case .success(let genres):72 self?.genres = genres // When setting self?.genres, it triggers the corresponding UI update73 case .failure(let error):74 self?.presentAlert(title:"Error", message: error.message)75}76}77}7879/// Presents a simple alert where the user can enter the name of a genre to save it on your Back4App Database80 func handleAddGenre(){81// Displays a form with a single input and executes the completion block when the user presses the submit button82presentForm(83 title:"Add genre",84 description:"Enter a description for the genre",85 placeholder: nil
86){[weak self] name in87 guard let name = name else{return}88 let genre =Genre(name: name)8990 let query = Genre.query("name"== name)9192 guard ((try? query.first())== nil)else{93 self?.presentAlert(title:"Error", message:"This genre already exists.")94return95}9697 genre.save {[weak self] result in98 switch result {99 case .success(let addedGenre):100 self?.presentAlert(title:"Success", message:"Genre added!")101 self?.genres.append(addedGenre)102 case .failure(let error):103 self?.presentAlert(title:"Error", message:"Failed to save genre: \(error.message)")104}105}106}107}108109/// Retrieves all the data saved under the Publisher class in your Back4App Database110 func fetchPublishers(){111 let query = Publisher.query()112113 query.find {[weak self] result in114 switch result {115 case .success(let publishers):116 self?.publishers = publishers
117 case .failure(let error):118 self?.presentAlert(title:"Error", message: error.message)119}120}121}122123/// Presents a simple alert where the user can enter the name of a publisher to save it on your Back4App Database124 func handleAddPublisher(){125// Displays a form with a single input and executes the completion block when the user presses the submit button126presentForm(127 title:"Add publisher",128 description:"Enter the name of the publisher",129 placeholder: nil
130){[weak self] name in131 guard let name = name else{return}132133 let query = Publisher.query("name"== name)134135 guard ((try? query.first())== nil)else{136 self?.presentAlert(title:"Error", message:"This publisher already exists.")137return138}139140 let publisher =Publisher(name: name)141142 publisher.save {[weak self] result in143 switch result {144 case .success(let addedPublisher):145 self?.presentAlert(title:"Success", message:"Publisher added!")146 self?.publishers.append(addedPublisher)147 case .failure(let error):148 self?.presentAlert(title:"Error", message:"Failed to save publisher: \(error.message)")149}150}151}152}153154/// Retrieves all the data saved under the Genre class in your Back4App Database155 func fetchAuthors(){156 let query = Author.query()157158 query.find {[weak self] result in159 switch result {160 case .success(let authors):161 self?.authors = authors
162 case .failure(let error):163 self?.presentAlert(title:"Error", message: error.message)164}165}166}167168/// Presents a simple alert where the user can enter the name of an author to save it on your Back4App Database169 func handleAddAuthor(){170// Displays a form with a single input and executes the completion block when the user presses the submit button171presentForm(172 title:"Add author",173 description:"Enter the name of the author",174 placeholder: nil
175){[weak self] name in176 guard let name = name else{return}177178 let query = Author.query("name"== name)179180 guard ((try? query.first())== nil)else{181 self?.presentAlert(title:"Error", message:"This author already exists.")182return183}184185 let author =Author(name: name)186187 author.save {[weak self] result in188 switch result {189 case .success(let addedAuthor):190 self?.presentAlert(title:"Success", message:"Author added!")191 self?.authors.append(addedAuthor)192 case .failure(let error):193 self?.presentAlert(title:"Error", message:"Failed to save author: \(error.message)")194}195}196}197}198}sw
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.
4 - Implementing relations
1:1 case
Adding 1:1 relations can easlity be achieved by adding a property in the Book object, i.e.,
Swift
1 struct Book: ParseObject {2...34 var isbn: ISBN?// Esablishes a 1:1 relation between Book and ISBN56...7}
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
Swift
1 let someBook: Book
2 let authors:[Author]3...45// We create a relation (identified by the name 'authors') between someBook and a set of authors6 let bookToAuthorsRelation =7 guard let bookToAuthorsRelation =try someBook.relation?.add("authors", objects: authors)// Book -> Author 8else{9fatalError("Failed to add relation")10}1112 let savedRelation =try bookToAuthorsRelation.save()// Saves the relation synchronously1314 bookToAuthorsRelation.save { result in// Saves the relation asynchronously15// Handle the result16}
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
Swift
1 extension MainController {2/// Collects the data to save an instance of Book on your Back4App database.3 func saveBook(){4...5// 1. First retrieve all the information for the Book (bookTitle, isbnValue, etc)6...78// Since we are making multiple requests to Back4App, it is better to use synchronous methods and dispatch them on the background queue9 DispatchQueue.global(qos:.background).async {10do{11 let isbn =ISBN(value: isbnValue)// 2. Instantiate a new ISBN object1213 let savedBook =tryBook(// 3. Instantiate a new Book object with the corresponding input fields14 title: bookTitle,15 publishingYear: publishingYear,16 genre: genre,17 isbn: isbn
18).save()// 4. Save the new Book object1920// 5. Add the corresponding relations for new Book object21 guard let bookToAuthorsRelation =try savedBook.relation?.add("authors", objects: authors),// Book -> Author22 let bootkToPublishersRelation =try savedBook.relation?.add("publishers", objects: publishers),// Book -> Publisher23 let genreRelation =try genre.relation?.add("books", objects:[savedBook])// Genre -> Book24else{25return DispatchQueue.main.async {26 self.presentAlert(title:"Error", message:"Failed to add relations")27}28}2930// 6. Save the relations31 _ =try bookToAuthorsRelation.save()32 _ =try bootkToPublishersRelation.save()33 _ =try genreRelation.save()3435 DispatchQueue.main.async {36 self.presentAlert(title:"Success", message:"Book saved successfully.")37}38}catch{39 DispatchQueue.main.async {40 self.presentAlert(title:"Error", message:"Failed to save book: \((error as! ParseError).message)")41}42}43}44}4546...47}
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
Swift
1 let query = Book.query().include("isbn")23 let books =try query.find()// Retrieves synchronously all the books together with its isbn45 query.find { result in// Retrieves asynchronously all the books together with its isbn6// Handle the result7}8
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
Swift
1 let book: Book // Book from which we are trying to retrieve its related authors23do{4 let authorsQuery =try Author.queryRelations("authors", parent: book)// 'authors' is the name of the relation it was saved with56 authorsQuery.find {[weak self] result in7 switch result {8 case .success(let authors):9 self?.authors = authors
1011 DispatchQueue.main.async {12// Update the UI13}14 case .failure(let error):15 DispatchQueue.main.async {16 self?.presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.message)")17}18}19}20}21}catch{22if let error = error as? ParseError {23presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.message)")24}else{25presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.localizedDescription)")26}27}
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
Swift
1// BookDetailsController.swift file2 class BookDetailsController: UITableViewController {3...45/// Retrieves the book's details, i.e., its relation with authors and publishers6 private func fetchDetails(){7do{8// Constructs the relations you want to query9 let publishersQuery =try Publisher.queryRelations("publishers", parent: book)10 let authorsQuery =try Author.queryRelations("authors", parent: book)1112// Obtains the publishers related to book and display them on the tableView, it presents an error if happened.13 publishersQuery.find {[weak self] result in14 switch result {15 case .success(let publishers):16 self?.publishers = publishers
1718// Update the UI19 DispatchQueue.main.async {20 self?.tableView.reloadSections(IndexSet([Section.publisher.rawValue]), with:.none)21}22 case .failure(let error):23 DispatchQueue.main.async {24 self?.presentAlert(title:"Error", message:"Failed to retrieve publishers: \(error.message)")25}26}27}2829// Obtains the authors related to book and display them on the tableView, it presents an error if happened.30 authorsQuery.find {[weak self] result in31 switch result {32 case .success(let authors):33 self?.authors = authors
3435// Update the UI36 DispatchQueue.main.async {37 self?.tableView.reloadSections(IndexSet([Section.author.rawValue]), with:.none)38}39 case .failure(let error):40 DispatchQueue.main.async {41 self?.presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.message)")42}43}44}45}catch{// If there was an error during the creation of the queries, this block should catch it46if let error = error as? ParseError {47presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.message)")48}else{49presentAlert(title:"Error", message:"Failed to retrieve authors: \(error.localizedDescription)")50}51}52}5354...55}
6 - Run the app!
Before pressing the run button on XCode, do not forget to configure your Back4App application in the AppDelegateclass!
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.