RSS

Book: Learning Core Data for iOS – Rough Cut Released!

Hello Again!

It’s been a long time since I’ve posted anything, as you know I’ve been busy writing you a book! Recently a rough cut of the first chapters of Learning Core Data for iOS was released to Safari Books Online. This service provides early access to a rough draft of the book as it is written. As I write more chapters, they will be added to Safari Books Online. Note however that they are first drafts, so, there may be minor issues that need fixing before the final book is released. I’ve just started Chapter 9, so I’m over halfway there.

payday loans lenders online

If you purchase the book through the rough cuts program you save ~25% on the cost of the book, as opposed to waiting for the final version to be released. In addition, you also have a chance to provide feedback on the book in case you pick up something I missed!

Here is what the cover looks like:

So, what’s in the book?

The book will show you from start to finish how to build a Core Data application called Grocery Dude. It was originally going to be called Grocery Pal, however that name was taken before I got a chance to upload the app to the App Store. The App Store app is as at Chapter 7 so far.  As I complete chapters I submit an update to the App Store.

The book includes ALL the code required to build Grocery Dude, along with detail instructions on HOW and WHY you implement things in a certain way. If you like the tutorials on this website then you should really like the book as it has had a lot more focus and care in being written.

The book covers topics such as:

  • Data Model design, versioning, automatic migrations and manual migrations with progress indication
  • Populating Table Views, Views and Picker Views using Core Data, including a chapter on performance optimization
  • Implementing data backup and restore using Dropbox
  • Integrating with Web Services or iCloud – including support for changing and disabling iCloud profiles
  • Preloading data via XML, a pre-populated persistent store and the most elegant deep copy solution you’ve seen

I do hope you’ll get some use out of the book.

Happy Reading!!

Cheers

Tim

 
9 Comments

Posted by on January 25, 2013 in Blog

 

Teamwork Released!

You might have noticed the tutorials slow down recently. Well, that’s because I’ve been working hard to expand the Staff Manager app into Teamwork which is now available on the App Store!

Over the coming months I’ll be working on a new iOS6 Core Data book which includes much better iCloud support, Dropbox integration, Database Backup/Restore etc. I’ll keep you posted!

Cheers All,

Tim

P.S. In the mean time while my attention is diverted on the book (and Teamwork enhancements) please be aware my e-mail response time will be very slow.

 
2 Comments

Posted by on August 18, 2012 in Blog

 

Core Data in iCloud

Introduction

The aim of this tutorial is to show how to add iCloud support to a project.  The starting point will be the final project from the end of the Core Data Universal / iPad Storyboard tutorial. I would recommend going through that tutorial first so you’re familiar with the Staff Manager project.

Prerequisites

Download the final project from the end of the Core Data Universal / iPad Storyboard tutorial. Extract and open it with Xcode.
Note: iCloud does NOT work in the iOS Simulator. I recommend testing on two devices, such as an iPad and an iPhone at the same time.

Create iCloud Staff Manager App ID

Head over to https://developer.apple.com/membercenter/ then enter the iOS Provisioning Portal.  The link looks like this:

Select App IDs

Select New App ID

Configure the App ID as follows then click Submit:

Click Configure:

Tick Enable for iCloud then click Done:

iCloud is now enabled for Staff Manager:

You will notice that the App ID has a prefix of the Team ID.  You will have a different Team ID prefix to me.  You will need to substitute your own prefix whenever you see my prefix in code:


To use the new App ID you’ll need a new Provisioning Profile so click Provisioning then New Profile:

Configure the Provisioning Profile as follows:

Click Submit then download your new profile.  If you can’t see the Download button refresh the page:

Once you have downloaded the provisioning profile file double click it which will automatically import it into Xcode:

In Xcode, configure the Staff Manager Code Signing Identity to use the new Provisioning Profile:

Enable Staff Manager Entitlements

To use the iCloud App ID we’ve just created we need to enable some Entitlements. Select the Staff Manager Targets:

Scroll to the Entitlements section (down the bottom) then configure it as follows:

Configure Core Data for iCloud

Currently the Persistent Store (StaffManager.sqlite) is stored on the local device. Once configured for iCloud it will still reside locally yet in a special iCloud enabled directory.

After poring through many approaches for enabling iCloud + Core Data I’ve come up with some succinct code used to create the persistent store. For re-usability and clarity the following variables are used:

  • iCloudEnabledAppID is the full App ID (including the Team Prefix). You will need to change this to match the Team Prefix found in your own iOS Provisioning Portal.
  • dataFile is the name of the SQLite database store file.
  • iCloud is the URL to your apps iCloud root path.
  • dataDirectory is the name of the directory the database will be stored in. It should always end with .nosync
  • logsDirectory is the name of the directory the database change logs will be stored in.
  • iCloudData = iCloud + dataDirectory
  • iCloudLogs = iCloud + logsDirectory

The upcoming code is responsible for returning a NSPersistentStoreCoordinator. Most of it is for setting what/where data and logs will be stored. The options NSMutableDictionary is used to set some new important options:

  • NSPersistentStoreUbiquitousContentNameKey will become the iCloud root (based on App ID)
  • NSPersistentStoreUbiquitousContentURLKey will become the change logs area.

Delete everything within the persistentStoreCoordinator method of AppDelegate.m then paste in the following code:

    if((__persistentStoreCoordinator != nil)) {
        return __persistentStoreCoordinator;
    }
 
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];    
    NSPersistentStoreCoordinator *psc = __persistentStoreCoordinator;
 
    // Set up iCloud in another thread:
 
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // ** Note: if you adapt this code for your own use, you MUST change this variable:
        NSString *iCloudEnabledAppID = @"5KFJ75859U.Tim-Roadley.Staff-Manager";
 
        // ** Note: if you adapt this code for your own use, you should change this variable:        
        NSString *dataFileName = @"StaffManager.sqlite";
 
        // ** Note: For basic usage you shouldn't need to change anything else
 
        NSString *iCloudDataDirectoryName = @"Data.nosync";
        NSString *iCloudLogsDirectoryName = @"Logs";
        NSFileManager *fileManager = [NSFileManager defaultManager];        
        NSURL *localStore = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:dataFileName];
        NSURL *iCloud = [fileManager URLForUbiquityContainerIdentifier:nil];
 
        if (iCloud) {
 
            NSLog(@"iCloud is working");
 
            NSURL *iCloudLogsPath = [NSURL fileURLWithPath:[[iCloud path] stringByAppendingPathComponent:iCloudLogsDirectoryName]];
 
            NSLog(@"iCloudEnabledAppID = %@",iCloudEnabledAppID);
            NSLog(@"dataFileName = %@", dataFileName); 
            NSLog(@"iCloudDataDirectoryName = %@", iCloudDataDirectoryName);
            NSLog(@"iCloudLogsDirectoryName = %@", iCloudLogsDirectoryName);  
            NSLog(@"iCloud = %@", iCloud);
            NSLog(@"iCloudLogsPath = %@", iCloudLogsPath);
 
            if([fileManager fileExistsAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName]] == NO) {
                NSError *fileSystemError;
                [fileManager createDirectoryAtPath:[[iCloud path] stringByAppendingPathComponent:iCloudDataDirectoryName] 
                       withIntermediateDirectories:YES 
                                        attributes:nil 
                                             error:&fileSystemError];
                if(fileSystemError != nil) {
                    NSLog(@"Error creating database directory %@", fileSystemError);
                }
            }
 
            NSString *iCloudData = [[[iCloud path] 
                                     stringByAppendingPathComponent:iCloudDataDirectoryName] 
                                    stringByAppendingPathComponent:dataFileName];
 
            NSLog(@"iCloudData = %@", iCloudData);
 
            NSMutableDictionary *options = [NSMutableDictionary dictionary];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
            [options setObject:iCloudEnabledAppID            forKey:NSPersistentStoreUbiquitousContentNameKey];
            [options setObject:iCloudLogsPath                forKey:NSPersistentStoreUbiquitousContentURLKey];
 
            [psc lock];
 
            [psc addPersistentStoreWithType:NSSQLiteStoreType 
                              configuration:nil 
                                        URL:[NSURL fileURLWithPath:iCloudData] 
                                    options:options 
                                      error:nil];
 
            [psc unlock];
        }
        else {
            NSLog(@"iCloud is NOT working - using a local store");
            NSMutableDictionary *options = [NSMutableDictionary dictionary];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
            [options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
 
            [psc lock];
 
            [psc addPersistentStoreWithType:NSSQLiteStoreType 
                              configuration:nil 
                                        URL:localStore 
                                    options:options 
                                      error:nil];
            [psc unlock];
 
        }
 
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:@"SomethingChanged" object:self userInfo:nil];
        });
    });
 
    return __persistentStoreCoordinator;

