Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4404

Introduction to Realm

$
0
0
Learn how to create a map-based app with Realm as the data storage engine!

Learn how to create a map-based app with Realm as the data storage engine!

Note from Ray: This is a brand new Swift tutorial released as part of the iOS 8 Feast. Enjoy!

Realm is a cross-platform mobile database just released to the public in July of 2014. It’s a data persistence solution designed specifically for mobile applications.

Realm is extremely simple to integrate in your projects, and most common functions – such as querying the database – consist of a single line of code!

Unlike wrappers around Core Data such as MagicalRecord, Realm does not rely on Core Data or even a SQLite backend.

The Realm developers claim that their proprietary data storage solution is even faster than SQLite and Core Data. Here’s some example Core Data code to fetch a set of records with a predicate and then sort the results:

let fetchRequest = NSFetchRequest(entityName: "Specimen")
let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString)
fetchRequest.predicate = predicate
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
let error = NSError()
let results = managedObjectContext?.executeFetchRequest(fetchRequest, error:&error)

What takes seven lines with Core Data can be achieved with only two lines in Realm:

let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString);
let specimens = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("name", ascending: true)

Working with Realm results in more concise code — which makes it easier to write and read your code.

This tutorial will introduce you to the basic features of Realm on iOS; you’ll learn how to link in the Realm framework, create models, perform queries, and update and delete records.

Getting Started

Here’s the scenario: you’ve accepted a position as an intern in the National Park Service and your job is to document the species found in the biggest national parks in the United States. You need an assistant to keep notes and document our findings, but the agency doesn’t have an assistant to spare, nor the budget to hire a new one. Instead, you’ll create a virtual assistant for yourself — an app named “Partner” — that you can carry around in your pocket!

Download the starter project for this tutorial here.

Open the starter project in Xcode. MapKit is already set up in your project, along with some basic functionality to create, update and delete new specimens in your app. Right now your app only contains instances of UITableView and MKMapView to provide the map functionality.

Note: If you’re interested in learning more about MapKit, check out our Introduction to MapKit tutorial, which provides an in-depth look at how to work with MapKit.

The starter project won’t build yet since it’s missing Realm. Download the the latest Realm distribution from this URL: http://static.realm.io/downloads/cocoa/latest.

Unzip the Realm archive. Inside the folder you’ll see another folder named iOS. Open this folder, and drag the Realm.framework file into the Frameworks folder of your Xcode project to keep everything organized:

realm-framework

Be sure to check the Copy items if needed option and click Finish to add the framework to your project.

Back to the Realm archive folder, open the folder named Swift. There should be a file named RLMSupport.swift here containing some Swift conventions for Realm classes, such as a Generator on RLMArray that lets you use Realm arrays like native arrays.

Drag this file into the RWRealmStarterProject folder of the Xcode project. Again, make sure you select Copy Items if Needed.

Thats it! Build and run the project to ensure everything compiles. If not, re-check the steps above carefully. You should see a basic screen like so:

realm-first

Introducing Realm Browser

There’s a nice utility included in the Realm distribution package that you’ll want to install to make your life a little easier.

The Realm Browser lets you read and edit Realm databases. It’s really useful while developing as the Realm database format is proprietary and not easily human-readable.

realm-browser

You’ll find the Realm Browser app in the browser folder of the Realm archive folder. Alternatively, you can access the Realm GitHub repository and build the project in the tools/RealmBrowser directory.

Concepts and Major Classes

In order to better understand what Realm does, here’s an overview of the Realm classes and conceptl you’ll use in this tutorial:

RLMRealm: RLMRealm is the heart of the framework; it’s your access point to the underlying database, similar to a Core Data managed object context. For your coding convenience, there’s a singleton defaultRealm which you’ll use in this tutorial. There’s also an in-memory realm instance that you can use when you don’t need to persist the results to disk, and you can create other realms as needed to support concurrent operations.

RLMObject: This is your realm model. The act of creating a model defines the schema of the database; to create a model you simply subclass RLMObject and define the fields you want to persist as properties.

Relationships: You create one-to-many relationships between objects by simply declaring a property of the type of the RLMObject you want to refer to. You can create many-to-one and many-to-many relationships via a property of type RLMArray, which leads you to…

