iOS

Parse data types on Swift

Introduction

When saving data on a Back4App Database, each entity is stored in a key-value pair format. The data type for the value field goes from the fundamental ones (such as String, Int, Double, Float, and Bool) to more complex structures. The main requirement for storing data on a Back4App Database is that the entity has to conform the ParseSwift protocol. On its turn, this protocol provides a set of methods to store, update and delete any instance of an entity.

In this guide, you will learn how to create and setup an entity to save it on your Back4App Database. In the project example the entity we are storing encloses information about a Recipe.

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 objects are parsed and stored on a Back4App Database.

Prerequisites

To complete this quickstart, you need:

Understanding our Recipes App

The app functionality is based on a form where one can enter information about a recipe. Depending on the information, the data type may vary. In our example the recipe has the following features:

Field Data type Description
Name String Name of the recipe
Servings Int Number of servings
Available Bool Determines whether the recipe is available or not
Category Category A custom enumeration which classifies a recipe in three categories: Breakfast, Lunch and Dinner
Ingredients [Ingredient] The set of ingredients enclosed in a custom Ingredient struct
Side options [String] Names of the additional options the recipe comes with
Nutritional information [String:String] A dictionary containing information about the recipe’s nutritional content
Release date Date A date showing when the recipe was available

Additionally, there are more data types which are used to implement Database functionality like relation between objects. These data types are not covered in this tutorial.

Quick reference of commands we are going to use

Given an object, say Recipe, if you want to save it on a Back4App Database, you have to first make this object to conform the ParseSwift protocol (available via the ParseSwift SDK).

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
import Foundation
import ParseSwift

struct Recipe: ParseObject {
    /// Enumeration for the recipe category
    enum Category: Int, CaseIterable, Codable {
        case breakfast = 0, lunch = 1, dinner = 2
        
        var title: String {
            switch self {
            case .breakfast: return "Breakfast"
            case .lunch: return "Lunch"
            case .dinner: return "Dinner"
            }
        }
    }

    ...

    /// A *String* type property
    var name: String?
    
    /// An *Integer* type property
    var servings: Int?
    
    /// A *Double* (or *Float*) type property
    var price: Double?
    
    /// A *Boolean* type property
    var isAvailable: Bool?
    
    /// An *Enumeration* type property
    var category: Category?
    
    /// An array of *structs*
    var ingredients: [Ingredient]
    
    /// An array of *Strings*
    var sideOptions: [String]
    
    /// A dictionary property
    var nutritionalInfo: [String: String]
    
    /// A *Date* type property
    var releaseDate: Date?
}

Before storing instances of this object in a Back4App Database, all its properties must conform the Codable and Hashable protocols.

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

The procedure for reading and updating a Recipe object is similar since they rely on the save() method. How a Recipe is instantiated determines if we are creating or updating the object on the Back4App Database.

When creating a new instance we use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var newRecipe: Recipe

// Setup newRecipe's properties
newRecipe.name = "My recipe's name"
newRecipe.servings = 4
newRecipe.price = 3.99
newRecipe.isAvailable = false
newRecipe.category = .breakfast
newRecipe.sideOptions = ["Juice"]
newRecipe.releaseDate = Date()
...

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

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

And to update an existing instance, we have to provide the objectId value which identifies the the object on the Back4App Database. A satandard update can be implemented in the following way

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let recipeToUpdate = Recipe(objectId: "OBJECT_ID")

// Update the properties you need
recipeToUpdate.name = "My updated recipe's name"
recipeToUpdate.servings = 5
recipeToUpdate.price = 5.99
recipeToUpdate.isAvailable = true
recipeToUpdate.category = .lunch
recipeToUpdate.sideOptions = ["Juice", "Coffee"]
recipeToUpdate.releaseDate = Date().addingTimeInterval(3600 * 24)
...

// Save changes synchronousty
try? recipeToUpdate.save()

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

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