Comment out the following code found in the didFinishLaunchingWithOptions method of AppDelegate.m :

    /*
    [self setupFetchedResultsController];
 
    if (![[self.fetchedResultsController fetchedObjects] count] > 0 ) {
        NSLog(@"!!!!! ~~> There's nothing in the database so defaults will be inserted");
        [self importCoreDataDefaultRoles];
    }
    else {
        NSLog(@"There's stuff in the database so skipping the import of default data");
    }
    */

Configure Notifications

When underlying data changes unfortunately the table views have no idea. To fix this we need to get them to watch for stuff changing notifications. First we need to configure the managedObjectContext so replace the managedObjectContext method in AppDelegate.m with the following two methods:

- (NSManagedObjectContext *)managedObjectContext {
 
    if (__managedObjectContext != nil) {
        return __managedObjectContext;
    }
 
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
 
    if (coordinator != nil) {
        NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
 
        [moc performBlockAndWait:^{
            [moc setPersistentStoreCoordinator: coordinator];
            [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(mergeChangesFrom_iCloud:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:coordinator];
        }];
        __managedObjectContext = moc;
    }
 
    return __managedObjectContext;
}
 
- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
 
	NSLog(@"Merging in changes from iCloud...");
 
    NSManagedObjectContext* moc = [self managedObjectContext];
 
    [moc performBlock:^{
 
        [moc mergeChangesFromContextDidSaveNotification:notification]; 
 
        NSNotification* refreshNotification = [NSNotification notificationWithName:@"SomethingChanged"
                                                                            object:self
                                                                          userInfo:[notification userInfo]];
 
        [[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
    }];
}

That code tells the Managed Object Context to post notifications when iCloud updates data. To use those notifications add the following method to RolesTVC.m and PersonsTVC.m:

- (void)reloadFetchedResults:(NSNotification*)note {
    NSLog(@"Underlying data changed ... refreshing!");
    [self performFetch];
}

Also add the following to the end of the viewDidLoad method of RolesTVC.m and PersonsTVC.m:

    // Refresh this view whenever data changes
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(reloadFetchedResults:)
                                                 name:@"SomethingChanged"
                                               object:[[UIApplication sharedApplication] delegate]];

Finally, add the following method to RolesTVC.m and PersonsTVC.m which cleans up a little:

- (void)viewDidUnload {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

That’s it!

Run the app on the iPad and iPhone at the same time. You should see changes made on one device reflected on the other pretty quickly.

There are a couple of things I’m not entirely happy with just yet and will work to resolve soon. These are:

  • Saving every keystroke for the purpose of updating the master split view isn’t fast.
  • Importing default data.

Hopefully you have enough information to get you started with iCloud anyway.

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer or making a small donation. I’m saving for a Retina Tutorial iPad!!

-Tim

Go to the Tutorials Index


 
84 Comments

Posted by on April 3, 2012 in iOS Tutorials

 

Tip – Exception Breakpoints

Introduction

I often receive e-mails asking questions around errors/crashes received when trying to adapt my tutorials. I thought it might help to show a quick tip which might help troubleshoot crashes.

Add an Exception Breakpoint

To add an Exception Breakpoint simply click + when inside the Xcode Breakpoint Navigator then select Add Exception Breakpoint:

Once you’ve done that just click Done:

Run your crashing app and instead of being thrown here…

…you would instead be thrown to a more likely source of the issue:

You can continue stepping through your code with these controls:

That at least should give you something slightly more informative to work with.

If you liked this tip or found something wrong with it please let me know!

-Tim

While you’re here why not check out the Tutorials Index to see if there’s something of interest to you ;-)


 
3 Comments

Posted by on March 26, 2012 in iOS Tutorials

 

Core Data Universal / iPad Storyboard

Introduction

The aim of this tutorial is to show how to upgrade a project to be universal so it supports both iPhone and iPad devices. The starting point will be the final project from the end of the 8-part Core Data Basics tutorial set. I would recommend going through that whole tutorial set so you’re familiar with the ‘Staff Manger’ project.

Prerequisites

Download the final project from the end of the Core Data Basics tutorial set. Extract and open it with Xcode.

Enable Universal Support

For an app to work on an iPad and iPhone you need to change the iOS Application Target Devices to Universal. Change this as follows:

Click File > New > File… to create a new Storyboard:

Ensure the Device Family is iPad:

Save the Storyboard as iPadStoryboard.storyboard then select it so you can see the blank canvas:

While I remember, set the Main Storyboard for iPad to iPadStoryboard on the Summary page of the Staff Manager Target as follows:

Cheating (a.k.a.) Re-using Work

Instead of repeating steps taken to make the iPhone storyboard we will use it as our starting point to create the iPad storyboard. Select everything on MainStoryboard.storyboard (Command-A), copy it (Command-C), then paste it (Command-V) on to the canvas of iPadStoryboard.storyboard.  You should now have a lovely confusing pile of view controllers:

Spread them out so the Person views are up top and Role views are down the bottom:

Configuring the iPad Storyboard

Now we have a good starting point it’s time to configure the storyboard for the iPad.  If you’ve ever used the Mail app on an iPad then you have used a UISplitViewController. A Split View Controller is a container (array) for two views – Master and Detail.  In the Mail app Master contains a list of emails and Detail shows the content of the email selected in the Master view.  In the Staff Manager app we will configure a Split View Controller.

  • PersonsTVC will be the Master view
  • PersonDetailTVC will be the Detail view

One of the fun parts of this tutorial will be getting the Master and Detail views talking via delegation. More about that later. For now, drag a Split View Controller to the Canvas and delete the two views that come with it. You should just have a Split View Controller sitting by itself:

Perform the following tasks which you should know how to do if you’ve followed the Core Data Basics tutorial set:

  • Delete the two Segue’s between the People and Person Detail Table Views.
  • Delete the Segue between the Tab Bar Controller and the People Navigation Controller.
  • Select the Person Detail TVC then click Editor > Embed In > Navigation Controller.

Now it’s time to connect the Split View Controller.  Hold down Control then drag a line from the Tab Bar Controller to the Split View Controller to create a Relationship Segue:

Hold down Control then drag a line from the Split View Controller to the People Navigation Controller to create a Master Relationship Segue:

Hold down Control then drag a line from the Split View Controller to the Person Detail Navigation Controller to create a Detail Relationship Segue:

iPadStoryboard.storyboard should now resemble the image below.  I’ve created gigantic labels so you know what’s what.  It’s important to understand this as we need to pass the managedObjectContext from the AppDelegate all the way through the hierarchy.

Reconfigure AppDelegate.m

If you run the app on an iPad it will crash.  This is because the iPad storyboard has a different view hierarchy so we need to differentiate between devices and pass managedObjectContext appropriately. Replace the entire contents of the didFinishLaunchingWithOptions method in the AppDelegate.m with the following:

 [self setupFetchedResultsController];
 
    if (![[self.fetchedResultsController fetchedObjects] count] > 0 ) {
        NSLog(@"!!!!! ~~> There's nothing in the database so defaults will be inserted");
        [self importCoreDataDefaultRoles];
    }
    else {
        NSLog(@"There's stuff in the database so skipping the import of default data");    
    }
 
    // TAB BAR
    UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
 
    // Override point for customization after application launch.
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
 
        NSLog(@"I'm an iPad");
 
        // *** Set up the Persons Split Views (2-Way Delegation & Pass Managed Object Context) *** //
 
        // Set up SPLIT VIEW for Persons
        UISplitViewController *splitViewController = [[tabBarController viewControllers] objectAtIndex:0];
 
        // Set up Split View MASTER view for Persons                
        UINavigationController *personsMasterTVCnav = [splitViewController.viewControllers objectAtIndex:0];
        splitViewController.delegate = (id)personsMasterTVCnav.topViewController;
        PersonsTVC *personsTVC = [[personsMasterTVCnav viewControllers] objectAtIndex:0]; 
        personsTVC.managedObjectContext = self.managedObjectContext; 
 
        // Set up Split View DETAIL view for Persons
        UINavigationController *personsDetailTVCnav = [splitViewController.viewControllers objectAtIndex:1];
        PersonDetailTVC *personDetailTVC = [personsDetailTVCnav.viewControllers objectAtIndex:0];
 
        // Set up MASTER and DETAIL delegation so we can send messages between views
        personsTVC.delegate = personDetailTVC;
        personDetailTVC.delegate = personsTVC;
 
        // *** Set up the Roles Views *** (Pass Managed Object Context)//
        UINavigationController *rolesTVCnav = [[tabBarController viewControllers] objectAtIndex:1];
        RolesTVC *rolesTVC = [[rolesTVCnav viewControllers] objectAtIndex:0];
        rolesTVC.managedObjectContext = self.managedObjectContext;
 
    } 
    else 
    {
        NSLog(@"I'm an iPhone or iPod Touch");
 
        // The Two Navigation Controllers attached to the Tab Bar (At Tab Bar Indexes 0 and 1)
        UINavigationController *personsTVCnav = [[tabBarController viewControllers] objectAtIndex:0];
        UINavigationController *rolesTVCnav = [[tabBarController viewControllers] objectAtIndex:1];
 
        // The Persons Table View Controller (First Nav Controller Index 0)
        PersonsTVC *personsTVC = [[personsTVCnav viewControllers] objectAtIndex:0];
        personsTVC.managedObjectContext = self.managedObjectContext;    
 
        // The Roles Table View Controller (Second Nav Controller Index 0)
        RolesTVC *rolesTVC = [[rolesTVCnav viewControllers] objectAtIndex:0];
        rolesTVC.managedObjectContext = self.managedObjectContext;
    }
 
    return YES;

