iPhone Development – core data relationships tutorial part 1

I’m going to start a short series on Core Data relationships and maybe throw in some general Core Data stuff too. Here in part one we’re just going to set our app up with core data and add two entities with a simple one to one relationship between them. A one to one relationship means that for every Fruit there will be one source and in our case here the reverse it true too, for every source there is one fruit.

Core Data Relationships

1.) Create a new Tab Bar Application named CoreDataRelationshipsTutorial.

2.) Change FirstView.xib so it looks similar to this.
FirstView.xib

3.) Add the core data framework to the app.
Add Core Data Framework

4.) Right click on Supporting Files and select New File, then choose Core Data select Data Model and hit Next. I just accepted the default name of Model and clicked save.
Add Data Model

5.) Select Model.xcdatamodeld and the visual editor will open. Click Add Entity and name it Fruit. Add an Attribute named fruitName of type String. Add another Entity named Source with an Attribute sourceName, which will also be of type String.

6.) Select Fruit and then click the plus symbol under Relationships. Name the relationship fruitSource. Set the destination to Source, there will be no inverse yet. In the relationship data model inspector uncheck the Optional checkbox. In the delete rule select Cascade.
Edit Data Model

7.) Now select Source and add a relationship named sourceFruit. Destination should be Fruit and set the inverse to artistCareer. Uncheck the Optional checkbox again.
Set up relationships

8.) Select Fruit under ENTITIES and then go under the file menu up top and select New File. Choose Core Data and NSManagedObject subclass,, click Next. Keep the default location and click Create.
Create Managed Objects

Repeat this same process after selecting Source under ENTITIES.

You should now see your new objects listed under Supporting Files.
xcode view

9.) Open up CoreDataRelationshipsTutorial-Prefix.pch and add an import for CoreDate. This saves us from having to import it into every file that will use it.

#import 

#ifndef __IPHONE_3_0
#warning "This project uses features only available in iPhone SDK 3.0 and later."
#endif

#ifdef __OBJC__
    #import 
    #import 
    #import 
#endif

10.) Now let’s add all the necessary Core Data code to the app delegate files.

First the header file. Import our FirstViewController, then declare private instance variables for our NSManagedObjectContext, NSManagedObjectModel and NSPersistentStoreCoordinator. Create an IBOutlet with out FirstViewController, and declare two methods that we’ll implement.

#import 
#import "FirstViewController.h"

@interface CoreDataRelationshipsTutorialAppDelegate : NSObject  
{

@private
    NSManagedObjectContext *managedObjectContext;
    NSManagedObjectModel *managedObjectModel;
    NSPersistentStoreCoordinator *persistentStoreCoordinator;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;

@property (nonatomic, retain) IBOutlet FirstViewController *firstViewController;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSURL *)applicationDocumentsDirectory;
- (void)saveContext;

@end

11.) Now open the app delegate implementation file. Synthesize our firstViewController, then set it’s managedObjectContext to the one created in the app delegate. You may see an error on the line that sets the managedObjectContext because we haven’t set that up in FirstViewController yet.

#import "CoreDataRelationshipsTutorialAppDelegate.h"

@implementation CoreDataRelationshipsTutorialAppDelegate

@synthesize window=_window;
@synthesize tabBarController=_tabBarController;
@synthesize firstViewController;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    firstViewController.managedObjectContext = self.managedObjectContext;

    self.window.rootViewController = self.tabBarController;
    [self.window makeKeyAndVisible];
    return YES;
}

Implement all these methods.

/**
 Returns the URL to the application's Documents directory.
 */
- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

