User Tools

Site Tools


ios-labs-s14:class-08

DriveSense Database

CoreData will be the most complex topic we cover in this class. See here for a theoretical understanding of CoreData.

Please pull the last version of the project from the Git repository regardless if you've finished or not. Make sure to run it and inspect the code to see how teh solutions were implemented!

DriveSense now appropriately receives GPS data and updates the user's location in the map (independently through CoreLocation and MapKit) but doesn't actually store any of that information. Here we will add the CoreData framework and save trips to the local database.

For the love of Jobs, please review variable scope before you get started.


Importing CoreData

You'll have to take the same steps as last time to link the project against the CoreData framework but this time there are a whole bunch of auxiliary methods and hooks you need to set up. Although the code looks a little daunting, we only have to add it into the application because we didn't choose the option to implement CoreData when creating the project from scratch; do this and all of this work is taken care of you when you start a new project. Its still useful to understand the steps.

The steps:

  1. Link against CoreData framework
  2. Add CoreData accessor methods into AppDelegate
  3. Import CoreData headers into every file
  1. Add the CoreData framework

Any objects that CoreData stores have to subclass NSManagedObject instead of NSObject. This object comes from the CoreData framework, and so to avoid importing <CoreData/CoreData.h> into every class in your project, we'll use the prefix header to import into the whole file.

Find this file in Supporting Files / PROJECTNAME-Prefix.pch. This is a header file for every header file. Any imports you place here will be copied into each header file at compile time, essentially importing into the whole project.

Importing CoreData Header

  1. In your prefix file, add “<CoreData/CoreData.h>” in the objective-c section (under Foundation)

Recall our cursory overview of |singletons last tutorial. The key points are, again, that you can access an object that is global in scope, and that there can only be one instance of the object at any given time.

It turns out that our choice of design pattern was not an accident. Every iOS application is, at heart, a singleton. As you may have noticed by now, every new iOS project begins with a class file autogenerated no matter what, and we've ignored them thoroughly. Open AppDelegate.m.

If you've seen a crash on the simulator yet (unlikely, I'm sure) then you've seen the following code:

Remember that all Objective-C code is ultimately compiled into C; an application as loaded onto the device is just a C program. The screen you see above is the main method for that C program, the primary point of entry. Every app has a file called main.m with similar code. The only thing it does is instantiate an object of type AppDelegate, which every app must also have.

The AppDelegate starts all other controllers, owns all other views behind the scenes. In previous versions of iOS, or if you want to heavily customize your application, you can see and use a lot of the setup code within AppDelegate to set up your app. As it is a singleton, any class may access it by calling the right accessor method.

Sounds like a great place to put a bunch of CoreData methods. Remember these are all stock, autogenerated methods; there's nothing special going on here, this is the minimum necessary to interact with the framework and is automatically included in CoreData projects.

Adding CoreData Methods and File

  1. See CoreData page, follow “Adding Accessor Methods” steps.
  2. Create the CoreData model through File > New > CoreData > Data Model (see screenshot)

Note that “CoreData” is a category, not a selection.

Using CoreData

CoreData has its own system of managing the App's model, it fully represents the model. The first step is creating the schema, or description of the database's object, through the CoreData interface. The file you created in the last step, Model.xcdatamodeld represents the schema of the application. It details what objects exist and how they are related to each other.

Note that although the term “database” has been used frequently here, CoreData is not technically a relational database.

The following steps detail how to create new objects through the interface, detail their data, and instantiate them into memory. These are not presented as steps in the tutorial, you should read them quickly, see the instructions below them, and then trace through them as needed.

The CoreData interface.

An entity is an object that subclasses NSManagedObject. These objects can be stored in CoreData.

Creating an Entity

  1. Select the xcdatamodeld file
  2. Click “Add Entity”
  3. Doubleclick the name to change it

An attribute is essentially a public instance variable when objects are instantiated in your code. When in the database, think of them as columns in a table.

Adding an Attribute

  1. Select an Entity
  2. Press the plus sign under Attributes to add a property
  3. Give the property a name and a type

Adding a Relationship

  1. Press the plus button under Relationships
  2. Name the relationship
  3. Select another Entity as the Destination
  4. In the right pane (with the relationship selected) choose “To Many” or “To One” to signify an array of objects or a single object

