iOS

CRUD Parse objects in iOS

Introduction

Storing data on Parse is built around the Parse.Object class. Each Parse.Object contains key-value pairs of JSON-compatible data. This data is schemaless, which means that you don’t need to specify ahead of time what keys exist on each Parse.Object. You can simply set whatever key-value pairs you want, and our backend will store it.

You can also specify the datatypes according to your application needs and persist types such as number, boolean, string, DateTime, list, GeoPointers, and Object, encoding them to JSON before saving. Parse also supports store and query relational data by using the types Pointers and Relations.

In this guide, you will learn how to perform basic data operations through a CRUD example app (ToDo list App), which will show you how to create, read, update and delete data from your Parse server database using the ParseSwift SDK.

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 learn how to perform basic database operations on back4app using a ToDo list app as an example

Prerequisites

To complete this quickstart, you need:

Understanding our To-do List App

To better understand the ParseSwift SDK you will perform CRUD operations on a To-do List App. The application database will have a simple task class with a title and a description (both strings). You can update each task’s title and/or description.

Quick reference of commands we are going to use

Once an object conforms the ParseSwift protocol, it automatically implements a set of methods that will allow you to manage the object and update any changes on your Back4App Database. Given the object ToDoListItem

1
2
3
4
5
6
7
8
9
struct ToDoListItem: ParseObject {
    ...
    
    /// Title for the todo item
    var title: String?
    
    /// Description for the todo item
    var description: String?
}

these methods are listed below.

Once created an instance of ToDoListItem object and set its custom properties, you can save it on your Back4App Database by calling any of the following methods

1
2
3
4
5
6
7
8
9
10
11
var newItem: ToDoIListtem
// newItem's properties

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

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

For reading objects stored on your Back4App Database, ToDoListItem now provides the query() static method which returns a Query<ToDoListItem>. This query object can be constructed using on or more QueryConstraint objects int he following way

1
2
3
4
5
6
7
8
9
10
11
let query = ToDoListItem.query() // A query to fetch all ToDoListItem items on your Back4App Database.
let query = ToDoListItem.query("title" == "Some title") // A query to fetch all ToDoListItem items with title "Some title" on your Back4App Database.
let query = ToDoListItem.query(["title" == "Some title", "description" = "Ok"]) // A query to fetch all ToDoListItem items with title = "Some title" and description = "Ok".

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

// Fetchs 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
}

Given the objectId of an object stored on you Back4App Database, you can update it in the following way

1
2
3
4
5
6
7
8
9
10
let itemToUpdate = ToDoListItem(objectId: "OOBJECT_ID")
// Update the properites of itemToUpdate

// Save changes synchronousty
itemToUpdate.save()

// Or save changes asynchronously
itemToUpdate.save { result in
    // handle the result
}

The deletion process is performed by calling the method delete() on the object to be deleted

1
2
3
4
5
6
7
8
9
var itemToDelete: ToDoListItem

// Delete itemToDelete synchronously
try? itemToDelete.delete()

// Delte itemToDelete asynchronously
itemToDelete.delete { result in
    // Handleresult
}

Step 1 - Create To-do List App Template

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

Go to Xcode, and find the SceneDelegate.swift file. In order to add a navigation bar on top of the app, we setup a UINavigationController as the root view controller in the following way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        
        window = .init(windowScene: scene)
        window?.rootViewController = UINavigationController(rootViewController: ToDoListController())
        window?.makeKeyAndVisible()

        // Additional logic
    }

    ...
}

The root view controller class (ToDoListController) for the navigation controller is a subclass of UITableViewController, this makes easy to layout a list of items.

Step 2 - Setup the CRUD object

Objects you want to save on your Back4App Database have to conform the ParseObject protocol. On our To-do Liat app this object is ToDoListItem. Therefore, you first need to create this object:

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

struct ToDoListItem: ParseObject {
    // Required properties from ParseObject protocol
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    
    /// Title for the todo item
    var title: String?
    
    /// Description for the todo item
    var description: String?
}

This object defines a class in your Back4App Database. Any new instance of this object is then stored in your database under the ToDoListItem class.

Step 3 - Setup ToDoListController

In ToDoListController we should implement all the necessary configuration for the navigationBar, and tableView properties

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
class ToDoListController: UITableViewController {    
    var items: [ToDoListItem] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupTableView()
        setupNavigationBar()
    }
    
    private func setupNavigationBar() {
        navigationItem.title = "To-do list".uppercased()
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(handleNewItem))
    }
    
    private func setupTableView() {
        tableView.register(ToDoListItemCell.self, forCellReuseIdentifier: ToDoListItemCell.identifier)
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ToDoListItemCell.identifier, for: indexPath) as! ToDoListItemCell
        cell.item = items[indexPath.row]
        return cell
    }

    /// This method is called when the user wants to add a new item to the to-do list
    @objc private func handleNewItem() {
        ...
    }

    ...
}

