iOS Data Persistence

I originally created List Maker as an Android application in order to better understand the Android Room library, which is used to persist data in a SQL database. The application is straightforward; users are able to create a parent list of items wherein each item is capable of navigating to a child list of items associated with the parent item. For example, the main list displayed in the app may contain an item named Groceries, which when tapped on, will navigate to a child list containing items such as Milk, Eggs, Spinach.

When researching how to persist data on iOS devices, I initially learned about Core Data. While Core Data initially seemed like a perfect solution for saving user created data, I ultimately decided to use Realm by MongoDB instead. The main reason for my decision was that the documentation existing for Realm was far superior to the documentation for Core Data.

The first step was to define a data type that not only supported the application logic but could also be understood by the Realm library.



  • As far as the application logic is concerned, both ParentItem and ChildItem only need to contain one member used to denote their name.
  • Additionally, since a given ParentItem may be associated with many ChildItems (a one-to-many) relationship, ParentItem must also contain a List of ChildItems. Here the type List is actually defined in the Realm library, and as such, it is necessary to specify the type of elements it will contain.
  • Likewise, a given ChildItem must keep track of which ParentItem it belongs to. This is accomplished by specifying a Linking Object that points to the List contained in an instance of a ParentItem.
class ParentItem: Object {
    @objc dynamic var name: String = ""
    let childItems = List()
}

class ChildItem: Object {
    @objc dynamic var name: String = ""
    var parentItem = LinkingObjects(fromType: ParentItem.self, property: "childItems")
}
                    

User interface

Initially, I had intended to create the user interface using SwiftUI, but I decided to use Storyboard layout instead because currently (iOS 15.6) the SwiftUI implementation of UIAlertController doesn't support adding TextFields to the alert. This is possible and easy to do from within a ViewController class. TextFields contained within an alert will be an essential component of how the user interacts with the application by allowing them to add and edit list items.

The Storyboard layout for List Maker is shown below:

Storyboard layour for List Maker iOS


  • A Navigation Controller is used to provide a back button to the parent list when a child list item is selected.
  • A Bar Button Item has been added to both Table Views allowing a user to add items.
  • A segue has been defined between the parent list and child list viewcontrollers.

Application Logic: ParentItemViewController

When using Storyboards, the viewcontroller class(es) are where the real magic happens. A View Controller defines how an iOS application should behave when a user interacts with it. This is accomplished by defining IBOutlets, which designate how user interface (UI) elements should change under specific conditions and IBActions, which specify program logic to execute if a UI element is interacted with. IBOutlets can be thought of as communicating application state to the user, and IBActions can be thought of as transmitting user actions to the application.



  • In order to use the built-in functionality provided by a UITableViewController, it is necessary to state that our view controller class conforms to this protocol.
  • An instance of our Realm database is created.
  • An object of Results type is declared. This object will hold any items returned by a query of our databse of type ParentItem.
  • When the view controlled by this ViewController class loads, a method (defined below) load() is called to populate the Table View with any existing list items.
  • Also in viewDidLoad(), support for allowing a user to long press a given list item is added. This long press gesture will be used to edit or delete list items.
  • The tableView (cellForRowAt) method defines how a given list element will be created. When creating the storyboard for this app, each Table View contained a prototype cell, which in the case of the parent list view was given the identifier ParentItemCell. This method specifies that the Table View used in this ViewController class should use the specified prototype cells as a template for its list items. In this case, the cell contains only a Text Label, which here is programmatically set to display either the name attribute of the ParentItem or else a message that no items have been added yet.
  • The tableView (didSelectRowAt) method defines what will happen when a user taps on a given item, or in terms of the Table View, a given cell. In this case, we will navigate to the Table View designated for displaying ChildItems. We are able to pass the essential information of which ParentItem was tapped by using the prepare (for segue) method. The ParentItem name will then be set in the ChildItemViewController class where it will be used to query the Realm database for all ChildItems associated with the tapped ParentItem.