All that code does is:

  1. Work out if the device is an iPad or iPhone/iPod.
  2. Create pointers to various controllers for the purpose of passing a managedObjectContext.

In the case of the iPad code you will have an error regarding the following line of code:

personsTVC.delegate = personDetailTVC;

This is error occurs because we haven’t configured the delegation that is required to let the Master and Detail views communicate.

Master Detail Communication via Delegation

If you look Apple’s documentation on the Split View Controller they state the following:

A split view controller does not provide any inherent support for managing the communication between the view controllers you assign to it. It is your responsibility to determine the best way to do that.

Oh joy, my responsibility! I think a reasonable way to help the Master and Detail views talk is by making them delegates of each other. When I first tried this I made the mistake of declaring the delegate protocols in PersonsTVC.h and PersonDetailTVC.h. Unfortunately this causes a ‘cannot find protocol declaration‘ circular reference error explained here. To get around this we actually need to move the protocol declarations to separate header files.  Create two new files with the following contents:

PersonsTVCDelegate.h

#import <Foundation/Foundation.h>
 
@class PersonsTVC;
@protocol PersonsTVCDelegate <NSObject>
- (void)personChangedOnMaster:(PersonsTVC *)controller;
@end

PersonDetailTVCDelegate.h

#import <Foundation/Foundation.h>
 
@class PersonDetailTVC;
@protocol PersonDetailTVCDelegate <NSObject>
- (void)theSaveButtonOnThePersonDetailTVCWasTapped:(PersonDetailTVC *)controller;
@end

Obviously remove the following code from PersonDetailTVC.h as it is now in PersonDetailTVCDelegate.h:

@class PersonDetailTVC;
@protocol PersonDetailTVCDelegate 
- (void)theSaveButtonOnThePersonDetailTVCWasTapped:(PersonDetailTVC *)controller;
@end

To use the new delegate headers add the following to PersonsTVC.h and PersonDetailTVC.h:

#import "PersonsTVCDelegate.h"
#import "PersonDetailTVCDelegate.h"

Add the following property to PersonsTVC.h:

@property (nonatomic, weak) id <PersonsTVCDelegate> delegate;

Of course add the following to PersonsTVC.m:

@synthesize delegate;

Edit the @interface in PersonDetailTVC.h so it conforms to the PersonsTVCDelegate protocol as follows:

@interface PersonDetailTVC : UITableViewController <PersonsTVCDelegate, PersonRoleTVCDelegate>

Now we have said we will conform to delegate protocols we need to actually conform to them.  That means adding the methods specified in the protocols to the classes that conform to them.

The PersonsTVC already conformed to the PersonDetailTVCDelegate protocol before this tutorial. You can tell this because the theSaveButtonOnThePersonDetailTVCWasTapped method already exists in PersonsTVC.m.

The PersonDetailTVC however needs to implement the personChangedOnMaster method required by the PersonsTVCDelegate protocol.  We communicate from the Master (PersonsTVC) to the Detail (PersonDetailTVC) via the personChangedOnMaster method.  Add the following to PersonDetailTVC.m:

- (void)personChangedOnMaster:(PersonsTVC *)controller {
 
    self.person = controller.selectedPerson;
    [self hideTableWhenPersonIsNotSelected];
    NSLog(@"PersonDetailTVC.m: personChangedOnMaster: %@ %@", self.person.firstname, self.person.surname);
    [self.navigationController popViewControllerAnimated:YES]; // Return detail view to root.
    [self viewDidLoad];
 
}

You will also have to add the following to the top of PersonDetailTVC.m so the class knows what a PersonsTVC is:

#import "PersonsTVC.h"

The personChangedOnMaster method refers to a hideTableWhenPersonIsNotSelected method so add it as follows:

- (void)hideTableWhenPersonIsNotSelected
{
    if (!self.person) {
        NSLog(@"No person means ill hide the stuff on this page.");
        [self.tableView setHidden:YES];
    }
    else {
        [self.tableView setHidden:NO];
        [self.tableView reloadData];
    }   
}

Delegation setup is complete. Before we can run the app we have to fix something.  The AppDelegate expects the splitViewController to be at object index 0 and the rolesTVCnav to be at index 1. Have a look at the Tab Bar Controller and if it looks like this:

Change it to this (by dragging):

Run the app in the iPad Simulator and you should be able to navigate between tabs:

Tidying Up

Fix the Tab Bar Icon

On the Split View Controller change the Bar Item Title to People and the icon to 111-user.png. This change will be reflected on the Tab Bar:

Add Autorotate

Add the following to the bottom of PersonsTVC.m, PersonDetailTVC.m, PersonRoleTVC, RolesTVC.m and RoleDetailTVC.m:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
    } else {
        return YES;
    }
}

Fix Add Person

On an iPad clicking the + should automatically add a Person. Add the following method to PersonsTVC.m:

- (IBAction)add:(id)sender
{
    // this is currently an iPad only method
 
    NSLog(@"Creating a new Person");
    Person *newPerson = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                      inManagedObjectContext:self.managedObjectContext];
    newPerson.firstname = @"New";
    newPerson.surname = @"Person";
 
    [self.managedObjectContext save:nil];
 
    [self performFetch];
 
    self.selectedPerson = newPerson;
 
    [delegate personChangedOnMaster:self];
}

Head back to iPadStoryboard.storyboard. Hold down Control then drag a line from the + to the yellow circle to connect it to the add method:

You should now be able to add people.

Change Detail when Master is Selected

To change the person that’s shown in the detail view depending on the person that is selected in the master view add the following to PersonsTVC.m:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    // iPad Only (Tell the detail view that we selected a row.)
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
 
        self.selectedPerson = [self.fetchedResultsController objectAtIndexPath:indexPath];
        NSLog(@"PersonssTVC: '%@ %@' selected on Master View",self.selectedPerson.firstname, self.selectedPerson.surname);
 
        [delegate personChangedOnMaster:self];
    }
}
 
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        // Tell the detail view that we selected a row.
        [delegate personChangedOnMaster:self];
    }
}

Update Master when Detail is Changed

Head back to iPadStoryboard.storyboard. Remove the Save button from the PersonDetailTVC as it doesn’t make sense for iPad. Widen the First and Surname UITextFields so they take up the whole Table View Cell of the Person Detail TVC. While you’re at it do the same for the Role Name UITextField on the Role Detail TVC.

From the Connections Inspector of the First UITextField drag a line from Editing Changed to the yellow circle to connect to the save method:

From the Connections Inspector of the Surname UITextField drag a line from Editing Changed to the yellow circle to connect to the save method:

If done correctly they should each have a Sent Event connection as follows:

This connection will cause the text fields to save to the database when text changes on the UITextFields. For this to be reflected on the Master view we need to add the following to the top of the theSaveButtonOnThePersonDetailTVCWasTapped method of PersonsTVC.m:

    // iPad Only (Tell the detail view that we selected a row.)
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
 
        NSLog(@"PersonsTVC: message recieved that the details was updated");
 
        [self performFetch];
        [self.tableView reloadData];
    }

The theSaveButtonOnThePersonDetailTVCWasTapped method name is now misleading as the save button isn’t ever pressed. I used such a descriptive name in previous tutorials to make it really clear what was going on. Rename the theSaveButtonOnThePersonDetailTVCWasTapped method to personDetailTVCDidSave in in PersonsTVC.m and also PersonDetailTVCDelegate.h.

In the save method of PersonDetailTVC.m replace this:

[self.delegate theSaveButtonOnThePersonDetailTVCWasTapped:self];

with this:

[self.delegate personDetailTVCDidSave:self];

As you edit the fields you can see the Master People table being updated in real time. To add this feature also for a Person’s Role , add the following to the end of the roleWasSelectedOnPersonRoleTVC method of PersonDetailTVC.m:

    [self.person setInRole:self.selectedRole];
    [self.person.managedObjectContext save:nil];
    [delegate personDetailTVCDidSave:self];

Handle Deletions

Add the following to the commitEditingStyle method of PersonsTVC.m inside the if statement (after endUpdates). This will ensure PersonDetail goes blank when there isn’t a selectedPerson due to a deletion.

[delegate personChangedOnMaster:self];

Hide Detail when Person Isn’t Selected