1
2
3
4
5
6
7
8
9
10
11
let query = Recipe.query() // A query to fetch all Recipe items on your Back4App Database.
let query = Recipe.query("name" == "Omelette") // A query to fetch all Recipe items with name "Omelette" on your Back4App Database.
let query = Recipe.query(["name" == "Omelette", "price" = 9.99]) // A query to fetch all Recipe items with name = "Omelette" and price = 9.99.

// Fetches the items synchronously or throws an error if found.
let fetchedRecipes = 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
}

Any 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 recipeToDelete: Recipe

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

// Delete recipeToDelete asynchronously
recipeToDelete.delete { result in
    // Handle the result
}

Step 1 - Create the Recipe App Template

We start by creating a new XCode project. This this tutorial the project should look like this

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: RecipesController())
        window?.makeKeyAndVisible()

        // Additional logic
    }

    ...
}

The root view controller class (RecipesController) for the navigation controller is a subclass of UIViewController in which we will layout a form to create and update Recipe objects on the Back4App Database.

Step 2 - Setup the Recipe object

Objects you want to save on your Back4App Database have to conform the ParseObject protocol. On our Recipes app this object is Recipe. Therefore, you first need to create this object. Create a new file Recipe.swift 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
55
import Foundation
import ParseSwift

struct Recipe: ParseObject {
    /// Enumeration for the recipe category
    enum Category: Int, CaseIterable, Codable {
        case breakfast = 0, lunch = 1, dinner = 2
        
        var title: String {
            switch self {
            case .breakfast: return "Breakfast"
            case .lunch: return "Lunch"
            case .dinner: return "Dinner"
            }
        }
    }
    
    // Required properties from ParseObject protocol
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?
    
    /// A *String* type property
    var name: String?
    
    /// An *Integer* type property
    var servings: Int?
    
    /// A *Double* (or *Float*) type property
    var price: Double?
    
    /// A *Boolean* type property
    var isAvailable: Bool?
    
    /// An *Enumeration* type property
    var category: Category?
    
    /// An array of *structs*
    var ingredients: [Ingredient]
    
    /// An array of *Strings*
    var sideOptions: [String]

    /// A dictionary property
    var nutritionalInfo: [String: String]
        
    /// A *Date* type property
    var releaseDate: Date?
    
    /// Maps the nutritionalInfo property into an array of tuples
    func nutritionalInfoArray() -> [(name: String, value: String)] {
        return nutritionalInfo.map { ($0.key, $0.value) }
    }
}

where we already added all the necessary properties to Recipe according to the recipes’s features table.

The Ingredient data type is a struct holding the quantity and the description of the ingredient. As mentioned before, this data type should conform the Codable and Hashable protocols to be part of Recipe’s properties

1
2
3
4
5
6
import Foundation

struct Ingredient: Hashable, Codable {
    var quantity: Float
    var description: String
}

Additionally, the property category in Recipe has an enumeration (Category) as data type which also conforms the corresponding protocols

1
2
3
4
5
6
7
8
9
struct Recipe: ParseObject {
    /// Enumeration for the recipe category
    enum Category: Int, CaseIterable, Codable {
        case breakfast = 0, lunch = 1, dinner = 2
        
        ...
    }
    ...   
}

Step 3 - Setting up RecipesController

In RecipesController we should implement all the necessary configuration for the navigationBar and the form used to capture all the Recipe properties. This tutorial does not cover how to implement the layout for the form. We then focus on the logic related with managing data types using ParseSwift SDK. Below we highlight the key points in RecipesController which allow us to understand how we implement the connection between the user interface and the data coming from your Back4App Database

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
class RecipesController: UIViewController {
    enum PreviousNext: Int { case previous = 0, next = 1 }
    
    ...
    
    var recipes: [Recipe] = [] // 1: An array of recipes fetched from your Back4App Database
    
    // Section header labels
    private let recipeLabel: UILabel = .titleLabel(title: "Recipe overview")
    private let ingredientsLabel: UILabel = .titleLabel(title: "Ingredients")
    private let nutritionalInfoLabel: UILabel = .titleLabel(title: "Nutritional information")
    