- (void)saveContext
{
    
    NSError *error = nil;
    NSManagedObjectContext *objectContext = self.managedObjectContext;
    if (objectContext != nil)
    {
        if ([objectContext hasChanges] && ![objectContext save:&error])
        {
            // add error handling here
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

#pragma mark -
#pragma mark Core Data stack

/**
 Returns the managed object context for the application.
 If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
 */
- (NSManagedObjectContext *)managedObjectContext
{
    
    if (managedObjectContext != nil)
    {
        return managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        managedObjectContext = [[NSManagedObjectContext alloc] init];
        [managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return managedObjectContext;
}

/**
 Returns the managed object model for the application.
 If the model doesn't already exist, it is created from the application's model.
 */
- (NSManagedObjectModel *)managedObjectModel
{
    if (managedObjectModel != nil)
    {
        return managedObjectModel;
    }
    managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
    
    return managedObjectModel;
}

/**
 Returns the persistent store coordinator for the application.
 If the coordinator doesn't already exist, it is created and the application's store added to it.
 */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    
    if (persistentStoreCoordinator != nil)
    {
        return persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataTabBarTutorial.sqlite"];
    
    NSError *error = nil;
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    } 
    
    return persistentStoreCoordinator;
}

12.) Open up FirstViewController.h and let’s set it up with the necessary code and instance variables.

#import 

@interface FirstViewController : UIViewController 
{
    
    NSFetchedResultsController  *fetchedResultsController;
    NSManagedObjectContext      *managedObjectContext;

}

@property (nonatomic, retain) NSString *fruitNameString;
@property (nonatomic, retain) NSString *fruitSourceString;


@property (nonatomic, retain) NSFetchedResultsController    *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext        *managedObjectContext;

- (IBAction) saveData;

@end

13.) Now for the implementation file. Let’s import our managed objects.

#import "FirstViewController.h"
#import "Fruit.h"
#import "Source.h"

Then synthesize the instance variables.

@synthesize fetchedResultsController, managedObjectContext;
@synthesize fruitNameString, fruitSourceString;

Let’s go ahead and set the values of those two strings in ViewDidLoad.

- (void)viewDidLoad
{
    [super viewDidLoad];
    fruitNameString = [[NSString alloc] initWithString:@"Apple"];
    fruitSourceString = [[NSString alloc] initWithString:@"Apple Tree"];
}

14.) Implement the saveData method.

- (IBAction) saveData
{
    NSLog(@"saveData");
    Fruit *fruit = (Fruit *)[NSEntityDescription insertNewObjectForEntityForName:@"Fruit" inManagedObjectContext:managedObjectContext];
    fruit.fruitName = fruitNameString;
    Source *source = (Source *)[NSEntityDescription insertNewObjectForEntityForName:@"Source" inManagedObjectContext:managedObjectContext];
    source.sourceName = fruitSourceString;
    
    // Because we set the relationship fruitSource as not optional we must set the source here
    fruit.fruitSource = source;
    
    NSError *error;
    
    // here's where the actual save happens, and if it doesn't we print something out to the console
    if (![managedObjectContext save:&error])
    {
        NSLog(@"Problem saving: %@", [error localizedDescription]);
    }


    // **** log objects currently in database ****
    // create fetch object, this object fetch's the objects out of the database
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Fruit" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    
    for (NSManagedObject *info in fetchedObjects)
    {
        NSLog(@"Fruit name: %@", [info valueForKey:@"fruitName"]);
        Source *tempSource = [info valueForKey:@"fruitSource"];
        NSLog(@"Source name: %@", tempSource.sourceName);

    }
    [fetchRequest release];
}

15.) Release our objects in the dealloc method and set them to nil in viewDidUnload.

- (void)viewDidUnload
{
    [super viewDidUnload];
    fetchedResultsController = nil;
    managedObjectContext = nil;
    fruitNameString = nil;
    fruitSourceString = nil;
}

- (void)dealloc
{
    [fetchedResultsController release];
    [managedObjectContext release];
    [fruitNameString release];
    [fruitSourceString release];

    [super dealloc];
}

16.) Open up FirstView.xib and connect the UIButton to our saveData IBAction.

17.) Open up MainWindow.xib, select the app delegate and connect firstViewController outlet to FirstViewController under the Tab Bar Controller.
Link view controller

18.) Now you can run the app and hit the Save Data button. Look in the console to see the results of the fetch.
console

The important things to note from this tutorial are these.

When we created the relationship from Fruit to Source we made it so that it was not optional. Therefore during our saveData method we had to set the fruitSource to something.

    Fruit *fruit = (Fruit *)[NSEntityDescription insertNewObjectForEntityForName:@"Fruit" inManagedObjectContext:managedObjectContext];
    fruit.fruitName = fruitNameString;
    Source *source = (Source *)[NSEntityDescription insertNewObjectForEntityForName:@"Source" inManagedObjectContext:managedObjectContext];
    source.sourceName = fruitSourceString;

Try commenting out that last line

//    source.sourceName = fruitSourceString;

And then running it again. What happens? Crash and burn. Because the relationship is not optional you must set the sourceName.

You can also see from the block of code above that we use reuse the same managedObjectContext to create both of the managed objects. Then we set the values and just saved the context. Doing this saved both objects (entities in Core Data).

Another thing to take note of happens in the fetch process. You notice that we only fetch the Fruit entity. But because of the relationship between Fruit and Source we can access the Source entity. We don’t need to do a separate fetch on Source.

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Fruit" inManagedObjectContext:managedObjectContext];
    [fetchRequest setEntity:entity];
    NSArray *fetchedObjects = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    
    for (NSManagedObject *info in fetchedObjects)
    {
        NSLog(@"Fruit name: %@", [info valueForKey:@"fruitName"]);
        Source *tempSource = [info valueForKey:@"fruitSource"];
        NSLog(@"Source name: %@", tempSource.sourceName);
        [tempSource release];
    }