Add the following to the end of the viewDidLoad method of PersonDetailTVC.m:

[self hideTableWhenPersonIsNotSelected];

Prevent Cell Highlighting

Sometimes if you tap the edge of a cell before entering text on the PersonDetail view the cell highlights Blue and you can’t edit text:

To fix it add the following method to PersonDetailTVC.m:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    if (indexPath.section == 0) {
        switch (indexPath.row) {
            case 0:
                [self.personFirstnameTextField becomeFirstResponder];
                break;
            case 1:
                [self.personSurnameTextField becomeFirstResponder];
            default:
                break;
        }
    }
}

Add the following method to RoleDetailTVC.m:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    if (indexPath.section == 0) {
        switch (indexPath.row) {
            case 0:
                [self.roleNameTextField becomeFirstResponder];
                break;
            default:
                break;
        }
    }
}

That will ensure you can at least edit the UITextField when you accidentally tap the UITableViewCell.  To stop it from going blue edit the Selection to None for each UITableViewCell:

That’s it! Add some people and roles

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to the Tutorials Index


 
13 Comments

Posted by on March 24, 2012 in iOS Tutorials

 

Core Data Basics Part 8 – Cleanup


Introduction

As people have followed the previous parts of this tutorial set they’ve kindly left some feedback which I will address in this tutorial. This cleanup isn’t strictly Core Data related however it’s good practice constantly question why you’re doing things a certain way. If you like you could just skip to the end and grab a tidier version of the code.

Here are the issues we’ll address:

  • Sqlite spamming the log window.
  • Add Role and Add Person are so similar to Role Detail and Person Detail that they’re an inefficient way to set up the storyboard.  I’ll show how to remove Add Role and Add Person.  I’ll then enable Role Detail and Person Detail for adding a Role and a Person.
  • Views specific to a single Core Data object (like selected person or role) do not need the managedObjectContext property.
  • The keyboard currently gets in the way on the Role Detail and Person Detail views.  Tapping on the background of a table view should dismiss the keyboard.
  • There are unnecessary sections in the Person Detail view so I’ll clean things up so it looks more professional.
  • Tab Bar is boring with no icons.

Prerequisites

We follow on from Part 7, so download the project from the end of Part 7 then extract and open it with Xcode.

Disable SQLite Log Spam

Near the end of Part 2 we enabled SQLite debug.  This is pretty noisy in the log window so it’s time to turn it off again. Click Staff Manager > Edit Scheme…:

You will see the existing argument for -com.apple.CoreData.SQLDebug 1:

Un-tick it then click Ok:

If you test the app and look at the log window there should be much less logging:

Remove Add Role Table View

Select MainStoryboard.storyboard. Find and delete the Add Role Table View Controller then line up the remaining views as follows:

Delete AddRoleTVC.h and AddRoleTVC.m (Move to Trash) as we don’t need them anymore.

Remove RolesTVC as a delegate of AddRoleTVC by performing the following:

  • Delete #import “AddRoleTVC.h” from RolesTVC.h
  • Edit the RolesTVC.h @interface to remove it as a delegate of AddRoleTVCDelegate
  • Delete the theSaveButtonOnTheAddRoleTVCWasTapped delegate method from RolesTVC.m

Use Role Detail Table View to Add a Role

In order to now use the Role Detail Table View to add a Role we need to reconfigure the Add Role Segue section of the prepareForSegue method in RolesTVC.m.  It is currently configured to set RolesTVC as a delegate of AddRoleTVC:

NSLog(@"Setting RolesTVC as a delegate of AddRolesTVC");
AddRoleTVC *addRoleTVC = segue.destinationViewController;
addRoleTVC.delegate = self;
addRoleTVC.managedObjectContext = self.managedObjectContext;

We don’t need to do that anymore so replace the outdated code with the following:

NSLog(@"Setting RolesTVC as a delegate of RoleDetailTVC");
RoleDetailTVC *roleDetailTVC = segue.destinationViewController;
roleDetailTVC.delegate = self;
 
NSLog(@"Creating a new role and passing it to RoleDetailTVC");
Role *newRole = [NSEntityDescription insertNewObjectForEntityForName:@"Role"
                                                 inManagedObjectContext:self.managedObjectContext];
 
roleDetailTVC.role = newRole;

Head over to MainStoryboard.storyboard again as we need to set up the new segue. Hold down Control then drag a line from the + on the RolesTVC to the RolesDetailsTVC to create a Push Segue:

Set the Identifier of the Storyboard Segue you just created to Add Role Segue:

Run the app, change to the Roles tab and create a new role using the + button.  I made one called A New Role as you can see below:

Note: A new Role is created as soon as + is tapped even if the user does not save on the following screen. I don’t think it’s too big an issue as a Role can easily be deleted via a swipe.

Remove Add Person Table View

Everything we just did to remove AddRoleTVC we need to do to remove AddPersonTVC. If you have trouble doing that just download this more recent version of the project where I’ve done that work for you. The storyboard should now look like this:

If you run the app and try to look at the detail of a Person or Role the app will crash.  The reason it crashes is due to existing segues passing a null managedObjectContext. There really is no need to pass a managedObjectContext to a detail view when you’re already passing an object. You can access the managedObjectContext of an object directly from the object itself.

Remove managedObjectContext Property

For PersonDetailTVC.h, PersonRoleTVC.h, RoleDetailTVC.h and RolePickerTVCell.h remove the following line of code:

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

For PersonDetailTVC.m, PersonRoleTVC.m, RoleDetailTVC.m and RolePickerTVCell.m remove the following line of code:

@synthesize managedObjectContext = __managedObjectContext;

Anywhere the managedObjectContext property was used we will now have an error. We do actually need to get a managedObjectContext from somewhere for saving data. Lucky for us we can ask Core Data objects what their managedObjectContext is.

PersonDetailTVC – Use person.managedObjectContext

In the PersonDetailTVC.m save method there will now be an error.  Update the following line of code to fix it:

[self.person.managedObjectContext save:nil];  // write to database

In the PersonDetailTVC.m prepareForSegue method there will now be an error. Remove the following line of code to fix it:

personRoleTVC.managedObjectContext = self.managedObjectContext;

RoleDetailTVC.h – Use role.managedObjectContext

In the RoleDetailTVC.m save method there will now be an error. Update the following line of code to fix it:

[self.role.managedObjectContext save:nil];  // write to database

RolePickerTVCell.h – Use selectedRole.managedObjectContext

In the RolePickerTVCell.m setupFetchedResultsController method there will now be an error. Update the following two lines of code to fix it:

// 0 - Ensure you have a MOC
if (!self.selectedRole.managedObjectContext) {
        NSLog(@"RolePickerTVCell wasn't given a Managed Object Context ... This should NOT happen!");
    }

and

// 5 - Fetch it
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.selectedRole.managedObjectContext
sectionNameKeyPath:nil cacheName:nil];

PersonRoleTVC Use selectedPerson.managedObjectContext (new)

The PersonRoleTVC is used to select a Role for a Person.  This means that the selectedRole.manageObjectContext will be NULL until a Role is selected. This means we need to pass the selected Person to the PersonRoleTVC.  To do that we first need a selectedPerson property.

Add the following to PersonRoleTVC.h:

#import "Person.h"

Add the following property to PersonRoleTVC.h:

@property (strong, nonatomic) Person *selectedPerson;

You’ll need to of course add this to PersonRoleTVC.m:

@synthesize selectedPerson;

Now that a selectedPerson property exists on PersonRoleTVC we need to pass the selected Person during the segue. Add the following code to the PersonDetailTVC.m prepareForSegue method in the Person Role Segue section:

personRoleTVC.selectedPerson = self.person;

In the PersonRoleTVC.m setupFetchedResultsController method there will still be an error. Update the following line of code to fix it:

// 5 - Fetch it
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.selectedPerson.managedObjectContext sectionNameKeyPath:nil cacheName:nil];

We need to stop the RolesTVC.m prepareForSegue method from passing managedObjectContext to RoleDetailTVC during Role Detail Segue. Remove the following line of code to fix it:

roleDetailTVC.managedObjectContext = self.managedObjectContext;

Lastly we need to stop the PersonsTVC.m prepareForSegue method from passing managedObjectContext to PersonDetailTVC during the Person Detail Segue. Remove the following line of code to fix it:

personDetailTVC.managedObjectContext = self.managedObjectContext;

Run the app now and you should be able to do everything without any crashes.

Go Away Keyboard

Let’s say you’re adding a new person Bob Marley. You’ve entered his name yet the keyboard is still hanging around and in the way of the Role Table View Cell:

Any time someone taps outside a TextField I want the keyboard to go away. To do that I need a Tap Gesture Recogniser. At the end of the viewDidLoad method in PersonDetailTVC.m add the following code:

UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissKeyboard)];
[tgr setCancelsTouchesInView:NO];
[self.tableView addGestureRecognizer:tgr];

