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.
ParentItem
and
ChildItem
only need to contain
one member used to denote their name.
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.
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")
}
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:
Navigation Controller
is used to provide a back button to the parent list
when a
child list item is selected.
Bar Button Item
has been added to both Table Views
allowing
a
user
to add items.
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.
UITableViewController
,
it is necessary to state that our view controller class conforms to this protocol.
Results
type is declared. This object will hold any items
returned by a query of our databse of type ParentItem
.
load()
is called to populate the Table View with any existing list items.
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.
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.
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]
}
}
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()
}
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()
}
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()
}
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()
}
newItemInput
will be used to store what a user types into a
text
field.
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.
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.
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)
}
//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)
}
}
}
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).
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.
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()
}
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.