To conclude this step, we implement the custom table view cell ToDoListItemCell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Content of ToDoListItemCell.swift file
class ToDoListItemCell: UITableViewCell {
    class var identifier: String { "\(NSStringFromClass(Self.self)).identifier" } // Cell's identifier
    
    /// When set, it updates the title and detail texts of the cell
    var item: ToDoListItem? {
        didSet {
            textLabel?.text = item?.title
            detailTextLabel?.text = item?.description
        }
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
        
        accessoryType = .detailButton // This accessory button will be used to present edit options for the item
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        accessoryType = .detailButton // This accessory button will be used to present edit options for the item
    }
}

Step 4 - CRUD flow

We implement all CRUD logic in the ToDoListController class. Go to ToDoListController.swidt and add the following methods to the ToDoListController 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
// MARK: - CRUD Flow
extension ToDoListController {
    /// Creates a ToDoListItem and stores it on your Back4App Database
    /// - Parameters:
    ///   - title: The title for the to-do task
    ///   - description: An optional description for the to-to task
    func createObject(title: String, description: String?) {

    }
    
    /// Retrieves all the ToDoListItem objects from your Back4App Database
    func readObjects() {

    }
    
    /// Updates a ToDoListItem object on your Back4App Database
    /// - Parameters:
    ///   - objectId: The object id of the ToDoListItem to update
    ///   - newTitle: New title for the to-to task
    ///   - newDescription: New description for the to-do task
    func updateObject(objectId: String, newTitle: String, newDescription: String?) {

    }
    
    /// Deletes a ToDoListItem on your Back4App Database
    /// - Parameter item: The item to be deleted on your Back4App Database
    func deleteObject(item: ToDoListItem) {

    }
}

- Create Object

Now we start implementing the createObject(title:description:) method. Create an instance of ToDoListItem using the init(title:description:) initializer. In order to save this new item on your Back4App Database, the ParseSwift protocol provides a save() method. This method can be called synchronously or asynchronously, choose one of them according to your use case. An asynchrononous implementation should look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func createObject(title: String, description: String?) {
    let item = ToDoListItem(title: title, description: description)
        
    item.save { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .success(let savedItem):
            self.items.append(savedItem)
            DispatchQueue.main.async {
                self.tableView.insertRows(at: [IndexPath(row: self.items.count - 1, section: 0)], with: .right)
            }
        case .failure(let error):
            DispatchQueue.main.async {
                self.showAlert(title: "Error", message: "Failed to save item: \(error.message)")
            }
        }
    }
}

Now we can complete the action for the add button located at the right side of the navigation bar. Go to ToDoListController and add the following

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
class ToDoListController: UITableViewController {
    enum ItemDescription: Int { case title = 0, description = 1 }

    ...

    /// This method is called when the user wants to add a new item to the to-do list
    @objc private func handleNewItem() {
        showEditController(item: nil)
    }
    
    /// Presents an alert where the user enters a to-do task for either create a new one (item parameter is nil) or edit an existing one
    private func showEditController(item: ToDoListItem?) {
        let controllerTitle: String = item == nil ? "New item" : "Update item"
        
        let editItemAlertController = UIAlertController(title: controllerTitle, message: nil, preferredStyle: .alert)
        
        editItemAlertController.addTextField { textField in
            textField.tag = ItemDescription.title.rawValue
            textField.placeholder = "Title"
            textField.text = item?.title
        }

        editItemAlertController.addTextField { textField in
            textField.tag = ItemDescription.description.rawValue
            textField.placeholder = "Description"
            textField.text = item?.description
        }
        
        let mainActionTitle: String = item == nil ? "Add" : "Update"
        
        let mainAction: UIAlertAction = UIAlertAction(title: mainActionTitle, style: .default) { [weak self] _ in
            guard let title = editItemAlertController.textFields?.first(where: { $0.tag == ItemDescription.title.rawValue })?.text else {
                return editItemAlertController.dismiss(animated: true, completion: nil)
            }
            
            let description = editItemAlertController.textFields?.first(where: { $0.tag == ItemDescription.description.rawValue })?.text
            
            editItemAlertController.dismiss(animated: true) {
                if let objectId = item?.objectId { // if the item passed as parameter is not nil, the alert will update it
                    self?.updateObject(objectId: objectId, newTitle: title, newDescription: description)
                } else {
                    self?.createObject(title: title, description: description)
                }
            }
        }
                
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        
        editItemAlertController.addAction(mainAction)
        editItemAlertController.addAction(cancelAction)

        present(editItemAlertController, animated: true, completion: nil)
    }
}

