I have been using Realm for a while now and I am very happy with it! However, I have stumbled upon some questions during my implementation.
I have made a test scenario to try to point out where I need some input.
I have a realm with a database of Person objects. These are all presented in a UITableView. I want to keep a specific order of the objects, and the user should be able to reorder the objects. From what I've read I have to use Realms 'List' to achieve this. This again means I have one class called Person and one class that's called PersonList. The PersonList only has one property: - list.
The app should only have one object of the PersonList in its Realm, but may have several objects of Person.
My questions:
What is the best practice to only have one instance of PersonList in my Realm? As you can see in my example below I first check if one exists, if not I create it.
What is the best practice when it comes to the use of Realm Notifications. Is it correct to add it to the list property of the one PersonList object in my Realm?
Let's say I want to have a seperate class that handles the write transactions in my Realm. As you can see in my example all the read/write transactions are kept in the UITableViewController class - is this considered messy?
My example below should be able to run fine using Xcode 8, Swift 3 and Realm 1.1.0.
I appreciate any feedback and thoughts!
Regards, Erik
import UIKit
import RealmSwift
class PersonList : Object {
var list = List<Person>()
}
class Person : Object {
dynamic var favorite = false
dynamic var username : String?
dynamic var firstName : String?
dynamic var lastName : String?
var fullName : String? {
get {
guard let firstName = firstName, let lastName = lastName else {
return nil
}
return "\(firstName) \(lastName)"
}
}
}
class ViewController: UITableViewController {
var results : List<Person>?
var notificationToken: NotificationToken? = nil
func addPerson() {
let alert = UIAlertController(title: "Add Person", message: "Please fill in the information", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { alertAction in
if let firstNameTextField = alert.textFields?[0], let lastNameTextField = alert.textFields?[1] {
self.savePerson(firstName: firstNameTextField.text, lastName: lastNameTextField.text)
}
}))
alert.addTextField { (textField : UITextField!) -> Void in
textField.placeholder = "First Name"
}
alert.addTextField { (textField : UITextField!) -> Void in
textField.placeholder = "Last Name"
}
self.present(alert, animated: true, completion: nil)
}
func savePerson(firstName: String?, lastName: String?) {
guard let firstName = firstName, !firstName.isEmpty else {
let alert = UIAlertController(title: "Oops!", message: "First name missing!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
return
}
guard let lastName = lastName, !lastName.isEmpty else {
let alert = UIAlertController(title: "Oops!", message: "Last name missing!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
return
}
let realm = try! Realm()
let newPerson = Person()
newPerson.firstName = firstName
newPerson.lastName = lastName
newPerson.username = "\(Date())"
do {
try realm.write {
results?.append(newPerson)
}
}
catch let error {
print("Error: \(error)")
}
}
func editButtonAction(_ sender: UIBarButtonItem) {
if tableView.isEditing {
tableView.setEditing(false, animated: true)
sender.title = "Edit"
}
else {
tableView.setEditing(true, animated: true)
sender.title = "Done"
}
}
override func viewDidLoad() {
super.viewDidLoad()
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addPerson))
let editButton = UIBarButtonItem(title: "Edit", style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.editButtonAction(_:)))
self.navigationItem.rightBarButtonItems = [addButton, editButton]
tableView.allowsSelectionDuringEditing = true
let realm = try! Realm()
//First, make sure a list exists in realm
if realm.objects(PersonList.self).first?.list == nil {
print("No existing list found in realm. Creating one.")
let defaultList = PersonList()
do {
try realm.write {
realm.add(defaultList)
}
}
catch let error { print("Error creating person list: \(error)") }
}
results = realm.objects(PersonList.self).first?.list
// Observe Results Notifications
notificationToken = results?.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
break
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic)
tableView.endUpdates()
break
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
print("Error: \(error)")
break
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return results?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = "PersonTestCell"
var cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier)
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: reuseIdentifier)
}
if let results = self.results {
let person = results[indexPath.row]
cell!.textLabel?.text = person.fullName ?? "Name not found."
cell!.detailTextLabel?.text = person.username ?? "Username not found."
}
return cell!
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
// Override to support conditional editing of the table view.
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let results = self.results {
//Delete Person
let realm = try! Realm()
do {
try realm.write {
results.remove(objectAtIndex: indexPath.row)
}
}
catch let error {
print("Error: \(error)")
}
}
}
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
return UITableViewCellEditingStyle.delete
}
// Override to support rearranging the table view.
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to toIndexPath: IndexPath) {
let realm = try! Realm()
do {
try realm.write {
results?.move(from: toIndexPath.row, to: fromIndexPath.row)
results?.move(from: fromIndexPath.row, to: toIndexPath.row)
}
}
catch let error {
print("Error: \(error)")
}
}
// Override to support conditional rearranging of the table view.
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the item to be re-orderable.
return true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
deinit {
notificationToken?.stop()
}
}
Thanks for using Realm! As for your questions:
What is the best practice to only have one instance of PersonList in my Realm? As you can see in my example below I first check if one exists, if not I create it.
There are a couple of ways you can handle this situation. What I recommend is that you give PersonList
a primary key, and you use a constant value for that primary key whenever you work with PersonList
. Realm enforces the invariant that only one object with a given primary key value can be stored.
As such:
Realm.object(ofType:forPrimaryKey:)
with your constant primary key to get an existing PersonList
.nil
, create a new PersonList
.PersonList
, use Realm.add(_:update:)
, with update
set to true
. This will add the object if it doesn't exist, or update the existing copy in the database if it was previously added.What is the best practice when it comes to the use of Realm Notifications. Is it correct to add it to the list property of the one PersonList object in my Realm?
Yes, your use of notifications seems appropriate to me.
Let's say I want to have a seperate class that handles the write transactions in my Realm. As you can see in my example all the read/write transactions are kept in the UITableViewController class - is this considered messy?
This is more of a coding style question than a Realm question, but it's ultimately a matter of personal preference. If you want to avoid creating a "massive view controller" with all of your logic there are a couple of things you can try:
Split your view controller class into a main class and a number of extensions, each living in its own file. For example, you might have an extension for Realm-related methods, one for table view delegate/data source methods, and such. Note that stored properties can't live in an extension, and have to be declared in the primary class declaration.
You could create one or more helper classes to organize your logic. For example, you have a couple of methods that present modal popups and write to the Realm. Those don't necessarily have to live in the table view class, and could live in a PersonManager
class. This class would be responsible for creating and presenting the alert controllers and for interacting with the Realm. You could then use closure-based callbacks or the delegate pattern if your PersonManager
needed to communicate back to the table view controller (although, with Realm notifications automatically handling refreshing your table view, that might not even be necessary!).
Hope that helps.