Now that you've described to CoreData how you want the database to look, its time to look at how you create new objects from Entities. Thankfully, CoreData can autogenerate source code files with the correct fields pre-filled for you.

Creating Source Code

  1. Ensure you have the .xcdatamodeld selected in the project explorer
  2. Go to File > New. Select the CoreData category on the left side and select “NSManagedObject subclass”
  3. Check the box to choose your schema
  4. Check all the boxes for entities on the next page

Creating a new instance of your model objects is similar to creating regular objects, but alloc/init is not used to instantiate them. Alloc/init creates space for an object in the heap, or dynamic object memory, but all CoreData objects live in the object context, a region of memory managed by CoreData. This allows CoreData to make a lot of optimizations to the way that these objects are managed.

Creating a New Instance

  1. Create a pointer to your new object, but instead of calling alloc/init on the right of the equals, call:

[NSEntityDescription insertNewObjectForEntityForName:@“ENTITYNAME” inManagedObjectContext:context];

For example:

GPSCoordinates *coord = [NSEntityDescription insertNewObjectForEntityForName:@"GPSCoordinates" inManagedObjectContext:context];

“insertNewObjectForEntityForName” creates an object within the context instead of app memory, giving CoreData control over it. Since you can have multiple databases in the same app, you have to pass the method the intended context.

Saving instances to the database requires you to call save: on the context. The method takes an argument of type NSError should something go wrong.

Saving Instances

  1. Use the following code to save objects to the database:
NSError *error;

    if (![context save:&error])
        NSLog(@"ERROR saving: %@", [error localizedDescription]);

Any data loaded from the database must be “fetched.” There are a number of ways to filter your fetches into the database (or “queries”), but we won't cover them now. Again, you execute your request on a given context: you must pass the name of the entity you are searching for.

Loading Data

  1. Use the following code to fetch data from the database:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Trip" inManagedObjectContext:context];
    [fetchRequest setEntity:entity];
    NSError* error;
    
    //returns an array on success
    [context executeFetchRequest:fetchRequest error:&error];

Creating Trips

Enough copy/pasting. Lets get to work.

Detailing Schema

  1. Select the model file
  2. Add two entities, “Trip” and “GPSCoordinate”
  3. Add two attributes to Trip. Call them date and name, of type Date and String, respectively
  4. Add three attributes to GPSCoordinate, named timestamp, lon, and lat of types Date, Double, Double
  5. Add three relationships to Trip. Each should have GPSCoordinate as a destination. Name them endCoordinate, startCoordinate, and gpsCoordinates. They should all be to-one except for gpsCoordinates, which should be to-many (indicating there are many entities stored under this relationship)
  6. Create the source code files from the schema

What the Trip entity should look like. Note the selection of to-many on the right pane.

Inspect the new source code files, checking out the autogenerated methods and properties.

Before we can put the objects to work, you need to get access to the context. Part of all that code you copied into AppDelegate earlier deals with exposing the context to the rest of the app.

Accessing Context

  1. Import AppDelegate into TripRecorder
  2. Create an instance variable of type NSManagedObjectContext called “context”
  3. Initialize it in init as follows:
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
 context = [appDelegate managedObjectContext];

Note the “delegate” method call above is the singleton accessor of the AppDelegate. UIApplication should be self-explanatory; you'll see it used rarely.

Lets put the objects to work.

Creating Trip Objects

  1. Import Trip and GPSCoordinate headers into TripRecorder
  2. Add a private Trip instance variable called into TripRecorder
  3. Create a new trip and assign it to this instance variable (using the steps outlined above, NOT alloc/init) in startRecording. Save the context.

You should save after every change to the schema, just to be safe. In reality it may affect performance and isn't strictly needed, but it ensures the database is always up to date no matter what happens to the app.

Making Coordinate objects requires a little conversion. You can assign values to NSManagedObjects just like any other properties, using the dot operator; its the type assignment that's a little tricky.

The date first: calling [NSDate date] returns the current date. You can assign this directly to the date property.

The lat and lon fields are of type NSNumber, but coordinate.latitude and coordinate.longitude return double. You must create a new NSNumber and call its conversion class method initWithDouble: shown below:

[NSNumber numberWithDouble:DOUBLE];

Creating Coordinate Objects

  1. Instantiate a new GPSCoordinate object in TripRecorder's didUpdateLocation
  2. Set its date and coordinate fields appropriately
  3. Add it to the Trip. Check Trip.h for a hint how to do this.
  4. Save the context.