RLMArray: This class has an API similar to that of NSArray; you can use it to store multiple RLMObject instances and define relationships. Its has other hidden powers, too; it can sort RLMObject instances, query the database, and can perform aggregate queries.

Write Transactions: Any operations in the database such as creating, editing, or deleting objects must be performed within transactions which are delineated by the beginWriteTransaction() and commitWriteTransaction() operations.

Queries: To retrieve objects from the database you’ll need to use queries. The simplest form of a query is calling allObjects() on an RLMObject. If your data retrieval needs are more complex you can make use of predicates, chain your queries, and order your results as well.

Now that you’ve had an introduction to Realm, it’s time to get your feet wet and build the rest of the project for this tutorial.

Creating Your First Model

Finally, it’s time to create your first Realm model!

Right-click the Models group in the Xcode project navigator and select New File…. Select iOS\Source\Swift File and click Next. Name the file Specimen and ensure that you select the RWRealmStarterProject target and click Create.

Open Specimen.swift and add replace the file’s contents with the following:

import UIKit
import Realm
 
class Specimen: RLMObject {
  dynamic var name = ""
  dynamic var specimenDescription = ""
  dynamic var latitude: Double = 0.0
  dynamic var longitude: Double = 0.0
  dynamic var created = NSDate()
}

The code above adds a few properties: name and specimenDescription store the specimen name and the specimen description respectively. Specific datatypes in Realm, such as strings, must be initialized with a value. In this case you initialize them with an empty string.

latitude and longitude store the coordinates for the specimen. Here you set the type to Double and initialize them with 0.0.

Finally, created stores the creation date of the Specimen. NSDate() returns the current date, so you can initialize the property with that value.

Now that you’ve created your first model in Realm, how about using what you’ve learned in a small challenge?

Specimens will be separated into different Categories. The challenge is to create a category model by yourself; name the file Category.swift and give your new model a single String property name.

If you want to check your work, the solution is below:

Solution Inside: Solution SelectShow>

You now have a Category model which you need to relate to the Specimen model somehow.

Recall the note above that stated you could create relationships between models by simply declaring a property with the appropriate model to be linked.

Open Specimen.swift and add the following declaration below the line where you define the created property:

dynamic var category = Category()

This sets up a one-to-many relationship between the Specimen and the Category models. This means each specimen can belong to only one Category, but each Category may have many specimens.

You have your basic data models in place — it’s time to add some records to your database!

Adding Records

When the user adds a new specimen, they’ll have a chance to enter the specimen name and select a category. Open CategoriesTableViewController.swift. This view controller will present the list of categories in a table view so the user can select one.

You’ll need to populate this table view with some default categories. You can store these Category instances in an instance of RLMArray. This is an array that stores RLMObjects — but has some cool functionality you will use a little bit later! :]

CategoriesTableViewController has a categories array as a placeholder for now. Find the following code at the top of the class definition:

var categories = []

…and replace it with the following line:

var categories = RLMArray(objectClassName: Category.className())

When you create an RLMArray you need to define what models it will store. In the code above you simply create the RLMArray with the class name in the initializer RLMArray(objectClassName: Category.className()); this indicates that this instance of RLMArray will store Category models.

You’ll want to give your user some default categories to choose from the first time the app runs.

Add the following helper method to the class definition:

func populateDefaultCategories() {
  categories = Category.allObjects() //1
 
  if categories.count == 0 { //2
    let realm = RLMRealm.defaultRealm() //3
    realm.beginWriteTransaction() //4
 
    //5
    let defaultCategories = ["Birds", "Mammals", "Flora", "Reptiles", "Arachnids" ]
    for category in defaultCategories {
      //6
      let newCategory = Category()
      newCategory.name = category
      realm.addObject(newCategory)
    }
 
    realm.commitWriteTransaction() // 7
    categories = Category.allObjects() //8
  }
}