Okay that does it for this tutorial. Next time we will look at a one to many relationship.

As always here’s the code.



This entry was posted in Data and tagged , . Bookmark the permalink.

15 Responses to iPhone Development – core data relationships tutorial part 1

  1. Penthesilea says:

    Hi Kent,

    thanks for your great tutorials. Normal everything works fine but this time I got an error even after running your Code. After pressing the button “Save Data” the simulator stops and in the main.m something does not work : int retVal = UIApplicationMain(argc, argv, nil, nil); (ESC-BAD_ACCES).
    What is necessary to do avoid this ?
    Best regards from Germany :)
    Penthesilea

    • Kent says:

      Yes I have found the error. Delete the lines that release Fruit and Source. Also delete the one that releases tempSource. My bad on those. I’m correcting the post now and will correct the code tonight.

      By the way these changes are in the saevData method of FirstViewController.m.

      Thanks

  2. Sabrina says:

    Hi,
    In my application, I used sqlite database. At first, I used a csv file to store data in my data base ( I create a function that extract data from the csv file and insert it in the DB) . then I use this DB to get some informations in a table view when the user touch up a button. The problem is every thing does work on the simulator but not on the device(iPhone 4).
    I know that the device is case sensitive, but i haven’t any lower or upper case in my DB name

    Sabrine
    I made a function in the appdelegate testing at the beginning if the DB does exist, if not it copy it.
    Unfortunately it doesn’t help
    I really don’t know how to solve this issue..

    • Kent says:

      Are you saying you are using aqlite as your storage container with Core Data? Or are you not using Core Data?

      If it works in simulator but not on device that could point to a path issue. First delete the app from the simulator, clean the app in xcode and run it again just to make sure it really is working in the simulator.

      • Sabrina says:

        I am not using Core data, just sqlite

        • Kent says:

          Did you try deleting it from your simulator and cleaning the project like I suggested. I don’t expect this to solve anything, but it will help in trouble shooting.

          I’m still guessing that there is a path issue that is showing up on the phone but not in the simulator. But without more information there’s no way to know for sure.

          • Sabrina says:

            Hi Kent
            I delete the application from simulator as you told. When i run It, It doesn’t work! and I get the same error message like when i tested on device.
            It seemd that the DB is empty! I don’t understand why; I have used (just one time)a method to populate the DB with data ( in application didFinishLaunchingWithOptions method ).
            Shall I do that every time I launch the application,?

  3. Kent says:

    Sabrina,

    I had suspected that might be the case. That if you deleted the app from the simulator and ran it again it wouldn’t work. So now at least we know it isn’t an issue with the device compared to the simulator, which should make it easier to debug.

    Try adding some data to the DB through terminal and see if that works. If so you will be able to narrow things down to the method you are using to populate the data.

    Here is a tutorial on using sqlite3, just in case you haven’t already seen it.
    http://www.theappcodeblog.com/2011/03/07/db-app-tutorial-part-1-create-a-database-using-sqlite/

  4. Sabrina says:

    Kent

    I Have another issue with the DB :
    When the user select a row in the table view , in the didSelectRowAtIndexPath property I call a method to insert a new element in the DB;
    The problem is it works just one time: when i select another row , the application crashed. I have to stop the application and run it to make that work and again it works just one time!!

    (Sorry for asking many question, but I really need help!)

  5. Sabrina says:

    and I get only this error msg
    “Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Error while inserting data. ‘not an error”

  6. nestor says:

    Hi Ken, thanks for these great tutorials!
    I’m just starting developing iphone/ipad apps. I have a question regarding how to use data in these applications. I have a webpage that use SQLServer, is there a way I can access my database to get some data to fill into my application? Thanks.

    • Kent says:

      You can access the data a few different ways. If you have a webpage that is accessing SQLServer you could write a web service to return a query of the data. You could possibly use Sqlite, but I haven’t actually tried hooking that up in the way you would want to. Can you have the webpage return xml to you, possibly by using a web service?

  7. iphone_coder says:

    Hello,

    I am new to iphone development. I am doing a project in which i have to have a login screen and then after scuccessful login app should be able to fetch the entries related to that user.

    So could you tell me what kind of relationship this will have?

  8. Craig says:

    The error usually relates to the fact the managed object context hasn’t been assigned.
    I put this bit of code in to help..

    AppDelegate *app = [[AppDelegate alloc] init];

    managedObjectContext = [app managedObjectContext];

    Just before this bit in the saveData routine

    NSLog(@”saveData”);
    Fruit *fruit = (Fruit *)[NSEntityDescription insertNewObjectForEntityForName:@”Fruit”

Add Comment Register



Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>