    // 2: A custom view containing input fields to enter the recipe's information (except nutritional info. and ingredients)
    let recipeOverviewView: RecipeInfoView
    
    // 3: A  stack view containig the fields to enter the recipe's ingredients
    let ingredientsStackView: UIStackView
    
    // 4: A  stack view containig the fields to enter the nutritional information
    let nutritionalInfoStackView: UIStackView
    
    // 5: Buttons to handle the CRUD logic for the Recipe object currently displayed
    private var saveButton: UIButton = UIButton(title: "Save")
    private var updateButton: UIButton = UIButton(title: "Update")
    private var reloadButton: UIButton = UIButton(title: "Reload")
    
    var currentRecipeIndex: Int? // 6: An integer containing the index of the current recipe presenten from the recipes property
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupNavigationBar()
        setupViews()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        handleReloadRecipes()
    }
    
    private func setupNavigationBar() {
        navigationController?.navigationBar.barTintColor = .primary
        navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]
        navigationController?.navigationBar.isTranslucent = false
        navigationController?.navigationBar.barStyle = .black
        navigationItem.title = "Parse data types".uppercased()
    }
    
    private func setupViews() {
        ... // See the project example for more details

        saveButton.addTarget(self, action: #selector(handleSaveRecipe), for: .touchUpInside)
        updateButton.addTarget(self, action: #selector(handleUpdateRecipe), for: .touchUpInside)
        reloadButton.addTarget(self, action: #selector(handleReloadRecipes), for: .touchUpInside)
    }
    
    ...
}

Step 3 - Handling user input and parsing a Recipe object

In a separate file (called RecipesController+ParseSwiftLogic.swift), using an extension we now implement the methods handleSaveRecipe(), handleUpdateRecipe() and handleUpdateRecipe() to handle the input data

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
import UIKit
import ParseSwift

extension RecipesController {
    /// Retrieves all the recipes stored on your Back4App Database
    @objc func handleReloadRecipes() {
        view.endEditing(true)
        let query = Recipe.query()
        query.find { [weak self] result in // Retrieves all the recipes stored on your Back4App Database and refreshes the UI acordingly
            guard let self = self else { return }
            switch result {
            case .success(let recipes):
                self.recipes = recipes
                self.currentRecipeIndex = recipes.isEmpty ? nil : 0
                self.setupRecipeNavigation()
                
                DispatchQueue.main.async { self.presentCurrentRecipe() }
            case .failure(let error):
                DispatchQueue.main.async { self.showAlert(title: "Error", message: error.message) }
            }
        }
    }
    
    /// Called when the user wants to update the information of the currently displayed recipe
    @objc func handleUpdateRecipe() {
        view.endEditing(true)
        guard let recipe = prepareRecipeMetadata(), recipe.objectId != nil else { // Prepares the Recipe object for updating
            return showAlert(title: "Error", message: "Recipe not found.")
        }
        
        recipe.save { [weak self] result in
            switch result {
            case .success(let newRecipe):
                self?.recipes.append(newRecipe)
                self?.showAlert(title: "Success", message: "Recipe saved on your Back4App Database! (objectId: \(newRecipe.id)")
            case .failure(let error):
                self?.showAlert(title: "Error", message: "Failedto save recipe: \(error.message)")
            }
        }
    }
    
    /// Saves the currently displayed recipe on your Back4App Database
    @objc func handleSaveRecipe() {
        view.endEditing(true)
        guard var recipe = prepareRecipeMetadata() else { // Prepares the Recipe object for storing
            return showAlert(title: "Error", message: "Failed to retrieve all the recipe fields.")
        }
        
        recipe.objectId = nil // When saving a Recipe object, we ensure it will be a new instance of it.
        recipe.save { [weak self] result in
            switch result {
            case .success(let newRecipe):
                if let index = self?.currentRecipeIndex { self?.recipes[index] = newRecipe }
                self?.showAlert(title: "Success", message: "Recipe saved on your Back4App Database! (objectId: \(newRecipe.id))")
            case .failure(let error):
                self?.showAlert(title: "Error", message: "Failed to save recipe: \(error.message)")
            }
        }
    }
    
    /// When called it refreshes the UI according to the content of *recipes* and *currentRecipeIndex* properties
    private func presentCurrentRecipe() {
        ...
    }
    
    /// Adds the 'Next recipe' and 'Previous recipe' button on the navigation bar. These are used to iterate over all the recipes retreived from your Back4App Database
    private func setupRecipeNavigation() {
        ...
    }
    
    /// Reads the information the user entered via the form and returns it as a *Recipe* object
    private func prepareRecipeMetadata() -> Recipe? {
        let ingredientsCount = ingredientsStackView.arrangedSubviews.count
        let nutritionalInfoCount = nutritionalInfoStackView.arrangedSubviews.count
        
        let ingredients: [Ingredient] = (0..<ingredientsCount).compactMap { row in
            guard let textFields = ingredientsStackView.arrangedSubviews[row] as? DoubleTextField,
                  let quantityString = textFields.primaryText,
                  let quantity = Float(quantityString),
                  let description = textFields.secondaryText
            else {
                return nil
            }
            return Ingredient(quantity: quantity, description: description)
        }
        
        var nutritionalInfo: [String: String] = [:]
        
        (0..<nutritionalInfoCount).forEach { row in
            guard let textFields = nutritionalInfoStackView.arrangedSubviews[row] as? DoubleTextField,
                  let content = textFields.primaryText, !content.isEmpty,
                  let value = textFields.secondaryText, !value.isEmpty
            else {
                return
            }
            nutritionalInfo[content] = value
        }
        
        let recipeInfo = recipeOverviewView.parseInputToRecipe() // Reads all the remaining fields from the form (name, category, price, serving, etc) and returns them as a tuple

        // we collect all the information the user entered and create an instance of Recipe.
        // The recipeInfo.objectId will be nil if the currently displayed information does not correspond to a recipe already saved on your Back4App Database
        let newRecipe: Recipe = Recipe(
            objectId: recipeInfo.objectId,
            name: recipeInfo.name,
            servings: recipeInfo.servings,
            price: recipeInfo.price,
            isAvailable: recipeInfo.isAvailable,
            category: recipeInfo.category,
            ingredients: ingredients,
            sideOptions: recipeInfo.sideOptions,
            nutritionalInfo: nutritionalInfo,
            releaseDate: recipeInfo.releaseDate
        )
        
        return newRecipe
    }
    
    /// Called when the user presses the 'Previous recipe' or 'Next recipe' button
    @objc private func handleSwitchRecipe(button: UIBarButtonItem) {
        ...
    }
}

Step 4 - Run the app!

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

The first time you run the project you should see something like this in the simulator (with all the fields empty)

Now you can start entering a recipe to then save it on your Back4App Database. Once you have saved one recipe, go to your Back4App dashboard and go to your application, in the Database section you will find the class Recipe where all recipes created by the iOS App.

In Particular, it is worth noting how non-fundamental data types like Ingredient, Recipe.Category or dictionaries are stored. If you navigate through the data saved under the Recipe class, you will find that

  • The nutritionalInformation dictionary is stored as a JSON object.
  • The [Ingredients] array is stored as an array of JSON objects.
  • The enumeration Recipe.Category, since it is has an integer data type as RawValue, it is transformed to a Number value type.
  • The releaseDate property, a Date type value in Swift, is also stored as a Date type value.

To conclude, when retrieving data from your Back4App Database, you do not need to decode all these fields manually, ParseSwift SDK does decode them automatically. That means, when creating a query (Query<Recipe> in case) to retrieve data, the query.find() method will parse all the data types and JSON objects to return a Recipe array, there is no additional parsing procedure to implement.