class ParentItemViewController: UITableViewController {

    let realm = try! Realm()
    
    var parentItems: Results?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        load()
        
        // support long press
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress))
        tableView.addGestureRecognizer(longPress)
    }
    
    //MARK: - Tableview Datascource Methods
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return parentItems?.count ?? 1
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ParentItemCell", for: indexPath)
        cell.textLabel?.text = parentItems?[indexPath.row].name ?? "TODO: Add your first item!"
        
        return cell
    }
    
    //MARK: - TableView Delegate Methods
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        // navigate to items
        performSegue(withIdentifier: "ParentToChild", sender: self)
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let destinationVC = segue.destination as! ChildItemViewController
        if let indexPath = tableView.indexPathForSelectedRow {
            destinationVC.parentItem = parentItems?[indexPath.row]
        }
    }
                


  • The load() method will query the Realm database for all ParentItem objects and store the results in the variable parentItems.

//MARK: - realm Database Methods

func load() {
    parentItems = realm.objects(ParentItem.self)
    tableView.reloadData()
}
                    


  • The save() method is used to write or commit an object to the database. In this app save() will be used to create new items. Because Realm's write() method can throw errors, it is necessary to wrap this statement within a do try catch block.
func save(item: ParentItem) {
    do {
        try realm.write {
            realm.add(item)
        }
    } catch {
        print(error)
    }
    self.tableView.reloadData()
}
                    


  • The delete() method is used to delete a long tapped item. Similar to above, this method can throw errors and must be handled accordingly.
func delete(indexPath: IndexPath) {
    if let item = parentItems?[indexPath.row] {
        do {
            try realm.write {
                realm.delete(item)
            }
        } catch {
            print(error)
        }
    }
    load()
}
                


                
            


  • The update() method is used to update the name attribute of a long tapped item.
func update(indexPath: IndexPath, newName: String) {
    if let item = parentItems?[indexPath.row] {
        do {
            try realm.write {
                item.name = newName
            }
        } catch {
            print(error)
        }
    }
    load()
}
                


  • The variable newItemInput will be used to store what a user types into a text field.
  • The add() function creates a new ParentItem and sets its name property to the value of the string entered into the text field. Afterwards, a call to the save() method described above is used to commit the new item to the Realm database.
  • In iOS, alerts are created as an object of type UIAlertController and then available user responses, termed actions, and of type UIAlertAction are subsequently added to alerts.


    • nilItemAlert describes the alert message that will be shown to a user if they do not enter any text into the presented text field and attempt to add such an unnamed item.
    • Affirmative and cancelling actions must be explicitly defined. An affirmative action will add the nameless item to the database anyways; a cancelling action will dismiss the alert thereby aborting the process of adding the item.
    • Similarly, an alert is created for adding a new object, and this alert will have a text field attached to it allowing the user to name their new item.
    • Lastly, an alert must be presented to the user by calling the present() method.
//MARK: - Add New Items

@IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
    var newItemInput = UITextField()
    
    func add() {
        let newItem = ParentItem()
        newItem.name = newItemInput.text!
        save(item: newItem)
    }
    
    
    // In cthe event a new item is an empty string
    let nilItemAlert = UIAlertController(title: "Empty Item", message: "Are you sure you want to add an item with no text? ", preferredStyle: .alert)
    let nilItemActionAffirm = UIAlertAction(title: "Yes", style: .default) { action in
        add()
    }
    let nilItemActionCancel = UIAlertAction(title: "No", style: .cancel, handler: nil)
    nilItemAlert.addAction(nilItemActionAffirm)
    nilItemAlert.addAction(nilItemActionCancel)
    
    let addItemAlert = UIAlertController(title: "Add New Item", message: "", preferredStyle: .alert)
    let addItemAction = UIAlertAction(title: "Add", style: .default) { action in
        if newItemInput.text != "" {
            add()
        } else {
            self.present(nilItemAlert, animated: true, completion: nil)
        }
    }
    let cancelAddItem = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
    
    addItemAlert.addTextField { (alertTextField) in
        alertTextField.placeholder = "New item..."
        newItemInput = alertTextField
    }
    addItemAlert.addAction(addItemAction)
    addItemAlert.addAction(cancelAddItem)
    present(addItemAlert, animated: true, completion: nil)
}
                    


  • When a user long presses on a cell, they will be presented an alert allowing them to either edit the name of a given item, delete the item, or cancel the alert.