Taking each numbered line in turn:

  1. Categories.allObjects() returns all rows for the specified object; in this case, you’re asking for all Category objects in the database.
  2. If count here is equal to 0 this means the database has no category records, which is the case the first time you run the app.
  3. You access the default realm singleton and store it in realm for easy access.
  4. This starts a transaction on the default realm — you’re now ready to add some records to the database.
  5. Here you create the list of default category names and then iterate through them.
  6. For each category name, you create a new instance of Category, populate name and add the object to the realm.
  7. When you’ve added all the categories, call commitWriteTransaction() to close the transaction and commit the records to the database.
  8. Finally, you fetch all of the categories you just created and store them in categories.

Operations that you perform inside a transaction will only run when you call commitWriteTransaction(). You can perform a simple set of create operations like you did in the code above, or you can perform multiple complex operations together like creating, updating, or deleting multiple objects at the same time.

Add the following line to the end of viewDidLoad():

populateDefaultCategories()

This calls the helper method to populate your test categories when the view loads.

Now that you have some data, you’ll need to update the table view data source methods to show the categories. Find tableView(_:cellForRowAtIndexPath:) and replace the method with the following implementation:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("CategoryCell", forIndexPath: indexPath) as UITableViewCell
 
  let category = categories.objectAtIndex(UInt(indexPath.row)) as Category
  cell.textLabel?.text = category.name
 
  return cell
}

This implementation retrieves the category object from the categories RLMArray and then sets the cell’s text label to show the category name.

Next, add the following line to CategoriesTableViewController class definition with the other properties:

var selectedCategory: Category!

You’ll use this property to store the currently selected category.

Find tableView(_:willSelectRowAtIndexPath:) and replace the method with the following:

override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath {
  selectedCategory = categories[UInt(indexPath.row)] as Category
  return indexPath
}

This method will now store the user’s selection to the property you declared above.

Build and run your app. Zoom and pan the map to somewhere interesting and create a new annotation by tapping on the + button in the top-right. Tap on the map pin to select it, and then tap on the annotation data to edit the details. Then tap the Category text field to see the list of Categories as shown below:

IntroductionToRelam_Categories

You can select a category, but that will only save it to the property and not anywhere else in the database. It’s all well and good to see the Categories show up in the app, but it’s always reassuring to actually see the records in the database. You can do this via the Realm Browser.

Working With the Realm Browser

One thing you don’t know at this point is where your Realm database lives. There’s a nice trick you can use to find it.

Open MapViewController.swift and add the following line to viewDidLoad() just after the call to super.viewDidLoad():

println(RLMRealm.defaultRealm().path)

This line simply prints the database location to the console. It’s a short step to then browse the database using the Realm Browser.

Build and run your app; you’ll see that it reports the location of the database in the Xcode console, as shown by the example below:

/Users/bill/Library/Developer/CoreSimulator/Devices/98817AD7-B6F0-403E-AC68-7551AB2B0607/data/Containers/Data/Application/13B349A1-0F8F-48AD-8BB6-1FD174E49571/Documents/default.realm

The easiest way to go to the database location is to open Finder, press Cmd-Shift-G and paste in the path your app reported. Use the path reported in your Xcode console; don’t copy the example path above, since it will undoubtedly be different on your system!

Once you open the folder in Finder, you might see one or two files. One of them will be default.realm, which is your database file. The second file, which may or may not be present, is default.realm.lock which prevents modification from other apps while the database is in use.

Double-click default.realm to open it with Realm Browser:

IntroductionToRealm_RealBrowserCategories

Note: Some complications may occur when the default.realm is already open in another application — in this case, your app running in the Simulator — and you try to open it in Realm Browser as well.

The .lock file is there to prevent this; the best way to open this database in Realm Browser is to exit the Simulator, delete the .lock file, then open the database from Realm Browser. When you’ve finished inspecting the database, exit Realm Browser then build and run the App in the simulator again.

Once the database is open in Realm Browser, you’ll see your Category class with a 5 next to it. This means that this class contains five records. Click a class to inspect the individual fields contained within.

realm-browser-data

Adding Categories

Now you can implement the logic to set the category of a specimen object.

Open AddNewEntryController.swift and add the following property to the class:

var selectedCategory: Category!

You’ll use this to store the selected Category.

Next, find unwindFromCategories() and add the following lines to the end of the method:

selectedCategory = categoriesController.selectedCategory
categoryTextField.text = selectedCategory.name

This method is called when the user selects a category from CategoriesTableViewController, which you set up in the previous step. Here, you retrieve the selected category and store it locally to selectedCategory and then fill in the text field with the category name.

Now that you have your categories taken care of, you can create your first specimen!

Still in AddNewEntryController.swift, add one more property to the class:

var specimen: Specimen!

This property will store the new specimen object.

Next, add the helper method below to the class:

func addNewSpecimen() {
  let realm = RLMRealm.defaultRealm() //1
 
  realm.beginWriteTransaction() //2
  let newSpecimen = Specimen() //3
  //4
  newSpecimen.name = nameTextField.text
  newSpecimen.category = selectedCategory
  newSpecimen.specimenDescription =  descriptionTextField.text
  newSpecimen.latitude = selectedAnnotation.coordinate.latitude
  newSpecimen.longitude = selectedAnnotation.coordinate.longitude
 
  realm.addObject(newSpecimen) //9
  realm.commitWriteTransaction() //10
 
  specimen = newSpecimen
}

Here’s what the code above does:

  1. You first get the default Realm.
  2. Here you start the transaction to add your new Specimen.
  3. Next, you create a new Specimen instance.
  4. Then you assign the specimen values. The values come from the text input fields in the user interface, the selected categories, and the coordinates from the map annotation.
  5. Here you add the new object to the realm.
  6. Finally, you commit the write transaction by calling commitWriteTransaction().

You’ll need some sort of validator to make sure all the fields are populated correctly in your Specimen. There’s an existing validateFields() method to do this that checks for a specimen name and description. Since you’ve just added the ability to assign a category, you’ll need to check for that field too.

Find the line in validateFields() that looks like this:

if (nameTextField.text.isEmpty || descriptionTextField.text.isEmpty) {

Change that line to this:

if (nameTextField.text.isEmpty || descriptionTextField.text.isEmpty || selectedCategory == nil) {

This verifies that all fields have been filled in and that you’ve selected a category as well.

Next, add the following method to the class:

override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject!) -> Bool {
  if validateFields() {
    if specimen == nil {
      addNewSpecimen()
    }
    return true
  } else {
    return false
  }
}

In the above code you call the method to validate the fields; if everything is filled in, then add the new specimen.

Build and run your app; tap the + button to create a new Specimen. Fill in the name and description, select a category, and tap Confirm to add your specimen to the database.

RW_Realm_Snow

The view controller dismisses — but nothing appears to happen. What’s the deal?

Ah — you’ve posted the record to your Realm — but you haven’t yet populated the map with your newly discovered specimen!

Retrieving Records

Now that you’ve added a specimen to the database, you want it to show up on the map.

If you want to check out your new record, open up Realm Browser and have a look at the data. Remember to exit the Simulator first.

RW_Realm_Browser_SnowLeopard

You’ll see your one lonely Specimen record, with all fields filled along with the latitude and longitude from MKAnnoation. You’ll also see the link to the Category of your Specimen — that means your one-to-many Category relationship is working as expected. Click the Category in your Specimen record to view the Category record itself.

Now you need to populate the map in the app.

Open SpecimenAnnotation.swift and add a property to the class:

var specimen: Specimen?

This will hold the specimen object for the annotation.

Next, replace the initializer with the following:

init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String, specimen: Specimen? = nil) {
  self.coordinate = coordinate
  self.title = title
  self.subtitle = subtitle
  self.specimen = specimen
}

The change here is to add an option to pass in a specimen object. The specimen will have a default value of nil which means you can omit that argument if you like. That means the rest of the app can continue to call the initializer with just the first three arguments as usual if there’s no specimen.

Now open MapViewController.swift add a new property to the class:

var specimens = RLMArray(objectClassName: Specimen.className())

Since you want to store a collection of Specimens in this property, you’ll need to declare it as type RLMArray. Remember, when you initialize an RLMArray object, you need to specify the class of the RLMObject instances it will hold.

Now you’ll need some sort of mechanism to retrieve all of the Specimen records. Still in MapViewController.swift, add the following method to the class:

func populateMap() {
  mapView.removeAnnotations(mapView.annotations) // 1
 
  specimens = Specimen.allObjects()  // 2
  // Create annotations for each one
  for specimen in specimens {
    let aSpecimen = specimen as Specimen
    let coord = CLLocationCoordinate2D(latitude: aSpecimen.latitude, longitude: aSpecimen.longitude);
    let specimenAnnotation = SpecimenAnnotation(coordinate: coord,
      title: aSpecimen.name,
      subtitle: aSpecimen.category.name,
      specimen: aSpecimen) // 3
    mapView.addAnnotation(specimenAnnotation) // 4
  }
}

Taking each numbered comment in turn:

  1. First, you clear out all the existing annotations on the map to start fresh.
  2. Next, you fetch all the specimens from Realm.
  3. You then create a SpecimenAnnotation with the coordinates of the specimen, as well as its name and category.
  4. Finally, you add the annotation to the MKMapView.

Now you need to call this method from somewhere. Find viewDidLoad() and add this line to the end of that method:

populateMap()

That will ensure the map will be populated with the specimens whenever the map view controller loads.

Next you just need to modify your annotation with the specimen name and category. Find unwindFromAddNewEntry() and replace the method with the following implementation:

@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {
  let addNewEntryController = segue.sourceViewController as AddNewEntryController
  let addedSpecimen = addNewEntryController.specimen as Specimen
  let addedSpecimenCoordinate = CLLocationCoordinate2D(latitude: addedSpecimen.latitude, longitude: addedSpecimen.longitude)
 
  if (lastAnnotation != nil) {
    mapView.removeAnnotation(lastAnnotation)
  } else {
    for annotation in mapView.annotations {
      let currentAnnotation = annotation as SpecimenAnnotation
      if currentAnnotation.coordinate.latitude == addedSpecimenCoordinate.latitude && currentAnnotation.coordinate.longitude == addedSpecimenCoordinate.longitude {
        mapView.removeAnnotation(currentAnnotation)
        break
      }
    }
  }
 
  let annotation = SpecimenAnnotation(coordinate: addedSpecimenCoordinate, title: addedSpecimen.name, subtitle: addedSpecimen.category.name, specimen: addedSpecimen)
 
  mapView.addAnnotation(annotation)
  lastAnnotation = nil;
}

This method is called once you’ve returned from AddNewEntryController and there’s a new specimen to add to the map. When you add a new specimen to the map, it gets the generic annotation icon; now that you have a category, you want to change that to the category-specific icon. Here you simply remove the last annotation added to the map (the generic-looking one) and replace it with an annotation that shows the name and category.

Build and run your app; create some new specimens of different categories and see how the map updates:

RW_Realm_map

A different view

You might have noticed the “Log” button in the top-left of the map view. In addition to the map, the app also has a text-based table view listing of all annotations called the Log view. The table view there is always blank, so it’s time to populate that with some data.

Open LogViewController.swift and replace the specimens property with the following:

var specimens = RLMArray(objectClassName: Specimen.className())

In the code above, you replace the placeholder array with an RLMArray which will hold Specimens just as you did in MapViewController.

Next, find viewDidLoad() and add this line after the call to super.viewDidLoad():

specimens = Specimen.allObjects().arraySortedByProperty("name", ascending: true) // add this line

This line will populate specimens with all the specimens in the database, sorted by name.

Next, replace tableView(_:cellForRowAtIndexPath:) with the following implementation:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  var cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as LogCell
  var specimen: Specimen!
 
  specimen = specimens[UInt(indexPath.row)] as Specimen
 
  cell.titleLabel.text = specimen.name
  cell.subtitleLabel.text = specimen.category.name
 
  switch specimen.category.name {
  case "Uncategorized":
    cell.iconImageView.image = UIImage(named: "IconUncategorized")
  case "Reptiles":
    cell.iconImageView.image = UIImage(named: "IconReptile")
  case "Flora":
    cell.iconImageView.image = UIImage(named: "IconFlora")
  case "Birds":
    cell.iconImageView.image = UIImage(named: "IconBird")
  case "Arachnid":
    cell.iconImageView.image = UIImage(named: "IconArachnid")
  case "Mammals":
    cell.iconImageView.image = UIImage(named: "IconMammal")
  default:
    cell.iconImageView.image = UIImage(named: "IconUncategorized")
  }
 
  return cell
}

