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.
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:
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.
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.
Note that “CoreData” is a category, not a selection.
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.
An entity is an object that subclasses NSManagedObject. These objects can be stored in CoreData.
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.
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 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.
[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.
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.
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];
Enough copy/pasting. Lets get to work.
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.
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.
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];
Its important to fill the endCoordinate field of a trip so we can later draw it on the map.
Lets examine the database to ensure everything went well.
- (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.
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.
@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
- (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.
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.