Edit option selected after long press
//MARK: - Long Press Support Methods
@objc func longPress(sender: UILongPressGestureRecognizer) {
    var newItemInput = UITextField()
    
    if sender.state == UIGestureRecognizer.State.began {
        let touchPoint = sender.location(in: tableView)
        if let indexPath = tableView.indexPathForRow(at: touchPoint) {
            let editItemAlert = UIAlertController(title: "Edit Item", message: "Enter the new name for this item:", preferredStyle: .alert)
            editItemAlert.addTextField { (alertTextField) in
                alertTextField.placeholder = "New name..."
                newItemInput = alertTextField
            }
            let removeItemAlert = UIAlertController(title: "Delete Category", message: "Are you sure you want to delete this category?", preferredStyle: .alert)
            let removeItemAction = UIAlertAction(title: "Delete", style: .destructive) { action in
                self.delete(indexPath: indexPath)
            }
            let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
            let editAction = UIAlertAction(title: "Edit", style: .default) { action in
                self.present(editItemAlert, animated: true) {  }
            }
            let confirmEditAction = UIAlertAction(title: "Save", style: .default) { action in
                self.update(indexPath: indexPath, newName: newItemInput.text!)
            }
            removeItemAlert.addAction(removeItemAction)
            removeItemAlert.addAction(cancelAction)
            removeItemAlert.addAction(editAction)
            editItemAlert.addAction(cancelAction)
            editItemAlert.addAction(confirmEditAction)
            present(removeItemAlert, animated: true, completion: nil)
        }
    }
}
                    

Application Logic: ChildItemViewController

The Table View responsible for displaying a list of ChildItems contains duplicate logic for almost all of its operations, and this logic won't be re-explained in order to avoid redundancy. The following differences are noteworthy:



  • ChildItemViewController contains a variable, parentItem that holds a reference to the ParentItem that was tapped in the previous Table View. Swift allows for variables to contain a didSet property that can execute code once the variable's value changes. (See Property Observers in Swift).
  • In this case, when the variable parentItem is assigned a value via the prepare (for segue) method in ParentItemViewController, the ChildItemViewController class will call the load() method in order to retreive all relevant items.
  • In this class, the load() method sets a variable of type Results to contain all ChildItems associated with the tapped ParentItem.
class ChildItemViewController: UITableViewController {
        
    var parentItem: ParentItem? {
        didSet{
            load()
        }
    }

    func load() {
        items = parentItem!.childItems.sorted(byKeyPath: "name", ascending: true)
        tableView.reloadData()
    }
                


  • When a ChildItem is added in this class, it is appended to the appropriate ParentItem member childItems.
//MARK: - Add new items

@IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
    var newItemInput = UITextField()
    
    func add() {
        
        if let parentItem = parentItem {
            do {
                try self.realm.write{
                    let newItem = ChildItem()
                    newItem.name = newItemInput.text!
                    parentItem.childItems.append(newItem)
                }
            } catch {
                print(error)
            }
        }
    }
                

That's it! This app is very straightforward. One of my earliest Android apps contained the same functionality, and I was curious to implement this in iOS. The Realm database by MongoDB provides a developer-friendly way to persist data, and it has excellent documentation. Core Data is another tried and true option for data persistence on iOS.

View the project source code on GitHub

Download on the App Store

Top Of Page