That Tap Gesture Recogniser (*tgr) action selects a dismissKeyboard method.  Add the dismissKeyboard method to PersonDetailTVC.m:

- (void)dismissKeyboard {
    [self.view endEditing:TRUE];
}

Run the app and whenever you tap anywhere outside the fields you’re editing then the keyboard should be dismissed.  Add the same code to RoleDetailTVC.m to ensure the keyboard is dismissed when tapping the background.

Remove Unnecessary Sections

The Person Detail Table View has some unnecessary sections. Open Up MainStoryboard.storyboard and change the Person Detail Table View as follows.  The light text is Placeholder Text on the Text Fields:

Add Tab Bar Icons

There a many free icons available to use on the Tab Bar. A google search led me to Glyphish.com who provide a pack of 200 free icons.  Download the whole lot if you like or just the two images we need.  Drag 37-suitcase.png and 111-user.png into the project.

Open MainStoryboard.storyboard then select the People Bar Item on the People Navigation Controller. Change the Bar Item image to 111-user.png:

Do the same for the Roles Bar Icon on the Roles Navigation Controller, setting the Bar Item Image to 37-suitcase.png. Run the app now and you should have a nicer Tab Bar:

That’s it, I hope it wasn’t too boring ;-)

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to the Tutorials Index


 
33 Comments

Posted by on March 17, 2012 in iOS Tutorials

 

Core Data Basics Part 7 – Search Bars


Introduction

If you’ve used an iPhone or iPad before you’ve probably used a Search Bar. This tutorial will show how to add a Search Bar to your project.  Again we will be using the Staff Manager project from previous parts of this tutorial set.  Specifically we will be adding the Search Bar to the Roles Table View of the Staff Manager project. Here’s what we’re aiming at:

Prerequisites

We follow on from Part 6, so download the project from the end of Part 6 then extract and open it with Xcode.

Adding a Search Bar

Select MainStoryboard.storyboard and find the Roles Table View Controller on the canvas. Drag a Search Bar and Search Display Controller on to the Roles Table View.  Be careful not to drag it into the prototype cell.

The Roles Table View should now look like this with a Search Bar:

Run the app and go to the Roles tab. Start typing in the Search Bar and you will notice the table view does not filter out data which doesn’t match the search criteria. Useless!

Making the Search Bar Useful

The data in the Table View should be filtered to show search results as a user types in the Search Bar. In order for this to work we need to set up the RolesTVC class to implement the UISearchDisplayDelegate and UISearchBarDelegate protocols. Update the following line in RolesTVC.h:

@interface RolesTVC : CoreDataTableViewController <AddRoleTVCDelegate, RoleDetailTVCDelegate, UISearchDisplayDelegate, UISearchBarDelegate>

 

To display a filtered subset of data we need to store the search results in an array. What better place than in a  searchResults NSMutableArray!  Add the following property to RolesTVC.h:

@property (nonatomic, retain) NSMutableArray *searchResults;

 

Of course, you will then need to add the following to RolesTVC.m:

@synthesize searchResults;

 

Also, create the following methods in RolesTVC.m.  They are responsible for initial setup and removal of the searchResults array:

- (void)viewDidLoad 
{
    self.searchResults = [NSMutableArray arrayWithCapacity:[[self.fetchedResultsController fetchedObjects] count]]; 
    [self.tableView reloadData];
}
 
- (void)viewDidUnload
{
	self.searchResults = nil;
}

 

So far in the tutorial series the CoreDataTableViewController has shielded us from needing to configure the numberOfRowsInSection method required by a TableView. This method simply tells the table how many rows to display.  The answer will be different depending on whether we are showing all data or only searchResults.  Add the following method to RolesTVC.m:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	if (tableView == self.searchDisplayController.searchResultsTableView)
	{
        return [self.searchResults count];
    }
	else
	{
        return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
    }
}

 

Likewise, the existing cellForRowAtIndexPath method also needs to differentiate between data from the searchResults and otherwise.  Change the cell configuration in the cellForRowAtIndexPath method within RolesTVC.m as follows:

    // Configure the cell...
    Role *role = nil;
 
	if (tableView == self.searchDisplayController.searchResultsTableView)
	{
        NSLog(@"Configuring cell to show search results");
        role = [self.searchResults objectAtIndex:indexPath.row];
    }
	else
	{
        NSLog(@"Configuring cell to show normal data");
        role = [self.fetchedResultsController objectAtIndexPath:indexPath];
    }

 

The next method is where most of the search filter magic happens. When you adapt the code to your own needs you will need to know what is happening here.  Here’s the overview:

  1. All searchResults are removed.
  2. A role object is created for every object fetched from Core Data.
  3. A role object is put into the searchResults array for all roles with a role name that matches searchText.

Add the following method to RolesTVC.m:

#pragma mark -
#pragma mark Content Filtering
 
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
	NSLog(@"Previous Search Results were removed.");
	[self.searchResults removeAllObjects]; 
 
	for (Role *role in [self.fetchedResultsController fetchedObjects])
	{
		if ([scope isEqualToString:@"All"] || [role.name isEqualToString:scope])
		{
			NSComparisonResult result = [role.name compare:searchText 
                                                   options:(NSCaseInsensitiveSearch|NSDiacriticInsensitiveSearch) 
                                                     range:NSMakeRange(0, [searchText length])];
            if (result == NSOrderedSame)
			{
                NSLog(@"Adding role.name '%@' to searchResults as it begins with search text '%@'", role.name, searchText);
				[self.searchResults addObject:role];
            }
		}
	}
}

Finally add these two additional small methods to RolesTVC.m:

#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods
 
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
    [self filterContentForSearchText:searchString scope:@"All"];
    return YES;
}
 
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption
{
    [self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:@"All"];
    return YES;
}

Note that I haven’t shown you how to configure search scopes or anything fancy. I didn’t want to show too much at once. If you’re comfortable with the basics, check out Apple’s TableSearch project to learn more.

Run the app now and you should be able to filter the Roles using the Search Bar:

_______

That’s it!

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to Part 8 or the Tutorials Index


 
43 Comments

Posted by on March 5, 2012 in iOS Tutorials

 

Core Data Basics Part 6 – Core Data UIPickerView


Introduction

In previous tutorials I’ve shown how to use a UITableView to select from existing values stored in Core Data. The example project showed how to select what Role a Person is in.  This tutorial will cover how to use a UIPickerView for the same purpose.  I think the Picker looks cooler and is just as practical:

As usual I’m writing this tutorial without a clue in the world of how to do what I’m teaching.  Luckily for me there are other tutorial writers out there!  After a lot of research I found this great tutorial then expanded on it to use Core Data. I hope the results are useful to you.

Prerequisites

We follow on from Part 5, so download the project from the end of Part 5 then extract and open it with Xcode.

CoreDataTableViewCell Class

Typically you would display values in a TableViewCell found within a TableView.  To change a value you would tap the TableViewCell then expect something to let you change that cell value.  In order to make TableViewCell show a Core Data Picker I’ve created a base subclass called CoreDataTableViewCell.  To use CoreDataTableViewCell we need to subclass it again in order to customise what it fetches from Core Data.

Start off by downloading, extracting and dragging the two files from CoreDataTableViewCell.zip into your project. Next create a new class by clicking File > New > New File

Call the class RolePickerTVCell and make it a subclass of UITableViewCell:

Replace all the code in RolePickerTVCell.h with the code below.  Note that RolePickerTVCell inherits from CoreDataTableViewCell:

#import <UIKit/UIKit.h>
#import "CoreDataTableViewCell.h"
#import "Role.h"
 
@class RolePickerTVCell;
 
@protocol RolePickerTVCellDelegate 
- (void)roleWasSelectedOnPicker:(Role*)role;
@end
 
@interface RolePickerTVCell : CoreDataTableViewCell 
 
@property (nonatomic, weak) id <RolePickerTVCellDelegate> delegate;
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, strong) Role *selectedRole;
 
@end

Replace all the code in RolePickerTVCell.m with the following:

#import "RolePickerTVCell.h"
#import "AppDelegate.h"
 
@implementation RolePickerTVCell
@synthesize managedObjectContext = __managedObjectContext;
@synthesize selectedRole = _selectedRole;
@synthesize delegate;
 