This method will now show the specimen name and specimen category.

Build and run your app; tap Log and you’ll see all of your entered Specimens in the table view like so:

RW_Realm_LogView

Deleting Records

You’ve learned how to create records in Realm — but what if you add something by accident, or want to remove something you’ve added previously? You’ll need some mechanism to delete records from Realm. You’ll find that it’s a pretty straightforward operation.

Open LogViewController.swift and add the following helper method:

func deleteRowAtIndexPath(indexPath: NSIndexPath) {
  let realm = RLMRealm.defaultRealm() //1
  let objectToDelete = specimens[UInt(indexPath.row)] as Specimen //2
  realm.beginWriteTransaction() //3
  realm.deleteObject(objectToDelete) //4
  realm.commitWriteTransaction() //5
 
  specimens = Specimen.allObjects().arraySortedByProperty("name", ascending: true) //6
 
  tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) //7
}

Here’s what’s going on in this method:

  1. First, you grab the default realm.
  2. Next, you find the object you want to delete from the Specimens array.
  3. You then start a write transaction.
  4. Here you call deleteObject() and pass the object you wish to delete as the parameter to the call.
  5. Then you commit the write transaction to post the delete operation to the database.
  6. Since you’ve removed a Specimen, you need to reload the master list just as you did in viewDidLoad.
  7. Finally, you update the UITableViewCell so it removes the deleted row.

Next, find tableView(_:commitEditingStyle:forRowAtIndexPath:) and add the following line inside the if statement block:

deleteRowAtIndexPath(indexPath)

When the table view calls this delegate method to signal a deletion, all you need to do is call your helper method.

Build and run your app; view the Log and swipe right on the row you wish to delete. Kill the Simulator and open the database in Realm Browser to verify that the record has been deleted:

RW_Realm_LogView_deleting

Fetching With Predicates

You really want your app to rock — so you’ll need a handy search feature. Your starter project contains an instance of UISearchController — you’ll just need to add a few modifications specific to your app in order to make it work with Realm.

Open LogViewController.swift and replace the searchResults property with the following:

var searchResults = RLMArray(objectClassName: Specimen.className())

Since the search results will be Specimen objects, you want the property to be of type RLMArray.

Now add the method below to the class:

func filterResultsWithSearchString(searchString: String) {
  let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) // 1
  let scopeIndex = searchController.searchBar.selectedScopeButtonIndex
  switch scopeIndex {
  case 0:
    searchResults = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("name", ascending: true) //2
  case 1:
    searchResults = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("distance", ascending: true) //3
  case 2:
    searchResults = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("created", ascending: true) //4
  default:
    searchResults = Specimen.objectsWithPredicate(predicate)
  }
}

Here’s what the above function does:

  1. First you create a predicate which searches for names that start with searchString. The [c] that follows BEGINSWITH indicates a case insensitive search.
  2. If the first segmented button is selected, sort the results by name ascending.
  3. If the second button is selected, sort the results by distance ascending.
  4. If the third button is selected, sort the results by created date ascending.
  5. If none of the buttons are selected, don’t sort the results — just take them in the order they’re returned from the database.

Since the search results table view calls the same data source methods, you’ll need a small change to tableView(_:cellForRowAtIndexPath:) to handle both the main log table view and the search results. In that method, find the line that assigns to the specimen variable:

specimen = specimens[UInt(indexPath.row)] as Specimen

Delete that one line and replace it with the following:

if (searchController.active) {
  specimen = searchResults[UInt(indexPath.row)] as Specimen
} else {
  specimen = specimens[UInt(indexPath.row)] as Specimen
}

The above code checks that the searchController is active; if so, it retrieves the specimen from the search results; if not, then it retrieves the specimen from the specimens array instead.

Finally you’ll need to add a function to sort the returned results when the user taps a button in the scope bar.

Replace the empty scopeChanged method with the code below:

@IBAction func scopeChanged(sender: AnyObject) {
  let scopeBar = sender as UISegmentedControl
 
  switch scopeBar.selectedSegmentIndex {
  case 0:
    specimens = Specimen.allObjects().arraySortedByProperty("name", ascending: true)
  case 1:
    break
  case 2:
    specimens = Specimen.allObjects().arraySortedByProperty("created", ascending: true)
  default:
    specimens = Specimen.allObjects().arraySortedByProperty("name", ascending: true)
  }
  tableView.reloadData()
}

In the code above you check which scope button is pressed — A-Z, Distance or Date Added — and call arraySortedByProperty(_:ascending:) accordingly. By default, the list will sort by name.

Note that the case for sorting by distance (case 1) is empty – the app doesn’t have distance yet, so there’s nothing to do here yet but that’s a preview of things to come!

Build and run your app; try a few different searches and see what you get for results!

RW_Realm_Sort

Updating Records

You’ve covered the addition of records and the deletion of records — all that’s left is updating.

If you tap in a cell in LogViewController you will segue to the AddNewEntryViewController but with the fields empty. Of course the first step to letting the user edit the fields is to show the existing data!

Open AddNewEntryViewController.swift and add the following helper method to the class:

func fillTextFields() {
  nameTextField.text = specimen.name
  categoryTextField.text = specimen.category.name
  descriptionTextField.text = specimen.specimenDescription
 
  selectedCategory = specimen.category
}

This method will fill in the user interface with the specimen data. Remember, AddNewEntryViewController has up to this point only been used for adding new specimens so those fields have always started out empty.

Next, add the following lines to the end of viewDidLoad():

if (specimen == nil) {
  title = "Add New Specimen"
} else {
  title = "Edit \(specimen.name)"
  fillTextFields()
}

The above code sets the navigation bar title to say whether the user is adding a new specimen or updating an existing one. If it’s an existing specimen, you also call your helper method to fill in the fields.

Now you’ll need a method to update the Specimen record with the user’s changes. Add the following method to the class:

func updateSpecimen() {
  let realm = RLMRealm.defaultRealm()
  realm.beginWriteTransaction()
 
  specimen.name = nameTextField.text
  specimen.category = selectedCategory
  specimen.specimenDescription = descriptionTextField.text
 
  realm.commitWriteTransaction()
}

As usual, the method begins with getting the default Realm and then the rest is wrapped between beginWriteTransaction() and commitWriteTransaction(). Inside the transaction, you simply update the three data fields.

Six lines of code to update the Specimen record is all it takes! :]

Now you need to call the above method when the user taps Confirm. Find shouldPerformSegueWithIdentifier(_:sender:) and add the following lines just before the return true inside the first if block:

else {
  updateSpecimen()
}

This will call your helper method to update the data when appropriate.

Now open LogViewController.swift and replace prepareForSegue(_:sender:) with the following implementation:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
  if (segue.identifier == "Edit") {
    let controller = segue.destinationViewController as AddNewEntryController
    var selectedSpecimen : Specimen!
    let indexPath = tableView.indexPathForSelectedRow()
 
    if searchController.active {
      let searchResultsController = searchController.searchResultsController as UITableViewController
      let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow()
      selectedSpecimen = searchResults[UInt(indexPathSearch!.row)] as Specimen
    } else {
      selectedSpecimen = specimens[UInt(indexPath!.row)] as Specimen
    }
 
    controller.specimen = selectedSpecimen
  }
}

You need to pass the selected specimen to the AddNewEntryController instance. The complication with the if/else is because getting the selected specimen is slightly different depending on whether the user is looking at search results or not.

Build and run your app; open the Log view and tap on an existing specimen. You should see the details with all the fields filled in, ready for editing.

RW_Realm_Updating

Including Ignored Properties

There’s one more thing to make your app perfect — and possibly put you in line for an Apple Design Award!

RW_Realm_Award

Hey — developers can dream, can’t we? :]

Remember how you can’t sort the Log view by distance? There’s a bit of code to put in place before that can happen — but the results will be worth it.

Open Specimen.swift and add one more property to the class:

dynamic var distance: Double = 0

This property will hold the distance between the annotation and the user’s location. However, there’s no real need to persist distance because the user’s location will change each time they use the app. You want the distance as part of the model, but you don’t want Realm to persist the field.