- Read Object

We move to the readObjects() method. Retreiveing ToDoListItem items from your Back4App Database is performed via a Query<ToDoListItem> object. This query is instanciated in the following way

1
2
3
4
func readObjects() {
    let query = ToDoListItem.query()
    ...
}

In this tutorial we use a query which will retreive all the items of type ToDoListItem from your Back4App Database. In case you want to retreive a set of specific items, you can provide QueryConstraint elements to ToDoListItem.query(QueryConstraint...). For instance, to fetch all items where title == "Some title", the query takes the form

1
let query = ToDoListItem.query("title" == "Some title")

Once you have the query ready, we proceed to retreive the items by calling query.find(). Again, this can be done synchronously or asynchronously. In our To-do List app we implement it asynchronously

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func readObjects() {
    let query = ToDoListItem.query()
        
    query.find { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .success(let items):
            self.items = items
            DispatchQueue.main.async {
                self.tableView.reloadSections([0], with: .top)
            }
        case .failure(let error):
            DispatchQueue.main.async {
                self.showAlert(title: "Error", message: "Failed to save item: \(error.message)")
            }
        }
    }
}

With readObjects() completed, we can now fetch all the tasks stored in your Back4App Database and show them right after the app enters to foreground. Go back to ToDoListController and override the viewDidAppear() method

1
2
3
4
5
6
7
8
9
10
11
class ToDoListController: UITableViewController {
    ...
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        readObjects()
    }

    ...
}

- Update object

Given the objectId of a ToDoListItem object, it is straightforward to perform an update. We simply instanciate a ToDoListItem object using the init(objectId:) initializer. Next, we update the properties we need and call the save() method (of ToDoListItem) to save changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func updateObject(objectId: String, newTitle: String, newDescription: String?) {
    var item = ToDoListItem(objectId: objectId)
    item.title = newTitle
    item.description = newDescription
        
    item.save { [weak self] result in
        switch result {
        case .success:
            if let row = self?.items.firstIndex(where: { $0.objectId == item.objectId }) {
                self?.items[row] = item
                DispatchQueue.main.async {
                    self?.tableView.reloadRows(at: [IndexPath(row: row, section: 0)], with: .fade)
                }
            }
        case .failure(let error):
            DispatchQueue.main.async {
                self?.showAlert(title: "Error", message: "Failed to save item: \(error.message)")
            }
        }
    }
}

- Delete object

Deleting objects on your Back4App Database is very similar to creating objects. We begin by creating an instance of ToDoListItem with the objectId of the item we want to delete. Next, we simply call (synchronously or ascynchronously) the delete() method of the object. If the deletion was successfull we update the UI, otherwise we report the error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func deleteObject(item: ToDoListItem) {
    item.delete { [weak self] result in
        switch result {
        case .success:
            if let row = self?.items.firstIndex(where: { $0.objectId == item.objectId }) {
                self?.items.remove(at: row)
                DispatchQueue.main.async {
                    self?.tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .left)
                }
            }
        case .failure(let error):
            DispatchQueue.main.async {
                self?.showAlert(title: "Error", message: "Failed to save item: \(error.message)")
            }
        }
    }
}

With deleteObject(item:) and updateObject(objectId:newTitle:newDescription) completed, we proceed to add the corresponding actions to call these operations. Go back to ToDoListController and add

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
// MARK: UITableViewDataSource delegate
extension ToDoListController {
    // When the user taps on the accessory button of a cell, we present the edit options for the to-do list task
    override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
        guard !items.isEmpty else { return }
        
        showEditOptions(item: items[indexPath.row])
    }
    
    /// Presents a sheet where the user can select an action for the to-do list item
    private func showEditOptions(item: ToDoListItem) {
        let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
        
        let editAction = UIAlertAction(title: "Edit", style: .default) { [weak self] _ in
            self?.showEditController(item: item)
        }
        
        let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in
            alertController.dismiss(animated: true) {
                self?.deleteObject(item: item)
            }
        }
        
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
            alertController.dismiss(animated: true, completion: nil)
        }
        
        alertController.addAction(editAction)
        alertController.addAction(deleteAction)
        alertController.addAction(cancelAction)

        present(alertController, animated: true, completion: nil)
    }
}

As we pointed out earlier, the accessory button in each ToDoListItemCell triggers an edit sheet via the tableView(_:accessoryButtonTappedForRowWith:) delegate method.

It’s done!

At this point, you have learned how to do the basic CRUD operations with Parse on iOS.