- (void)setupFetchedResultsController
{
    // 0 - Ensure you have a MOC
    if (!self.managedObjectContext) {
        NSLog(@"RolePickerTVCell wasn't given a Managed Object Context ... so it's going to go get one itself!");
        AppDelegate *ad = [[UIApplication sharedApplication] delegate];
        self.managedObjectContext = ad.managedObjectContext;
    }
 
	// 1 - Decide what Entity you want
	NSString *entityName = @"Role"; // Put your entity name here
	NSLog(@"RolePickerTVCell is Setting up a Fetched Results Controller for the Entity named %@", entityName);
 
	// 2 - Request that Entity
	NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
 
	// 3 - Filter it if you want
	//request.predicate = [NSPredicate predicateWithFormat:@"Person.name = Blah"];
 
	// 4 - Sort it if you want
	request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name"
																					 ascending:YES
																					  selector:@selector(localizedCaseInsensitiveCompare:)]];
	// 5 - Fetch it
	self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
																		managedObjectContext:self.managedObjectContext
																		  sectionNameKeyPath:nil
																				   cacheName:nil];
	[self.fetchedResultsController performFetch:nil];
 
    NSLog(@"The following roles were fetched for the Picker by RolePickerTVCell:");
    for (Role *fetchedRole in [self.fetchedResultsController fetchedObjects]) {
        NSLog(@"Role: %@", fetchedRole.name);        
    }
}
 
- (void)initalizeInputView {
 
    // Prepare the Data for the Picker
    [self setupFetchedResultsController];
 
	self.picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
	self.picker.showsSelectionIndicator = YES;
	self.picker.autoresizingMask = UIViewAutoresizingFlexibleHeight;
 
	if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
		UIViewController *popoverContent = [[UIViewController alloc] init];
		popoverContent.view = self.picker;
		popoverController = [[UIPopoverController alloc] initWithContentViewController:popoverContent];
		popoverController.delegate = self;
	}
}
 
- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
 
		[self initalizeInputView];
 
        self.picker.delegate = self;
		self.picker.dataSource = self;
    }
    return self;
}
 
- (void)done:(id)sender {
 
    NSLog(@"Passing back the selected '%@' Role to the delegate", self.selectedRole.name);
    [self.delegate roleWasSelectedOnPicker:self.selectedRole];
    [self resignFirstResponder];
}
 
#pragma mark -
#pragma mark UIPickerViewDataSource
 
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
	return [[self.fetchedResultsController fetchedObjects] count];
}
 
#pragma mark -
#pragma mark UIPickerViewDelegate
 
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
 
    // Display the Roles we've fetched on the picker
    Role *role = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
    return role.name;
}
 
- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
    // Configure the row height
	return 44.0f;
}
 
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
    // Configure the width of the picker wheel thing
	return 300.0f;
}
 
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
 
    Role *role = [[self.fetchedResultsController fetchedObjects] objectAtIndex:row];
    self.selectedRole = role;
    NSLog(@"The '%@' Role was selected using the picker", self.selectedRole.name); 
}
 
@end

RolePickerTVCell exists to help you select a role so a selectedRole property also exists for you to store a pointer to a role. The rest of the class methods have the following purposes:

  • setupFetchedResultsController is where you customise what you want to bring back to the Picker.
  • initalizeInputView calls setupFetchedResultsController then sets up some device specific settings for the picker.
  • initWithCoder is the first method called.  It simply calls initalizeInputView.
  • done is called when the done button on the picker is pressed.  This method passes the selectedRole to the delegate.
  • The remaining methods are there because RolePickerTVCell implements the UIPickerViewDataSource and UIPickerViewDelegate protocols.

Currently the Staff Manager app only lets you assign a role to a person via the Person Detail Table View.  For our example we will configure the Add Person Table View to use the picker for role selection. Click MainStoryboard.storyboard then select the Add Person Table View.  Edit the Add Person Table View as shown below, note that Table View Section – Role does not need a Text Field:

Select the Role Table View Cell on the Add Person Table View then set its Custom Class to RolePickerTVCell:

Run the app now and add a person with the ‘+’ button.  When you tap the Role cell the Core Data Role Picker should appear:

That’s almost great. If you tap done the role isn’t passed back to the Add Person Table View yet because AddPersonTVC isn’t a delegate of RolePickerTVCell.  You should be getting used to configuring delegates by now although they are a difficult concept to grasp sometimes.  Before we configure the delegate Control-drag a line from the Role Table View Cell to AddPersonTVC.h to create a new property called personRoleTVCell:

Now let’s set up the delegate.  Open up AddPersonTVC.h and add/edit the following lines of code:

#import "RolePickerTVCell.h"
@interface AddPersonTVC : UITableViewController <RolePickerTVCellDelegate>
@property (nonatomic, strong) Role *selectedRole;

Add the following to AddPersonTVC.m:

@synthesize selectedRole;
 
- (void)viewWillAppear:(BOOL)animated {
 
    personRoleTVCell.textLabel.text = @"";
    personRoleTVCell.delegate = self;
    personRoleTVCell.managedObjectContext = self.managedObjectContext;    
}
- (void)roleWasSelectedOnPicker:(Role *)role {
 
    self.selectedRole = role;
    personRoleTVCell.textLabel.text = self.selectedRole.name;
    NSLog(@"AddPersonTVC has set '%@' as the Selected Role", self.selectedRole.name);
}

Finally, add the following to the existing save method in AddPersonTVC.m just after person.surname = … :

person.inRole = selectedRole;

One thing I noticed is that the Person Detail Table View isn’t currently loading in the role associated with a person so edit the existing viewDidLoad method of the PersonDetailTVC to include these lines:

self.personRoleTableViewCell.textLabel.text = self.person.inRole.name;
self.selectedRole = self.person.inRole; // ensure null role doesn't get saved.

That’s it!  You should now be able to select a role for a new person using a picker!

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to Part 7 or the Tutorials Index


 
30 Comments

Posted by on February 26, 2012 in iOS Tutorials

 

Core Data Basics Part 5 – Preloading Data


Introduction

Pre-loading default data into Core Data can be as simple or difficult as you make it. Some factors that may or may not complicate the task are:

  • What format the data is currently in
  • How much data you need to import

The format of the existing data is the biggest issue.  If it’s in database format your import code will be different than if it is in xml format. If it’s just a small list of things in text format then you can probably skip the whole import business and copy/paste your data directly into your code. If you want to know how to parse XML you should read and apply principles covered in Game Template Part 3 – Game Data Persistence.  I’m going to focus on where and how to write data from code into Core Data.

Prerequisites

We follow on from Part 4, so download the project from the end of Part 4 then extract and open it with Xcode.

Inserting Default Data

To insert default data we need to detect when the default import is required. One way to do that is to to select a count of items within a particular entity we’re interested in.  The most appropriate place for this check is just after Core Data has been set up – i.e. when the Persistent Store & Object Model & Context are ready.  Typically this is in AppDelegate.m so insert the following code at the top of the existing didFinishLaunchingWithOptions method:

    [self setupFetchedResultsController];
 
    if (![[self.fetchedResultsController fetchedObjects] count] > 0 ) {
        NSLog(@"!!!!! ~~> There's nothing in the database so defaults will be inserted");
        [self importCoreDataDefaultRoles];
    }
    else {
        NSLog(@"There's stuff in the database so skipping the import of default data");
    }

Obviously you’re going to need the setupFetchedResultsController method so also add the following above the existing didFinishLaunchingWithOptions method. You should be familiar with setupFetchedResultsController as we have used it a few times already. I’ve customised it to fetch the Role entity:

- (void)setupFetchedResultsController
{
    // 1 - Decide what Entity you want
    NSString *entityName = @"Role"; // Put your entity name here
    NSLog(@"Setting up a Fetched Results Controller for the Entity named %@", entityName);
 
    // 2 - Request that Entity
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
 
    // 3 - Filter it if you want
    //request.predicate = [NSPredicate predicateWithFormat:@"Person.name = Blah"];
 
    // 4 - Sort it if you want
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name"
                                                                                     ascending:YES
                                                                                      selector:@selector(localizedCaseInsensitiveCompare:)]];
    // 5 - Fetch it
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
    [self.fetchedResultsController performFetch:nil];
}

The other thing you need now are methods to import all the defaults. In these methods you could set up a whole bunch of stuff to parse and import from DB/XML/whatever. Your data mapping is really up to you to customise as you need it. You’ll get the idea from this below code, which you should paste above the setupFetchedResultsController method:

- (void)insertRoleWithRoleName:(NSString *)roleName
{
    Role *role = [NSEntityDescription insertNewObjectForEntityForName:@"Role"
                                               inManagedObjectContext:self.managedObjectContext];
 
    role.name = roleName;
 
    [self.managedObjectContext save:nil];
}
 
- (void)importCoreDataDefaultRoles {
 
    NSLog(@"Importing Core Data Default Values for Roles...");
    [self insertRoleWithRoleName:@"C/C++ Developer"];
    [self insertRoleWithRoleName:@"Obj-C Developer"];
    [self insertRoleWithRoleName:@"Java Developer"];
    [self insertRoleWithRoleName:@"ASP.NET Developer"];
    [self insertRoleWithRoleName:@"Unix Engineer"];
    [self insertRoleWithRoleName:@"Windows Engineer"];
    [self insertRoleWithRoleName:@"Business Analyst"];
    [self insertRoleWithRoleName:@"Infrastructure Manager"];
    [self insertRoleWithRoleName:@"Project Manager"];
    [self insertRoleWithRoleName:@"Operations Manager"];
    [self insertRoleWithRoleName:@"Desktop Support Analyst"];
    [self insertRoleWithRoleName:@"Chief Information Officer"];
    NSLog(@"Importing Core Data Default Values for Roles Completed!");
}