Realm supports properties like this and calls them ignored properties. Add the following method to the class next:

func ignoredProperties() -> NSArray {
  let propertiesToIgnore = [distance]
  return propertiesToIgnore
}

To define your list of ignored property you simply implement the method ignoredProperties() and return an array of properties that you don’t want persisted.

Since you don’t store the distance, it’s clear you’ll need to calculate it yourself.

Open MapViewController.swift and add the following method:

func updateLocationDistance() {
  let realm = RLMRealm.defaultRealm()
 
  for specimen in specimens {
    let currentSpecimen = specimen as Specimen
    let currentLocation = CLLocation(latitude: currentSpecimen.latitude, longitude: currentSpecimen.longitude)
    let distance = currentLocation.distanceFromLocation(mapView.userLocation.location)
    realm.beginWriteTransaction()
    currentSpecimen.distance = Double(distance)
    realm.commitWriteTransaction()
  }
}

For each specimen, you calculate the distance between it and the user’s current location. Even though you don’t persist the distance, you still need to store it in the record and wrap the change in a write transaction.

Next, add the following code to the end of prepareForSegue(_:sender:)

else if (segue.identifier == "Log") {
  updateLocationDistance()
}

Here, you’re calling the helper method to calculate the distance before the user reaches the Log screen.

Next, open LogViewController.swift and find tableView(_:cellForRowAtIndexPath:). Add the following lines near the end of the method, just before the return statement:

if specimen.distance < 0 {
  cell.distanceLabel.text = "N/A"
} else {
  cell.distanceLabel.text = String(format: "%.2fkm", specimen.distance / 1000)
}

Finally, find scopeChanged() and replace the break statement in case 1 with the following:

specimens = Specimen.allObjects().arraySortedByProperty("distance", ascending: true)

Build and run your app, and…CRASH!

'RLMException', reason: 'Column count does not match interface - migration required’

Oh, man — what went wrong?

When you added the distance property to the Specimen model you changed the schema — and you didn’t tell Realm how to handle the addition of a new field. The process to migrate a database from one version to another is beyond the scope of this tutorial. This isn’t limited to Realm; Core Data also requires that you migrate the database when fields are added, changed or removed.

For the purposes of this tutorial, you can simply remove the app from the Simulator and build and run your app again. This forces the app to create a brand new database — with your new schema in use.

Delete the app from the simulator, then build and run your app. Add some new specimens and bring up the Log view, where you’ll see the relative distance to each as below:

RWP_Realm_Distance

You might need to simulate a location to calculate a valid distance. From the Simulator menu, select Debug\Location and pick one of the items in the list.

Where to Go From Here?

You can download the finished project here.

In this tutorial you’ve learned how to create, update, delete and fetch records from the Realm database, how to use predicates, and sort the results by their properties.

Some of you may ask, “Since Realm is a relatively new project, should I use it in production apps?”

Realm has only recently been released to the public, but it’s been used in production since 2012. I have personally implemented a relatively complicated project using Realm, and even though the app is still under development it’s working really well!

Realm is stable if you’re using Objective-C; however even with the recent release of Xcode 6 GM, Swift is still very much in flux so I’d suggest waiting a little longer for Swift and Realm to be more stable before using them in production apps.

There are many other features of Realm that weren’t touched on in this tutorial:

  • Migrations: In this tutorial you saw that modifying the Realm schema caused issues. To learn how to migrate databases between versions, check out the Realm documentation regarding migrations to see how you can implement this mechanism in your app.
  • Other Realms: You’ve only worked with the “default” realm, but there are other realms that you can use, such as an in-memory default realm that don’t persist anywhere. You can also use multiple realms if you need to use different databases to hold different kinds of data.
  • Concurrency: There’s an art to accessing a realm from different threads; read through the documentation to see what rules you need to follow.

You can learn about the above topics and much more in the official documentation, which I’ve found to be quite good.

If you have any comments or questions on this tutorial or Realm in general, please join the discussion below!

Introduction to Realm is a post from: Ray Wenderlich

The post Introduction to Realm appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4404

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>