Its important to fill the endCoordinate field of a trip so we can later draw it on the map.

Closing Out Trips

  1. In “stopRecording,” create a new GPS coordinate from the instance variable lastReceivedLocation
  2. Assign it to the Trip's instance variable as endCoordinate
  3. Set the Trip instance variable to nil

Lets examine the database to ensure everything went well.

Debugging Database

  1. Write a new method called getTrips, have it return an NSArray. Make the method public
  2. Use the code provided above to fetch all Trip objects
  3. copy the following method into RootViewController:
- (void) printTrips {
    NSArray * trips = [[TripRecorder recorder] getTrips];
    
    NSLog(@"SharedModel: loaded %lu trips from database", trips.count);
    
    for(Trip *trip in trips) {
        long numCoordinates = trip.gpsCoordinates.count;
        NSLog(@"%@ has %lu coordinates, date: %@", trip.name, numCoordinates, trip.date);
    }
}

Where do you call this method? Find a place you can trigger it on command, maybe in a currently unused function somewhere….

Test. Record a few trips on the simulator, then trigger the debug method and inspect the log.


Drawing on the Map

Lets trace the trips retrieved in the previous section onto the map. Each trip will have a red trace line and two pins, one for the start coordinate and one for the end coordinate.

Create Annotation Object

  1. Create new class MapAnnotation, subclass of NSObject
  2. Have it implement the protocol MKAnnotation by typing “<MKAnnotation>” immediately after the superclass name in the header file
  3. Import it into MapRoot
  4. Add the following properties to the header file
@property (nonatomic, copy) NSString * title;
@property (nonatomic, copy) NSString * subtitle;
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;

When the user touches the trips button (the car icon) you should add the annotations. The method for adding annotations to a map is called addAnnotation. Create a new annotation object for each start and stop coordinate and add it to the map.

Note that the property called coordinate on the annotation object is of type CLLocationCoordinate2D– you cannot pass the GPSCoordinate object! Instead, convert the lat and long from the GPSCoordinate object to the following helper function:

CLLocationCoordinate2DMake(LAT, LONG);

Which returns an object of type CLLocation2D

Create Overlay

  1. Copy the following methods into MapRoot:
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
    //check to make sure its a polyline (this method is called for anything else thats drawn on a map)
    if (![overlay isKindOfClass:[MKPolygon class]]) {
        MKPolyline *route = overlay;
        MKPolylineRenderer *renderer = [[MKPolylineRenderer alloc] initWithPolyline:route];
        renderer.strokeColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:1];
        renderer.lineWidth = 3.0;
        return renderer;
    } else {
        return nil;
    }
}

- (void) drawRouteForCoordinates:(NSOrderedSet *) points{
    //draw the route for the map based on the coordiantes
    MKMapPoint* pointArray = malloc(sizeof(CLLocationCoordinate2D) * [points count]);
    
    //extract the CLLocation2D objects, add them all to a point in the reference frame of the map
    //note the array is a C array
    for(int i = 0; i < [points count]; i++) {
        GPSCoordinate *coordinate = [points objectAtIndex:i];
        
        double lat = [coordinate.lat doubleValue];
        double lon = [coordinate.lon doubleValue];
        
        MKMapPoint point = MKMapPointForCoordinate(CLLocationCoordinate2DMake(lat, lon));
        pointArray[i] = point;
    }
    
    // create the polyline based on the array of points, add to map
    [map addOverlay:[MKPolyline polylineWithPoints:pointArray count:[points count]]];
    free(pointArray);
}

The first method is an optional delegate method for MKMapViews that returns a renderer specifying how the given object should be drawn. In this case we're making a thin red line. The second method is a little tricky; since the points in the line drawn are formed from structs, you need to make a C array instead of an Objective-C array to store the points. Didn't want to deal with that now.

Call drawRouteForCoordinates for each trip. Pass it the array of GPSCoordinate objects from the Trip object.


Conclusion

CoreData will not be that useful everyday, but if you ever have to write an app that has to deal with a large dataset it will make your life a lot easier. If you nail down the basics and make some nice wrapper methods for the data objects, it can also make Model management far easier.

Next class we will cover Social integration (Facebook) and draw on some maps.


ios-labs-s14/class-08.txt · Last modified: 2014/02/28 14:55 by mbarboi