Those methods we’ve just added will be giving you a bunch of errors so you’ll need to add the following to AppDelegate.h

#import <CoreData/CoreData.h>
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;

Also add the following to AppDelegate.m

@synthesize fetchedResultsController = __fetchedResultsController;

Testing the Import

If you’ve already run the Staff Manager app once (and have entered Roles into the Simulator) then you will need to click iOS Simulator > Reset Content and Settings to ensure that there is no data in the database.  Once you’ve done that run the app and you should see default roles when you select the Roles tab. Note also the log entries telling you that defaults were inserted.  They should only fire once!

That’s it for this tutorial.  I know it was a pretty short one however I wanted you to focus on just getting data  into Core Data from code.

Here’s the complete source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to Part 6 or the Tutorials Index


 
14 Comments

Posted by on February 21, 2012 in iOS Tutorials

 

Core Data Basics Part 4 – Relationships


Introduction

In this Part 4 of Core Data Basics I’ll demonstrate how to implement and use Relationships.  As we will be modifying an existing Core Data Model I will introduce Data Model Versioning.  The Tab Bar Controller will also be introduced to manage our growing Storyboard

Prerequisites

We follow on from Part 3 where we’ve already created a simple Storyboard app with delegation that Adds, Views, Edits and Deletes Core Data. Download the project from the end of Part 3 then extract and open it with Xcode.

Data Model Versioning

In Part 2 we created a simple Data Model that contained one entity called Role which contained a single attribute called name. To explain relationships I’ll need to update the Data Model. Remember this: Any time you intend to release an updated Data Model you should create a new version of the Data Model before you edit it.

To add a new version of the existing Data Model select Model.xcdatamodeld then click Editor > Add Model Version... Accept the default name of Model 2 then click Finish:

You will now have two versions of the Core Data Model so set the active version to Model 2 using File Inspector while Model.xcdatamodeld is selected:

The green tick should now be on Model 2:

To begin modifying the new version select Model 2.xcdatamodel then click Add Entity. Create a new Person entity with two attributes firstname and surname with a type of String:

Change Editor Style to Graph by toggling this little button:

You may need to drag the two entities apart to see them properly like this:

Configuring Relationships

To ask Core Data what Role a person is in (or what persons are in a role) we will need a relationship. Consider the following scenario:

A person (Tim Roadley) is in a role (Project Manager). On the flip side the role (Project Manager) is held by multiple people (Tim Roadley, Joe Blogs and Fred Random). You can see that those two points of view on the same relationship need different names – inRole and heldBy.  In addition to that it’s a one to many relationship as (Tim Roadley) can’t be in two roles at once… well, at least not in my model.  Create a relationship between the two entities by holding down Control and dragging a line between them.  Rename the newRelationship’s as follows:

Using Data Model Inspector edit heldBy so it is a To-Many Relationship:

You will now notice the double arrow indicating what direction the To-Many relationship is in:

Now that our Data Model has been updated we need to generate the Person NSManagedObject sub-class and regenerate the Role NSManagedObject sub-class. Select both the Person and Role entities, click Editor > Create NSManagedObject Subclass… then Create. Replace the existing Role files.

Inspect both Role and Person classes. There is the potential that one of them might not know about the other. To be on the safe side just regenerate both classes all over again making sure to overwrite the existing four files:

Run the app and it will crash with the error ‘The model used to open the store is incompatible with the one used to create the store‘ … awesome! We need to update the AppDelegate to put some extra options on the Persistent Store Coordinator.

Find this line of code in the persistentStoreCoordinator method of AppDelegate.m:

if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])

Change it to this:

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])

What that does is enable the automatic migration of old Data Models to the latest version! You can read more about it here.  Run the app again and it should not crash.

Re-using Hard Work

People Classes

We’ve already learned how to configure Add, View, Edit and Delete Role Table View Controllers in previous parts of the tutorial.  We need the same Table View Controllers for Person so why double up on work? Remember that:

  • RolesTVC forms a perfect template for PersonsTVC
  • AddRoleTVC forms a perfect template for AddPersonTVC
  • RoleDetailTVC forms a perfect template for PersonDetailTVC

We may as well leverage search and replace to create the Person classes.  Create a new PersonsTVC, AddPersonTVC and PersonDetailTVC class. It doesn’t matter how you create the classes as we replace their entire contents.  You should now have 6 new Person TVC files in the project.  You may want to start organising the folder structure a little too:

Time for some boring stuff:

  • Copy all the code from RolesTVC.h to PersonsTVC.h
  • Copy all the code from RolesTVC.m to PersonsTVC.m
  • Copy all the code from AddRoleTVC.h to AddPersonsTVC.h
  • Copy all the code from AddRoleTVC.m to AddPersonsTVC.m
  • Copy all the code from RoleDetailTVC.h to PersonDetailTVC.h
  • Copy all the code from RoleDetailTVC.m to PersonDetailTVC.m
  • Replace the word role with the word person in all of the new Person class files.
  • Replace the word Role with the word Person in all of the new Person class files.

Notice the lower/titlecase of role/Role and person/Person.  It’s critical you match case when doing the search and replace.  Just click the little magnifying glass so you can then tick the Match Case option:

After all that you should be left with a few errors to do with references to the name attribute in the Person entity which doesn’t exist. For each of those errors you can change name to firstname and make most of them go away.

As a part of that work change the sort descriptor in the setupFetchedResultsController method of PersonsTVC.m from name to firstname:

    // 4 - Sort it if you want
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"firstname" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]];

Comment out the following lines in AddPersonTVC.h:

//@property (strong, nonatomic) IBOutlet UITextField *personNameTextField;

Comment out the following lines in AddPersonTVC.m:

//@synthesize personNameTextField;
//[self setPersonNameTextField:nil];
//person.firstname = personNameTextField.text;

Comment out the following lines in PersonDetailTVC.h:

//@property (strong, nonatomic) IBOutlet UITextField *personNameTextField;

Comment out the following lines in PersonDetailTVC.m

//@synthesize personNameTextField;
//self.personNameTextField.text = self.person.name;
//[self setPersonNameTextField:nil];
//[self.person setName:personNameTextField.text];

Run the app to ensure all the errors are gone. Just in case you got a bit lost here’s a copy of the code so far.

People Views

Open MainStoryboard.storyboard and duplicate everything on it by pressing Command-A then Command-D. Move the copied stuff up the top so it looks like the picture below:

We will reconfigure the stuff up the top to become the People views. You should know how to do this next stuff by now, so, for the copied views only perform the following:

  • Rename Roles Navigation Item Title to People
  • Rename Add Role Navigation Item Title to Add Person
  • Rename Role Detail Navigation Item Title to Person Detail
  • Set the Reuse Identifier of the People Prototype Cell to Persons Cell
  • Rename Add Person Table View Section Header from Role Name to First Name
  • Copy/Paste the Table View Section - First Name into its containing Table View.
  • Rename copied Table View Section Header from First Name to Surname
  • Rename Person Detail Table View Section Header from Role Name to First Name
  • Copy/Paste the Table View Section - First Name into its containing Table View.
  • Rename copied Table View Section Header from First Name to Surname
  • Rename the Segue between People and Add Person to Add Person Segue
  • Rename the Segue between People and Person Detail to Person Detail Segue
  • Set the Class of the People Table View Controller to PersonsTVC
  • Set the Class of the Add Person Table View Controller to AddPersonTVC
  • Set the Class of the Person Detail Table View Controller to PersonDetailTVC
The Person views should new resemble this:
The First Name and Surname Text Fields need to be linked to the underlying classes of the views they’re found on (AddPersonTVC.h and PersonDetailTVC.h).  Use the same procedure used in Part 2 to link this up. As a reminder swap to Assistant Editor then ensure you’re viewing the correct underlying class (see image below for how to change). Hold Control and drag from each Text field to the to the respective class header, call the new properties personFirstnameTextField and personSurnameTextField. If you were successful you will have created two new properties with little grey dots to the left of them that indicate a connection to a view. Don’t forget you need to do this for both AddPersonTVC.h and PersonDetailTVC.h:
Add the following code to the save method of AddPersonTVC.m under the commented out lines:
    person.firstname = personFirstnameTextField.text;
    person.surname = personSurnameTextField.text;
Add the following code to the viewDidLoad method of PersonDetailTVC.m under the commented out lines:
    self.personFirstnameTextField.text = self.person.firstname;
    self.personSurnameTextField.text = self.person.surname;
Add the following code to the save method of PersonDetailTVC.m under the commented out lines:
    self.person.firstname = self.personFirstnameTextField.text;
    self.person.surname = self.personSurnameTextField.text;

