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.
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
[Ingredients]
The set of ingredients enclosed in a customIngredientstruct
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).
Swift
1 import Foundation
2 import ParseSwift
34 struct Recipe: ParseObject {5/// Enumeration for the recipe category6 enum Category: Int, CaseIterable, Codable {7 case breakfast =0, lunch =1, dinner =289 var title: String {10 switch self {11 case .breakfast:return"Breakfast"12 case .lunch:return"Lunch"13 case .dinner:return"Dinner"14}15}16}1718...1920/// A *String* type property21 var name: String?2223/// An *Integer* type property24 var servings: Int?2526/// A *Double* (or *Float*) type property27 var price: Double?2829/// A *Boolean* type property30 var isAvailable: Bool?3132/// An *Enumeration* type property33 var category: Category?3435/// An array of *structs*36 var ingredients:[Ingredient]3738/// An array of *Strings*39 var sideOptions:[String]4041/// A dictionary property42 var nutritionalInfo:[String: String]4344/// A *Date* type property45 var releaseDate: Date?46}
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:
Create
Update
Read
Delete
//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 use1 var newRecipe: Recipe
23// Setup newRecipe's properties4 newRecipe.name ="My recipe's name"5 newRecipe.servings =46 newRecipe.price =3.997 newRecipe.isAvailable =false8 newRecipe.category =.breakfast
9 newRecipe.sideOptions =["Juice"]10 newRecipe.releaseDate =Date()11...1213// Saves newRecipe on your Back4App Database synchronously and returns the new saved Item. It throws and error if something went wrong.14 let savedRecipe =try? savedRecipe.save()1516// Saves savedRecipe on your Back4App Database asynchronously, and passes a Result<ToDoListItem, ParseError> object to the completion block to handle the save proccess.17 savedRecipe.save { result in18// Handle the result to check the save was successfull or not19}
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
Swift
1 class SceneDelegate: UIResponder, UIWindowSceneDelegate {23 var window: UIWindow?45 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions){6 guard let scene =(scene as? UIWindowScene)else{return}78 window =.init(windowScene: scene)9 window?.rootViewController =UINavigationController(rootViewController:RecipesController())10 window?.makeKeyAndVisible()1112// Additional logic13}1415...16}
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.
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
Swift
1 import Foundation
2 import ParseSwift
34 struct Recipe: ParseObject {5/// Enumeration for the recipe category6 enum Category: Int, CaseIterable, Codable {7 case breakfast =0, lunch =1, dinner =289 var title: String {10 switch self {11 case .breakfast:return"Breakfast"12 case .lunch:return"Lunch"13 case .dinner:return"Dinner"14}15}16}1718// Required properties from ParseObject protocol19 var objectId: String?20 var createdAt: Date?21 var updatedAt: Date?22 var ACL: ParseACL?2324/// A *String* type property25 var name: String?2627/// An *Integer* type property28 var servings: Int?2930/// A *Double* (or *Float*) type property31 var price: Double?3233/// A *Boolean* type property34 var isAvailable: Bool?3536/// An *Enumeration* type property37 var category: Category?3839/// An array of *structs*40 var ingredients:[Ingredient]4142/// An array of *Strings*43 var sideOptions:[String]4445/// A dictionary property46 var nutritionalInfo:[String: String]4748/// A *Date* type property49 var releaseDate: Date?5051/// Maps the nutritionalInfo property into an array of tuples52 func nutritionalInfoArray()->[(name: String, value: String)]{53return nutritionalInfo.map {($0.key, $0.value)}54}55}
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
Swift
1 import Foundation
23 struct Ingredient: Hashable, Codable {4 var quantity: Float
5 var description: String
6}
Additionally, the property category in Recipe has an enumeration (Category) as data type which also conforms the corresponding protocols
Swift
1 struct Recipe: ParseObject {2/// Enumeration for the recipe category3 enum Category: Int, CaseIterable, Codable {4 case breakfast =0, lunch =1, dinner =256...7}8...9}
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
Swift
1 class RecipesController: UIViewController {2 enum PreviousNext: Int { case previous =0, next =1}34...56 var recipes:[Recipe]=[]// 1: An array of recipes fetched from your Back4App Database78// Section header labels9 private let recipeLabel: UILabel =.titleLabel(title:"Recipe overview")10 private let ingredientsLabel: UILabel =.titleLabel(title:"Ingredients")11 private let nutritionalInfoLabel: UILabel =.titleLabel(title:"Nutritional information")1213// 2: A custom view containing input fields to enter the recipe's information (except nutritional info. and ingredients)14 let recipeOverviewView: RecipeInfoView
1516// 3: A stack view containig the fields to enter the recipe's ingredients17 let ingredientsStackView: UIStackView
1819// 4: A stack view containig the fields to enter the nutritional information20 let nutritionalInfoStackView: UIStackView
2122// 5: Buttons to handle the CRUD logic for the Recipe object currently displayed23 private var saveButton: UIButton =UIButton(title:"Save")24 private var updateButton: UIButton =UIButton(title:"Update")25 private var reloadButton: UIButton =UIButton(title:"Reload")2627 var currentRecipeIndex: Int?// 6: An integer containing the index of the current recipe presenten from the recipes property2829 override func viewDidLoad(){30 super.viewDidLoad()31setupNavigationBar()32setupViews()33}3435 override func viewDidAppear(_ animated: Bool){36 super.viewDidAppear(animated)37handleReloadRecipes()38}3940 private func setupNavigationBar(){41 navigationController?.navigationBar.barTintColor =.primary
42 navigationController?.navigationBar.titleTextAttributes =[.foregroundColor: UIColor.white]43 navigationController?.navigationBar.isTranslucent =false44 navigationController?.navigationBar.barStyle =.black
45 navigationItem.title ="Parse data types".uppercased()46}4748 private func setupViews(){49...// See the project example for more details5051 saveButton.addTarget(self, action: #selector(handleSaveRecipe),for:.touchUpInside)52 updateButton.addTarget(self, action: #selector(handleUpdateRecipe),for:.touchUpInside)53 reloadButton.addTarget(self, action: #selector(handleReloadRecipes),for:.touchUpInside)54}5556...57}
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
Swift
1 import UIKit
2 import ParseSwift
34 extension RecipesController {5/// Retrieves all the recipes stored on your Back4App Database6 @objc func handleReloadRecipes(){7 view.endEditing(true)8 let query = Recipe.query()9 query.find {[weak self] result in// Retrieves all the recipes stored on your Back4App Database and refreshes the UI acordingly10 guard let self = self else{return}11 switch result {12 case .success(let recipes):13 self.recipes = recipes
14 self.currentRecipeIndex = recipes.isEmpty ? nil :015 self.setupRecipeNavigation()1617 DispatchQueue.main.async { self.presentCurrentRecipe()}18 case .failure(let error):19 DispatchQueue.main.async { self.showAlert(title:"Error", message: error.message)}20}21}22}2324/// Called when the user wants to update the information of the currently displayed recipe25 @objc func handleUpdateRecipe(){26 view.endEditing(true)27 guard let recipe =prepareRecipeMetadata(), recipe.objectId != nil else{// Prepares the Recipe object for updating28returnshowAlert(title:"Error", message:"Recipe not found.")29}3031 recipe.save {[weak self] result in32 switch result {33 case .success(let newRecipe):34 self?.recipes.append(newRecipe)35 self?.showAlert(title:"Success", message:"Recipe saved on your Back4App Database! (objectId: \(newRecipe.id)")36 case .failure(let error):37 self?.showAlert(title:"Error", message:"Failedto save recipe: \(error.message)")38}39}40}4142/// Saves the currently displayed recipe on your Back4App Database43 @objc func handleSaveRecipe(){44 view.endEditing(true)45 guard var recipe =prepareRecipeMetadata()else{// Prepares the Recipe object for storing46returnshowAlert(title:"Error", message:"Failed to retrieve all the recipe fields.")47}4849 recipe.objectId = nil // When saving a Recipe object, we ensure it will be a new instance of it.50 recipe.save {[weak self] result in51 switch result {52 case .success(let newRecipe):53if let index = self?.currentRecipeIndex { self?.recipes[index]= newRecipe }54 self?.showAlert(title:"Success", message:"Recipe saved on your Back4App Database! (objectId: \(newRecipe.id))")55 case .failure(let error):56 self?.showAlert(title:"Error", message:"Failed to save recipe: \(error.message)")57}58}59}6061/// When called it refreshes the UI according to the content of *recipes* and *currentRecipeIndex* properties62 private func presentCurrentRecipe(){63...64}6566/// 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 Database67 private func setupRecipeNavigation(){68...69}7071/// Reads the information the user entered via the form and returns it as a *Recipe* object72 private func prepareRecipeMetadata()-> Recipe?{73 let ingredientsCount = ingredientsStackView.arrangedSubviews.count
74 let nutritionalInfoCount = nutritionalInfoStackView.arrangedSubviews.count
7576 let ingredients:[Ingredient]=(0..<ingredientsCount).compactMap { row in77 guard let textFields = ingredientsStackView.arrangedSubviews[row] as? DoubleTextField,78 let quantityString = textFields.primaryText,79 let quantity =Float(quantityString),80 let description = textFields.secondaryText
81else{82return nil
83}84returnIngredient(quantity: quantity, description: description)85}8687 var nutritionalInfo:[String: String]=[:]8889(0..<nutritionalInfoCount).forEach { row in90 guard let textFields = nutritionalInfoStackView.arrangedSubviews[row] as? DoubleTextField,91 let content = textFields.primaryText,!content.isEmpty,92 let value = textFields.secondaryText,!value.isEmpty
93else{94return95}96 nutritionalInfo[content]= value
97}9899 let recipeInfo = recipeOverviewView.parseInputToRecipe()// Reads all the remaining fields from the form (name, category, price, serving, etc) and returns them as a tuple100101// we collect all the information the user entered and create an instance of Recipe.102// The recipeInfo.objectId will be nil if the currently displayed information does not correspond to a recipe already saved on your Back4App Database103 let newRecipe: Recipe =Recipe(104 objectId: recipeInfo.objectId,105 name: recipeInfo.name,106 servings: recipeInfo.servings,107 price: recipeInfo.price,108 isAvailable: recipeInfo.isAvailable,109 category: recipeInfo.category,110 ingredients: ingredients,111 sideOptions: recipeInfo.sideOptions,112 nutritionalInfo: nutritionalInfo,113 releaseDate: recipeInfo.releaseDate
114)115116return newRecipe
117}118119/// Called when the user presses the 'Previous recipe' or 'Next recipe' button120 @objc private func handleSwitchRecipe(button: UIBarButtonItem){121...122}123}
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.