Remove the old references to roleNameTextField from the Outlets of the Add Person and Person Details Table View Controllers:

Edit the // Configure the cell … code in cellForRowAtIndexPath method of PersonsTVC.m:

    // Configure the cell...
    Person *person = [self.fetchedResultsController objectAtIndexPath:indexPath];
    NSString *fullname = [NSString stringWithFormat:@"%@ %@", person.firstname, person.surname];
    cell.textLabel.text = fullname;
    //cell.textLabel.text = person.firstname;

Tab Bar Controllers

Now it’s time to bring it all together with a Tab Bar Controller so drag one on to the canvas to the left of the existing stuff. Delete the two views that come with it.  Hold down Control and drag a line to both existing Navigation controllers to create a viewControllers relationship Segue. Set the Tab Bar Controller as the initial view controller so it is the first view shown when the app launches (you may need to un-tick one of the Navigation Controllers as also being the initial view controller. At a very high level the canvas should look like this:
On each Navigation Controller double click the little icon to set the Title of each Bar Item.  Call one Roles and the other People.  If you look at the Tab Bar Controller it will now show these names automatically.  It’s important to note the order from left to right as 0,1 .. etc as we will be referring to this index in code soon:

One of the final things to do now is to configure the AppDelegate.m to pass the managed object context through to each of the view controllers’ managedObjectContext properties. Without this we will break Core Data. Ensure the following code is in AppDelegate.m:

#import "PersonsTVC.h"
#import "RolesTVC.h"

Edit the didFinishLaunchingWithOptions method of the AppDelegate.m as follows:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // The Tab Bar
    UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
 
    // The Two Navigation Controllers attached to the Tab Bar (At Tab Bar Indexes 0 and 1)
    UINavigationController *personsTVCnav = [[tabBarController viewControllers] objectAtIndex:0];
    UINavigationController *rolesTVCnav = [[tabBarController viewControllers] objectAtIndex:1];
 
    // The Persons Table View Controller (First Nav Controller Index 0)
    PersonsTVC *personsTVC = [[personsTVCnav viewControllers] objectAtIndex:0];
    personsTVC.managedObjectContext = self.managedObjectContext;    
 
    // The Roles Table View Controller (Second Nav Controller Index 0)
    RolesTVC *rolesTVC = [[rolesTVCnav viewControllers] objectAtIndex:0];
    rolesTVC.managedObjectContext = self.managedObjectContext;
 
    //NOTE: Be very careful to change these indexes if you change the tab order
 
    // The following stuff was commented out since we're using a Tab Bar Controller
    //UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
    //RolesTVC *controller = (RolesTVC *)navigationController.topViewController;
    //controller.managedObjectContext = self.managedObjectContext;
    return YES;
}

You should now be able to run the app and tab switch between Adding, Viewing, Editing and Deleting both People and Roles!

Relationships in Action

The goal of my example relationship is to show people’s roles on the People table. To show more than one data value on the People table, change the Table View Cell Style of the People Prototype Cells to Subtitle:

To associate a person with a role I was going to use a UIPickerView however found that it is quite an art to get one of those displaying properly with Core Data on all devices.  I will save that for another tutorial and for now settle on creating a Person’s Role Table View Controller backed by a new class called PersonRoleTVC.  PersonRoleTVC uses techniques you should by now be used to from previous parts of this tutorial such as passing a context and delegation.  Create a new PersonRoleTVC class then replace all the code in PersonRoleTVC.h with the following:

#import <UIKit/UIKit.h>
#import "Role.h"
#import "CoreDataTableViewController.h" // so we can fetch
 
@class PersonRoleTVC;
@protocol PersonRoleTVCDelegate
- (void)roleWasSelectedOnPersonRoleTVC:(PersonRoleTVC *)controller;
@end
 
@interface PersonRoleTVC : CoreDataTableViewController
@property (nonatomic, weak) id  delegate;
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) Role *selectedRole;
 
@end

Also replace all of the code in PersonRoleTVC.m with the following:

#import "PersonRoleTVC.h"
 
@implementation PersonRoleTVC
@synthesize fetchedResultsController = __fetchedResultsController;
@synthesize managedObjectContext = __managedObjectContext;
@synthesize delegate;
@synthesize selectedRole;
 
- (void)setupFetchedResultsController
{
    // 1 - Decide what Entity you want
    NSString *entityName = @"Role"; // Put your entity name here
    NSLog(@"Setting up a Fetched Results Controller for the Entity named %@", entityName);
 
    // 2 - Request that Entity
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
 
    // 3 - Filter it if you want
    //request.predicate = [NSPredicate predicateWithFormat:@"Person.name = Blah"];
 
    // 4 - Sort it if you want
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name"
                                                                                     ascending:YES
                                                                                      selector:@selector(localizedCaseInsensitiveCompare:)]];
    // 5 - Fetch it
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                                                        managedObjectContext:self.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
    [self performFetch];
}
 
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [self setupFetchedResultsController];
}
 
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
    self.selectedRole = [self.fetchedResultsController objectAtIndexPath:indexPath];
    NSLog(@"The PersonRoleTVC reports that the %@ role was selected", self.selectedRole.name);
    [self.delegate roleWasSelectedOnPersonRoleTVC:self];
 
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Person Role Cell";
 
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }
 
    // Configure the cell...
    Role *role = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = role.name;
 
    return cell;
}
 
@end

Open up MainStoryboard.storyboard then:

  • On the Person Detail Table View Controller copy/paste the Surname Table View Section into the same Table View.
  • Rename the copied Table View Section to Role then delete the UITextField within it.
  • Drag a new Table View Controller to the canvas to the right of the Person Detail Table View Controller.
  • Drag a Push Segue from the Role Table View Cell to the new table view and rename the Segue Identifier to Person Role Segue.
  • Rename the new Table View Controller Navigation Item Title to Person’s Role.
  • Set the Reuse Identifier of the Person’s Role Prototype Cells to Person Role Cell.
  • Set the Custom Class of the Person’s Role Table View Controller to PersonRoleTVC.

The storyboard should look a little like this:

Use Assistant Editor to Contol-drag a new property from the Role Table View Cell (from Person Detail table) to the PersonDetailTVC.h file.  Call the new property personRoleTableViewCell. This should create a new connected property for you automatically in PersonDetailTVC.h as follows:

@property (strong, nonatomic) IBOutlet UITableViewCell *personRoleTableViewCell;

PersonDetailTVC.h also needs to know what a Role is so it can store the selected role for the save.  To get that info back from PersonRoleTVC it needs to be its delegate too.  Edit/add to PersonDetailTVC.h as follows:

#import "Role.h"
#import "PersonRoleTVC.h"
@interface PersonDetailTVC : UITableViewController <PersonRoleTVCDelegate>
@property (strong, nonatomic) Role *selectedRole;

Of course You will now have to add a few things to PersonDetailTVC.m like this:

@synthesize selectedRole;

…and this (remove existing save method):

- (IBAction)save:(id)sender
{
    NSLog(@"Telling the PersonDetailTVC Delegate that Save was tapped on the PersonDetailTVC");
 
    self.person.firstname = self.personFirstnameTextField.text; // Set Firstname
    self.person.surname = self.personSurnameTextField.text; // Set Surname
    [self.person setInRole:self.selectedRole]; // Set Relationship!!!
    [self.managedObjectContext save:nil];  // write to database
    [self.delegate theSaveButtonOnThePersonDetailTVCWasTapped:self];
}
 
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender  // !
{
    if ([segue.identifier isEqualToString:@"Person Role Segue"])
	{
        NSLog(@"Setting PersonDetailTVC as a delegate of PersonRoleTVC");
        PersonRoleTVC *personRoleTVC = segue.destinationViewController;
        personRoleTVC.delegate = self;
        personRoleTVC.managedObjectContext = self.managedObjectContext;
	}
    else {
        NSLog(@"Unidentified Segue Attempted!");
    }
}
 
- (void)roleWasSelectedOnPersonRoleTVC:(PersonRoleTVC *)controller
{
    self.personRoleTableViewCell.textLabel.text = controller.selectedRole.name;
    self.selectedRole = controller.selectedRole;
    NSLog(@"PersonDetailTVC reports that the %@ role was selected on the PersonRoleTVC", controller.selectedRole.name);
    [controller.navigationController popViewControllerAnimated:YES];
}

The very last thing you have to do is add the following to the cellForRowAtIndexPath method of the PersonsTVC.m file:

cell.detailTextLabel.text = person.inRole.name;

Here’s the final result:

That’s it! Stay tuned for Part 5 where I’ll discuss pre-loading default data.

Here’s the source code so far

If you liked this tutorial or found something wrong with it please let me know!

If you want to support my work and have an iPad please consider purchasing iSoccer *wink*

-Tim

Go to Part 5 or the Tutorials Index


 
33 Comments

Posted by on February 19, 2012 in iOS Tutorials