Learning Core Data for iOS (2014)

7. Picker Views

In the middle of difficulty lies opportunity.

Albert Einstein

In Chapter 6, “Views,” you started building the Item view as the concept of passing managed objects around the application was demonstrated. Its primary focus introduced text fields as a means to edit managed objects. This chapter will explain how to create special text fields that have a Core Data–driven UIPickerView as the input view. The purpose of a picker text field is to make selecting from predefined values, such as a shop aisle, as easy and fast as possible.

Overview

Picker views make it easy for a user to relate managed objects. The item relationship properties unit.name, locationAtHome.storedIn, and locationAtShop.aisle are ideal candidates for setting with a picker view. The reason for this is that their potential values are relevant to many items. For example, a picker view can be used to define an item’s aisle, as shown in Figure 7.1. To make the picker view appear, a special UITextField subclass will be created. This subclass will present the picker view as an input view, which is the same type of view that the keyboard is usually shown in.

Image

Figure 7.1 A picker view


Note

To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter06.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name.


Introducing CoreDataPickerTF

To provide a picker view as an input view, a UITextField subclass called CoreDataPickerTF will be created. This new class will adopt the UIKeyInput, UIPickerViewDelegate, and UIPickerViewDataSource protocols. Just as CoreDataTVC underpins PrepareTVC and ShopTVC, so too willCoreDataPickerTF underpin other custom subclasses. These custom subclasses will be assigned to new text fields used to set an item’s unit, home location, or shop location.

When something is selected on the picker, the selected value needs to be reflected back to the relevant text field. For example, once you’ve selected a shop location from the picker, the name of the shop location should show up in the text field. For this to happen, the CoreDataPickerTF class will define a CoreDataPickerTFDelegate protocol. Any text fields that use a subclass of CoreDataPickerTF will need to adopt this protocol to be able to receive the updated string value from the picker. Listing 7.1 shows the selectedObjectID:changedForPickerTF method defined as a part of this new protocol in addition to some new properties.

Listing 7.1 CoreDataPickerTF.h


#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
@class CoreDataPickerTF;
@protocol CoreDataPickerTFDelegate <NSObject>
- (void)selectedObjectID:(NSManagedObjectID*)objectID
      changedForPickerTF:(CoreDataPickerTF*)pickerTF;
@optional
- (void)selectedObjectClearedForPickerTF:(CoreDataPickerTF*)pickerTF;
@end

@interface CoreDataPickerTF : UITextField
<UIKeyInput, UIPickerViewDelegate, UIPickerViewDataSource>
@property (nonatomic, weak) id <CoreDataPickerTFDelegate> pickerDelegate;
@property (nonatomic, strong) UIPickerView *picker;
@property (nonatomic, strong) NSArray *pickerData;
@property (nonatomic, strong) UIToolbar *toolbar;
@property (nonatomic) BOOL showToolbar;
@property (nonatomic, strong) NSManagedObjectID *selectedObjectID;
@end


There are six properties in CoreDataPickerTF, as shown in Listing 7.1:

image pickerDelegate is a reference to anything that’s set as a delegate of the CoreDataPickerTFDelegate protocol. Messages will be sent to delegates when a row is selected on the picker, telling them what was selected.

image picker is a reference to the picker view.

image pickerData is a reference to the array that’ll be populated with Core Data fetch results.

image toolbar is a reference to the toolbar found directly above the picker view. The toolbar will have a Clear button and a Done button. The Clear button will clear the current selection, and the Done button will close the picker view.

image showToolbar is a flag used to indicate whether the toolbar should be hidden. This property exists only to make it easy for you to hide the toolbar in your own applications, should you choose to reuse CoreDataPickerTF. Grocery Dude will always show the toolbar.

image selectedObjectID is a reference to the objectID of the selected managed object associated to an item. When the selection on the picker changes, this property will change to reflect the new selection.

Update Grocery Dude as follows to configure CoreDataPickerTF:

1. Select the Generic Core Data Classes group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to UITextField and Class name to CoreDataPickerTF and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in CoreDataPickerTF.h with the code from Listing 7.1.

The CoreDataPickerTF implementation has four sections:

image The DELEGATE+DATASOURCE: UIPickerView section will implement the protocol methods associated with populating the UIPickerView. It will also handle returning values to the pickerDelegate when an object is selected on the picker.

image The INTERACTION section will implement methods that handle new buttons available on the input accessory view, which clears the selection or hides the picker.

image The DATA section will implement methods that populate the NSArray with the data that drives the picker. It will also handle selecting an appropriate default row.

image The VIEW section will implement methods that create the picker view in an input view and the toolbar in the input accessory view. Redrawing due to device rotation will also be handled in this section.

DELEGATE+DATASOURCE: UIPickerView

This section has five methods that implement the required UIPickerViewDataSource protocol methods and some optional UIPickerViewDelegate protocol methods:

image The numberOfComponentsInPickerView method is used to specify how many columns the picker view has. In a picker view, a column is known as a component. Grocery Dude only needs one component so this method will be hard coded to return 1. You may wish to implement multicomponent core data picker views in your own projects, so this is where you would specify how many components you want. If you do that then please note you’ll need an array for each component.

image The numberOfRowsInComponent method is used to specify the total number of rows the picker view has. This value will vary depending on how many objects are in the pickerData array. The best way to provide this value is to return [pickerData count], which indicates how many objects are in the pickerData array.

image The widthForComponent method is used to specify how wide each component is and will be hard coded to return 280.

image The titleForRow method is used to specify what’s displayed in each row of each component. This method is similar to the cellForRowAtIndexPath method a table-view uses to populate each row.

image The didSelectRow method is used to handle what happens when a row is selected. The CoreDataPickerTF will by default send a message to any delegates, notifying them that the string value has changed. The CoreDataPickerTF subclasses should override this method.

Listing 7.2 shows the code involved.

Listing 7.2 CoreDataPickerTF.m: DELEGATE+DATASOURCE: UIPickerView


#import "CoreDataPickerTF.h"
@implementation CoreDataPickerTF
#define debug 1

#pragma mark - DELEGATE & DATASOURCE: UIPickerView
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView
numberOfRowsInComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    return [self.pickerData count];
}
- (CGFloat)pickerView:(UIPickerView *)pickerView
rowHeightForComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    return 44.0f;
}
- (CGFloat)pickerView:(UIPickerView *)pickerView
    widthForComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    return 280.0f;
}
- (NSString *)pickerView:(UIPickerView *)pickerView
             titleForRow:(NSInteger)row
            forComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    return [self.pickerData objectAtIndex:row];
}
- (void)pickerView:(UIPickerView *)pickerView
      didSelectRow:(NSInteger)row
       inComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    NSManagedObject *object = [self.pickerData objectAtIndex:row];
    [self.pickerDelegate selectedObjectID:object.objectID
                       changedForPickerTF:self];
}
@end


Update Grocery Dude as follows to implement the DELEGATE+DATASOURCE: UIPickerView section:

1. Replace all code in CoreDataPickerTF.m with the code from Listing 7.2.

Interaction

This section has two simple methods:

image The done method will be called when the Done button on the picker’s toolbar is tapped. It dismisses the picker.

image The clear method will be called when the Clear button on the picker’s toolbar is tapped. It sends a message to the picker delegate, telling it that the current selection should been cleared.

Listing 7.3 shows the code involved.

Listing 7.3 CoreDataPickerTF.m: INTERACTION


#pragma mark - INTERACTION
- (void)done {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self resignFirstResponder];
}
- (void)clear {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.pickerDelegate selectedObjectClearedForPickerTF:self];
    [self resignFirstResponder];
}


Update Grocery Dude as follows to implement the INTERACTION section:

1. Add the code from Listing 7.3 to the bottom of CoreDataPickerTF.m before @end.

Data

This section contains two methods, both of which are initially empty:

image The fetch method should be overridden in a CoreDataPickerTF subclass in order to populate self.pickerData with objects that the picker will display.

image The selectDefaultRow method should be overridden in a CoreDataPickerTF subclass. The overriding method should be configured to select a default row on the picker. Any existing association an item has to objects available on the picker will determine this default row selection. For example, if a “Milk” item object is already associated with the “Fridge” home location object, then the default picker selection will be Fridge.

Listing 7.4 shows the code involved. Because both methods must be overridden, these methods raise an exception if they’re called.

Listing 7.4 CoreDataPickerTF.m: DATA


#pragma mark - DATA
- (void)fetch {
    [NSException raise:NSInternalInconsistencyException format:
    @"You must override the '%@' method to provide data to the picker",
    NSStringFromSelector(_cmd)];
}
- (void)selectDefaultRow {
    [NSException raise:NSInternalInconsistencyException format:
    @"You must override the '%@' method to set the default picker row",
    NSStringFromSelector(_cmd)];
}


Update Grocery Dude as follows to implement the DATA section:

1. Add the code from Listing 7.4 to the bottom of CoreDataPickerTF.m before @end.

View

This section contains five methods:

image The createInputView method returns a UIView containing a picker.

image The createInputAccessoryView method returns a UIView containing a toolbar with Clear and Done buttons.

image The initWithFrame and initWithCoder methods set the inputView and inputAccessoryView properties inherited from UITextField using createInputView and createInputAccessoryView.

image The deviceDidRotate method is used to ensure the picker view is redrawn whenever the device is rotated.

Listing 7.5 shows the code involved.

Listing 7.5 CoreDataPickerTF.m: VIEW


#pragma mark - VIEW
- (UIView *)createInputView {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    self.picker = [[UIPickerView alloc] initWithFrame:CGRectZero];
    self.picker.showsSelectionIndicator = YES;
    self.picker.autoresizingMask = UIViewAutoresizingFlexibleHeight;
    self.picker.dataSource = self;
    self.picker.delegate = self;
    [self fetch];
    return self.picker;
}
- (UIView *)createInputAccessoryView {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    self.showToolbar = YES;
    if (!self.toolbar && self.showToolbar) {
        self.toolbar = [[UIToolbar alloc] init];
        self.toolbar.barStyle = UIBarStyleBlackTranslucent;
        self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleHeight;
        [self.toolbar sizeToFit];
        CGRect frame = self.toolbar.frame;
        frame.size.height = 44.0f;
        self.toolbar.frame = frame;

        UIBarButtonItem *clearBtn = [[UIBarButtonItem alloc]
                                     initWithTitle:@"Clear"
                                     style:UIBarButtonItemStyleBordered
                                     target:self
                                     action:@selector(clear)];
        UIBarButtonItem *spacer = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                             target:nil
                             action:nil];
        UIBarButtonItem *doneBtn =[[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                             target:self
                             action:@selector(done)];
        NSArray *array =
        [NSArray arrayWithObjects:clearBtn, spacer, doneBtn, nil];
        [self.toolbar setItems:array];
    }
    return self.toolbar;
}
-  (id)initWithFrame:(CGRect)aRect {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self = [super initWithFrame:aRect]) {

        self.inputView = [self createInputView];
        self.inputAccessoryView = [self createInputAccessoryView];
    }
    return self;
}
- (id)initWithCoder:(NSCoder*)aDecoder {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self = [super initWithCoder:aDecoder]) {
        self.inputView = [self createInputView];
        self.inputAccessoryView = [self createInputAccessoryView];
    }
    return self;
}
- (void)deviceDidRotate:(NSNotification*)notification {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.picker setNeedsLayout];
}


Update Grocery Dude as follows to implement the VIEW section:

1. Add the code from Listing 7.5 to the bottom of CoreDataPickerTF.m before @end.

CoreDataPickerTF is now ready to be subclassed to provide relevant picker views populated with data from Core Data. Three subclasses are required that will generate six new files in Grocery Dude. This calls for additional organization of the Xcode project.

Update Grocery Dude as follows to introduce a new group:

1. Right-click the existing Grocery Dude group and then select New Group.

2. Set the new group name to Grocery Dude Picker Text Fields.

Introducing UnitPickerTF

To choose an existing unit of measurement for an item, a picker is required. To present a picker, a text field with a custom CoreDataPickerTF subclass is required. A CoreDataPickerTF subclass implements three methods:

image The fetch method is responsible for constructing a Core Data fetch request and populating the self.pickerData array with Unit objects.

image The selectDefaultRow method will be configured to select a default row on the picker. Any existing association an item may have to the objects available on the picker will determine this default row selection. For example if a Bananas item object is already associated with a Kg unit object, then the default picker selection will be Kg. The selectedObjectID property is used to determine what object is currently selected by iterating through the self.pickerData array while looking for a name match. Note that currently it is possible for a unit to have the same name. This won’t be possible by the end of the book once de-duplication is introduced in Chapter 15, “Taming iCloud.”

image The titleForRow method will customize each row of the picker to show the name of the unit that the row represents.

Listing 7.6 shows the code involved.

Listing 7.6 UnitPickerTF.m


#import "UnitPickerTF.h"
#import "CoreDataHelper.h"
#import "AppDelegate.h"
#import "Unit.h"
@implementation UnitPickerTF
 #define debug 1
- (void)fetch {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSFetchRequest *request =
    [NSFetchRequest fetchRequestWithEntityName:@"Unit"];
    NSSortDescriptor *sort =
    [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
    [request setSortDescriptors:[NSArray arrayWithObject:sort]];
    [request setFetchBatchSize:50];
    NSError *error;
    self.pickerData = [cdh.context executeFetchRequest:request
                                                 error:&error];
    if (error) {
        NSLog(@"Error populating picker: %@, %@"
        , error, error.localizedDescription);}
    [self selectDefaultRow];
}
- (void)selectDefaultRow {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedObjectID && [self.pickerData count] > 0) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Unit *selectedObject =
       (Unit*)[cdh.context existingObjectWithID:self.selectedObjectID
                                          error:nil];
        [self.pickerData enumerateObjectsUsingBlock:^(
         Unit *unit, NSUInteger idx, BOOL *stop) {
            if ([unit.name compare:selectedObject.name] == NSOrderedSame) {
                [self.picker selectRow:idx inComponent:0 animated:NO];
                [self.pickerDelegate selectedObjectID:self.selectedObjectID
                                   changedForPickerTF:self];
                *stop = YES;
            }
        }];
    }
}
- (NSString *)pickerView:(UIPickerView *)pickerView
             titleForRow:(NSInteger)row
            forComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    Unit *unit = [self.pickerData objectAtIndex:row];
    return unit.name;
}
@end


Update Grocery Dude as follows to create the UnitPickerTF class:

1. Select the Grocery Dude Picker Text Fields group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to CoreDataPickerTF and Class name to UnitPickerTF and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in UnitPickerTF.m with the code from Listing 7.6.

Creating the Unit Picker

The UnitPickerTF class is ready to use, so a new text field is required on ItemVC.

Update Grocery Dude as follows to create the unit picker text field:

1. Select Main.storyboard.

2. Drag a Text Field anywhere onto the Scroll View of the Item view and configure it as follows using Attributes Inspector (Option+image+4):

image Set the Font to System Bold 17.0.

image Set the Text Alignment to Center.

image Set the Placeholder Text to Unit.

image Set the Border Style to the Line (represented by a rectangle).

image Set Background to Other > Crayons > Mercury (the second lightest gray crayon).

3. Configure the Height of the Text Field to 48 using Size Inspector (Option+image+5).

4. Arrange the Unit text field to the guides as shown in Figure 7.2, and then set its Custom Class to UnitPickerTF using Identity Inspector (Option+image+3).

Image

Figure 7.2 The unit picker text field

Connecting the Unit Picker

To reference the new picker text field in code, the outlets need to be connected. The Assistant Editor is used to achieve this.

Update Grocery Dude as follows to connect the unit picker text field to ItemVC.h:

1. Add #import "UnitPickerTF.h" to the top of ItemVC.h.

2. Select Main.storyboard.

3. Ensure the Item View Controller is selected and then show the Assistant Editor (Option+image+Return).

4. Set the Assistant Editor to Automatic > ItemVC.h if it isn’t set to this already.

5. Hold down Control and drag a line from the Unit Text Field to the bottom of ItemVC.h before @end. Set the Name of the new property to unitPickerTextField, as shown in Figure 7.3. Double-check that the Type is UnitPickerTF and the Storage is Strong.

Image

Figure 7.3 Connecting the unit picker text field

6. Show the Standard Editor (image+Return).

Configuring ItemVC for the Unit Picker

The Assistant Editor was used to create a reference to the unit picker text field through a new unitPickerTextField property. For picker text fields to send selected values back to the text field, the ItemVC needs to adopt the CoreDataPickerTFDelegate protocol.

Update Grocery Dude as follows:

1. Update ItemVC.h to adopt the CoreDataPickerTFDelegate protocol. For convenience, Listing 7.7 shows what ItemVC.h should look like once this update has been made.

Listing 7.7 ItemVC.h


#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
#import "UnitPickerTF.h"
@interface ItemVC : UIViewController
<UITextFieldDelegate,CoreDataPickerTFDelegate>
@property (strong, nonatomic) NSManagedObjectID *selectedItemID;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
@property (strong, nonatomic) IBOutlet UITextField *nameTextField;
@property (strong, nonatomic) IBOutlet UITextField *quantityTextField;
@property (strong, nonatomic) IBOutlet UnitPickerTF *unitPickerTextField;
@end


Because the ItemVC class now adopts both the CoreDataPickerTFDelegate and UITextFieldDelegate protocols, it needs to be set as a delegate of each for the picker text field. Being a delegate of the former will ensure selected values on the unit picker can be reflected in the unit text field. Being a delegate of the latter will ensure the unit text field moves into view when obscured by the unit picker. To achieve this functionality, the UITextFieldDelegate methods will set the selected text field as the activeField later in the chapter. Listing 7.8 shows the code involved in setting the delegates.

Listing 7.8 ItemVC.m: viewDidLoad


self.unitPickerTextField.delegate = self;
self.unitPickerTextField.pickerDelegate = self;


Update Grocery Dude as follows to configure the unit picker text field delegates:

1. Add the code from Listing 7.8 to the bottom of the viewDidLoad method of ItemVC.m.

As these updates are implemented, you may have noticed Xcode warning that the selectedObjectID:changedForPickerTF method hasn’t been implemented. This method is required of the CoreDataPickerTFDelegate protocol. It will be updated for each of the picker text fields that are implemented. This will all be done in a new PICKERS section in ItemVC.m.

The PICKERS section will have two methods:

image The selectedObjectID:changedForPickerTF method will be called whenever the picker sends this message to its delegates. This method will set the selected item unit value and update the unitPickerTextField.text value to reflect the unit name. This approach will also be used for the upcoming homeLocationPickerTextField and shopLocationPickerTextField.

image The selectedObjectClearedForPickerTF method will be called whenever the picker sends this message to its delegates. This method will clear the selected item unit value and any text in unitPickerTextField.text. This approach will also be used for the upcominghomeLocationPickerTextField and shopLocationPickerTextField.

Listing 7.9 shows the code involved.

Listing 7.9 ItemVC.m: PICKERS


#pragma mark - PICKERS
- (void)selectedObjectID:(NSManagedObjectID *)objectID
      changedForPickerTF:(CoreDataPickerTF *)pickerTF {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];;

        NSError *error;
        if (pickerTF == self.unitPickerTextField) {
            Unit *unit = (Unit*)[cdh.context existingObjectWithID:objectID
                                                            error:&error];
            item.unit = unit;
            self.unitPickerTextField.text = item.unit.name;
        }
        [self refreshInterface];
        if (error) {
            NSLog(@"Error selecting object on picker: %@, %@",
            error, error.localizedDescription);}
    }
}
- (void)selectedObjectClearedForPickerTF:(CoreDataPickerTF *)pickerTF {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];

        if (pickerTF == self.unitPickerTextField) {
            item.unit = nil;
            self.unitPickerTextField.text = @"";
        }
        [self refreshInterface];
    }
}


Update Grocery Dude as follows to implement the PICKERS section:

1. Add #import "Unit.h" to the top of ItemVC.m.

2. Add the code from Listing 7.9 to the bottom of ItemVC.m before @end.

To ensure that the unit picker can be refreshed with the latest data each time it is shown, the fetch method in UnitPickerTF.m needs to be exposed. This will allow it to be called from the textFieldDidBeginEditing method of ItemVC.m. In addition, the picker will need to reload after fetch is called. Listing 7.10 shows the code involved.

Listing 7.10 ItemVC.m: textFieldDidBeginEditing


if (textField == _unitPickerTextField && _unitPickerTextField.picker) {
    [_unitPickerTextField fetch];
    [_unitPickerTextField.picker reloadAllComponents];
}


Update Grocery Dude as follows to ensure the unit picker always has the latest data:

1. Add the following code to UnitPickerTF.h before @end:

- (void)fetch;

2. Add the code from Listing 7.10 to the bottom of the textFieldDidBeginEditing method of ItemVC.m.

The final touch required to the unit picker text field is to ensure that its text value is populated with the current unit name whenever the view appears. The viewWillAppear method already has a call to refreshInterface, so this is an ideal place to set the unit picker text field text value. In addition, the item’s associated unit should be set as the selected unit on the picker using its objectID. Listing 7.11 shows the code involved.

Listing 7.11 ItemVC.m: refreshInterface


- (void)refreshInterface {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];
        self.nameTextField.text = item.name;
        self.quantityTextField.text = item.quantity.stringValue;
        self.unitPickerTextField.text = item.unit.name;
        self.unitPickerTextField.selectedObjectID = item.unit.objectID;
    }
}


Update Grocery Dude as follows to ensure the unitPickerTextField displays the appropriate unit name:

1. Replace the refreshInterface method of ItemVC.m with the method from Listing 7.11.

2. Run the application. If there are no items or units in the persistent store, create some using the add/edit views created in the previous chapter. Once there are items and units in the persistent store, test setting an item’s unit using the picker. The expected result is shown in Figure 7.4.

Image

Figure 7.4 The unit picker text field in action

The unit picker is now completely operational, so it’s time to perform a similar process to implement the home and shop location pickers.

Introducing LocationAtHomePickerTF

A picker is required to set the location an item is stored in at home. The same technique used to create the unit picker will be applied to create the home location picker. Again, the fetch, selectDefaultRow, and titleForRow methods will be implemented in a CoreDataPickerTF subclass.

Listing 7.12 shows the code involved.

Listing 7.12 LocationAtHomePickerTF.m


#import "LocationAtHomePickerTF.h"
#import "CoreDataHelper.h"
#import "AppDelegate.h"
#import "LocationAtHome.h"
@implementation LocationAtHomePickerTF
#define debug 1
- (void)fetch {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSFetchRequest *request =
    [NSFetchRequest fetchRequestWithEntityName:@"LocationAtHome"];
    NSSortDescriptor *sort =
    [NSSortDescriptor sortDescriptorWithKey:@"storedIn" ascending:YES];
    [request setSortDescriptors:[NSArray arrayWithObject:sort]];
    [request setFetchBatchSize:50];
    NSError *error;
    self.pickerData = [cdh.context executeFetchRequest:request
                                                 error:&error];
    if (error) {
        NSLog(@"Error populating picker: %@, %@",
        error, error.localizedDescription);
    }
    [self selectDefaultRow];
}
- (void)selectDefaultRow {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedObjectID && [self.pickerData count] > 0) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        LocationAtHome *selectedObject = (LocationAtHome*)[cdh.context
                                existingObjectWithID:self.selectedObjectID
                                               error:nil];
        [self.pickerData enumerateObjectsUsingBlock:^(
            LocationAtHome *locationAtHome, NSUInteger idx, BOOL *stop) {
            if ([locationAtHome.storedIn compare:selectedObject.storedIn]
                 == NSOrderedSame) {
                [self.picker selectRow:idx inComponent:0 animated:NO];
                [self.pickerDelegate selectedObjectID:self.selectedObjectID
                                   changedForPickerTF:self];
                *stop = YES;
            }
        }];
    }
}
- (NSString *)pickerView:(UIPickerView *)pickerView
             titleForRow:(NSInteger)row
            forComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    LocationAtHome *locationAtHome = [self.pickerData objectAtIndex:row];
    return locationAtHome.storedIn;
}
@end


Update Grocery Dude as follows to create the LocationAtHomePickerTF class:

1. Select the Grocery Dude Picker Text Fields group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to CoreDataPickerTF and Class name to LocationAtHomePickerTF and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in LocationAtHomePickerTF.m with the code from Listing 7.12.

Because the code structure is so similar, it makes sense to create the equivalent code for the shop location picker now, too.

Introducing LocationAtShopPickerTF

A picker is required to set the location where an item is stored in a shop. Again, the fetch, selectDefaultRow, and titleForRow methods will be implemented in a CoreDataPickerTF subclass.

Listing 7.13 shows the code involved.

Listing 7.13 LocationAtShopPickerTF.m


#import "LocationAtShopPickerTF.h"
#import "CoreDataHelper.h"
#import "AppDelegate.h"
#import "LocationAtShop.h"
@implementation LocationAtShopPickerTF
#define debug 1
- (void)fetch {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSFetchRequest *request =
    [NSFetchRequest fetchRequestWithEntityName:@"LocationAtShop"];
    NSSortDescriptor *sort =
    [NSSortDescriptor sortDescriptorWithKey:@"aisle" ascending:YES];
    [request setSortDescriptors:[NSArray arrayWithObject:sort]];
    [request setFetchBatchSize:50];
    NSError *error;
    self.pickerData = [cdh.context executeFetchRequest:request
                                                 error:&error];
    if (error) {
        NSLog(@"Error populating picker: %@, %@",
        error, error.localizedDescription);
    }
    [self selectDefaultRow];
}
- (void)selectDefaultRow {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedObjectID && [self.pickerData count] > 0) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        LocationAtShop *selectedObject = (LocationAtShop*)[cdh.context
                                existingObjectWithID:self.selectedObjectID
                                               error:nil];
        [self.pickerData enumerateObjectsUsingBlock:^(
            LocationAtShop *locationAtShop, NSUInteger idx, BOOL *stop) {
            if ([locationAtShop.aisle compare:selectedObject.aisle]
                == NSOrderedSame) {
                [self.picker selectRow:idx inComponent:0 animated:NO];
                [self.pickerDelegate selectedObjectID:self.selectedObjectID
                                   changedForPickerTF:self];
                *stop = YES;
            }
        }];
    }
}
- (NSString *)pickerView:(UIPickerView *)pickerView
             titleForRow:(NSInteger)row
            forComponent:(NSInteger)component {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    LocationAtShop *locationAtShop = [self.pickerData objectAtIndex:row];
    return locationAtShop.aisle;
}
@end


Update Grocery Dude as follows to create the LocationAtShopPickerTF class:

1. Select the Grocery Dude Picker Text Fields group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to CoreDataPickerTF and Class name to LocationAtShopPickerTF and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in LocationAtShopPickerTF.m with the code from Listing 7.13.

Creating the Location Pickers

The LocationAtHomePickerTF and LocationAtShopPickerTF classes are ready to use, so two new text fields are required on ItemVC.

Update Grocery Dude as follows to create the home and shop location picker text fields:

1. Select Main.storyboard.

2. Drag two Text Fields anywhere onto the Scroll View of the Item view and configure them as follows using Attributes Inspector (Option+image+4):

image Set the Font of both new text fields to System Bold 17.0.

image Set the Text Alignment of both new text fields to Center.

image Set the Border Style of both new text fields to Line (represented by a rectangle).

image Set Background to Other > Crayons > Mercury (the second lightest gray crayon).

image Set the Placeholder Text of one of the text fields to Location at Home and the other to Location at Shop.

3. Set the Custom Class of the Location at Home text field to LocationAtHomePickerTF and the Custom Class of the Location at Shop text field to LocationAtShopPickerTF using Identity Inspector (Option+image+3).

4. Configure the Height of both new text fields to 48 using Size Inspector (Option+image+5).

5. Arrange the text fields to the guides as shown in Figure 7.5. Then click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints in ItemVC.

Image

Figure 7.5 The home location and shop location picker text fields

Connecting the Location Pickers

To reference the new picker text fields in code, the outlets need to be connected. The Assistant Editor is used to achieve this.

Update Grocery Dude as follows to connect the home and shop location picker text fields to ItemVC.h:

1. Add #import "LocationAtHomePickerTF.h" to the top of ItemVC.h.

2. Add #import "LocationAtShopPickerTF.h" to the top of ItemVC.h.

3. Select Main.storyboard.

4. Ensure the Item View Controller is selected.

5. Show the Assistant Editor (Option+image+Return).

6. Set the Assistant Editor to Automatic > ItemVC.h if it isn’t set to this already.

7. Hold down Control and drag a line from the Location at Home text field to ItemVC.h before @end. Set the Name of the new property to homeLocationPickerTextField. Double-check the Type is LocationAtHomePickerTF and the Storage is Strong.

8. Hold down Control and drag a line from the Location at Shop text field to ItemVC.h before @end. Set the Name of the new property to shopLocationPickerTextField. Double-check the Type is LocationAtShopPickerTF and the Storage is Strong.

9. Show the Standard Editor (image+Return).

When you examine ItemVC.h, there should now be filled-in circles next to each of the connected picker text fields. The expected result is shown in Figure 7.6.

Image

Figure 7.6 Connected picker text fields

Configuring ItemVC for the Location Pickers

To ensure the home and shop location pickers are refreshed with the latest data like the unit picker, the respective fetch methods need to be exposed. Likewise, the fetch and reloadAllComponents call will need to be implemented, too. Listing 7.14 shows the new code involved in bold.

Listing 7.14 ItemVC.m: textFieldDidBeginEditing


- (void)textFieldDidBeginEditing:(UITextField *)textField {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (textField == self.nameTextField) {

        if ([self.nameTextField.text isEqualToString:@"New Item"]) {
            self.nameTextField.text = @"";
        }
    }
    if (textField == _unitPickerTextField && _unitPickerTextField.picker) {
        [_unitPickerTextField fetch];
        [_unitPickerTextField.picker reloadAllComponents];
    } else if (textField == _homeLocationPickerTextField &&
               _homeLocationPickerTextField.picker) {
        [_homeLocationPickerTextField fetch];
        [_homeLocationPickerTextField.picker reloadAllComponents];
    } else if (textField == _shopLocationPickerTextField &&
               _shopLocationPickerTextField.picker) {
        [_shopLocationPickerTextField fetch];
        [_shopLocationPickerTextField.picker reloadAllComponents];
    }
}


Update Grocery Dude as follows to ensure the home and show location pickers always have the latest data:

1. Add the following code to the bottom of LocationAtHomePickerTF.h and LocationAtShopPickerTF.h before @end:

- (void)fetch;

2. Replace the existing textFieldDidBeginEditing method in ItemVC.m with the code from Listing 7.14.

The ItemVC also needs to be set as a delegate of homeLocationPickerTextField and shopLocationPickerTextField in the same way as with unitPickerTextField. Listing 7.15 shows the code involved in configuring these delegates.

Listing 7.15 ItemVC.m: viewDidLoad


self.homeLocationPickerTextField.delegate = self;
self.homeLocationPickerTextField.pickerDelegate = self;
self.shopLocationPickerTextField.delegate = self;
self.shopLocationPickerTextField.pickerDelegate = self;


Update Grocery Dude as follows to configure the home and shop location picker and text field delegates:

1. Add the code from Listing 7.15 to the bottom of the viewDidLoad method of ItemVC.m.

In addition, the refreshInterface method needs to be updated to cater for the home and shop location picker text fields. Listing 7.16 shows the code involved.

Listing 7.16 ItemVC.m: refreshInterface


- (void)refreshInterface {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];
        self.nameTextField.text = item.name;
        self.quantityTextField.text = item.quantity.stringValue;
        self.unitPickerTextField.text = item.unit.name;
        self.unitPickerTextField.selectedObjectID = item.unit.objectID;
        self.homeLocationPickerTextField.text =
            item.locationAtHome.storedIn;
        self.homeLocationPickerTextField.selectedObjectID =
            item.locationAtHome.objectID;
        self.shopLocationPickerTextField.text =
            item.locationAtShop.aisle;
        self.shopLocationPickerTextField.selectedObjectID =
            item.locationAtShop.objectID;
    }
}


Update Grocery Dude as follows to ensure the home and shop location text fields display the appropriate information:

1. Replace the refreshInterface method of ItemVC.m with the method from Listing 7.16.

The final touches involve updating the PICKERS section methods in ItemVC.m to cater for the home and shop location pickers. The selectedObjectID:changedForPickerTF method in ItemVC.m has an existing if/else statement used to react to the appropriate picker when a delegate method is called. Listing 7.17 shows the new code in bold.

Listing 7.17 ItemVC.m: selectedObjectID:changedForPickerTF


- (void)selectedObjectID:(NSManagedObjectID *)objectID
      changedForPickerTF:(CoresDataPickerTF *)pickerTF {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];;
        NSError *error;
        if (pickerTF == self.unitPickerTextField) {
            Unit *unit = (Unit*)[cdh.context existingObjectWithID:objectID
                                                            error:&error];
            item.unit = unit;
            self.unitPickerTextField.text = item.unit.name;
        }
        else if (pickerTF == self.homeLocationPickerTextField) {
            LocationAtHome *locationAtHome =
            (LocationAtHome*)[cdh.context existingObjectWithID:objectID
                                                         error:&error];
            item.locationAtHome = locationAtHome;
            self.homeLocationPickerTextField.text =
            item.locationAtHome.storedIn;
        }
        else if (pickerTF == self.shopLocationPickerTextField) {
            LocationAtShop *locationAtShop =
            (LocationAtShop*)[cdh.context existingObjectWithID:objectID
                                                         error:&error];
            item.locationAtShop = locationAtShop;
            self.shopLocationPickerTextField.text =
            item.locationAtShop.aisle;
        }
        [self refreshInterface];
        if (error) {
            NSLog(@"Error selecting object on picker: %@, %@",
            error, error.localizedDescription);
        }
    }
}


Update Grocery Dude as follows to update the first PICKER section method:

1. Replace the selectedObjectID:changedForPickerTF method in ItemVC.m with the code from Listing 7.17.

The same approach will be used to update selectedObjectClearedForPickerTF in ItemVC.m. This method nils out an item’s home or shop locations, which will force the “..Unknown..” object to be used instead. Listing 7.18 shows the new code in bold.

Listing 7.18 ItemVC.m: selectedObjectClearedForPickerTF


- (void)selectedObjectClearedForPickerTF:(CoreDataPickerTF *)pickerTF {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID
                                           error:nil];

        if (pickerTF == self.unitPickerTextField) {
            item.unit = nil;
            self.unitPickerTextField.text = @"";
        }
        else if (pickerTF == self.homeLocationPickerTextField) {
            item.LocationAtHome = nil;
            self.homeLocationPickerTextField.text = @"";
        }
        else if (pickerTF == self.shopLocationPickerTextField) {
            item.LocationAtShop = nil;
            self.shopLocationPickerTextField.text = @"";
        }
        [self refreshInterface];
    }
}


Update Grocery Dude as follows to update the second PICKER section method:

1. Replace the selectedObjectClearedForPickerTF method in ItemVC.m with the code from Listing 7.18.

All picker views are now completely configured. Run the application and create some units, home locations, and shop locations.

Picker-Avoiding Text Field

When a picker is shown, there’s less room to display its related text field. This means it’s likely that the related text field (referred to as the active text field) will become hidden behind its picker. To keep the active text field visible on the Item view, the scroll view needs to be resized in response to the keyboard being shown or hidden. Once the scroll view is resized, the active text field can be made visible using the scrollRectToVisible method of UIScrollView. The following two methods will be implemented in preparation for this functionality:

image The keyboardDidShow method finds the top of the keyboard input view where the picker will be located and resizes the scrollView. The offset will differ depending on the current orientation. Once the scrollView frame size matches the remaining visible area, the active text field can be brought into view.

image The keyboardWillHide method does the same thing as keyboardWillShow; it just makes the scrollView bigger instead of smaller.

Listing 7.19 shows the code involved.

Listing 7.19 ItemVC.m: INTERACTION


- (void)keyboardDidShow:(NSNotification *)n {

    // Find top of keyboard input view (i.e. picker)
    CGRect keyboardRect =
    [[[n userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
    CGFloat keyboardTop = keyboardRect.origin.y;

    // Resize scroll view
    CGRect newScrollViewFrame =
    CGRectMake(0, 0, self.view.bounds.size.width, keyboardTop);
    newScrollViewFrame.size.height = keyboardTop - self.view.bounds.origin.y;
    [self.scrollView setFrame:newScrollViewFrame];

    // Scroll to the active Text-Field
    [self.scrollView scrollRectToVisible:self.activeField.frame animated:YES];
}
- (void)keyboardWillHide:(NSNotification *)n {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CGRect defaultFrame =
    CGRectMake(self.scrollView.frame.origin.x,
               self.scrollView.frame.origin.y,
               self.view.frame.size.width,
               self.view.frame.size.height);

    // Reset Scrollview to the same size as the containing view
    [self.scrollView setFrame:defaultFrame];

    // Scroll to the top again
    [self.scrollView scrollRectToVisible:self.nameTextField.frame
                                animated:YES];
}


Update Grocery Dude as follows to add to the INTERACTION section:

1. Add the following property to ItemVC.h before @end. This property will be used to store a reference to the active text field:

@property (strong, nonatomic) IBOutlet UITextField *activeField;

2. Add the code from Listing 7.19 to the bottom of the INTERACTION section of ItemVC.m.

3. Add _activeField = textField; to the bottom of the textFieldDidBeginEditing method of ItemVC.m.

4. Add _activeField = nil; to the bottom of the textFieldDidEndEditing method of ItemVC.m.

The next step is to ensure the methods from Listing 7.19 are called when the keyboard is shown or hidden. This is achieved by observing UIKeyboardDidShowNotification and UIKeyboardWillHideNotification. The code involved is shown in Listing 7.20.

Listing 7.20 ItemVC.m: viewWillAppear


// Register for keyboard notifications while the view is visible.
[[NSNotificationCenter defaultCenter] addObserver:self
                                 selector:@selector(keyboardDidShow:)
                                     name:UIKeyboardDidShowNotification
                                   object:self.view.window];
[[NSNotificationCenter defaultCenter] addObserver:self
                                 selector:@selector(keyboardWillHide:)
                                     name:UIKeyboardWillHideNotification
                                   object:self.view.window];


Update Grocery Dude as follows to observe and respond to keyboard notifications:

1. Add the code from Listing 7.20 to the top of the viewWillAppear method of ItemVC.m.

There’s no need to continue observing keyboard notifications when the Item view is not onscreen. The viewDidDisappear method is an ideal place to remove these observers. Listing 7.21 shows the code involved.

Listing 7.21 ItemVC.m: viewDidDisappear


// Unregister for keyboard notifications while the view is not visible.
[[NSNotificationCenter defaultCenter] removeObserver:self
                                     name:UIKeyboardDidShowNotification
                                   object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
                                     name:UIKeyboardWillHideNotification
                                      object:nil];


Update Grocery Dude as follows to stop observing keyboard notifications:

1. Add the code from Listing 7.21 to the bottom of the viewDidDisappear method of ItemVC.m.

Run the application again, and ensure that there are several shop locations in the persistent store. On the Item view, tap the Shop Location picker text field and change the shop location of an item. You should notice that the shop location picker text field comes into view automatically.

Summary

Throughout this chapter, you’ve seen how to bind Core Data fetched results to a picker, then present that picker in an inputView triggered from a custom text field. The picker text fields for Grocery Dude were fully implemented in the process, so configuring an item should now be very fast. This will become even more apparent once there are several units, home locations, and shop locations added to the persistent store.

Exercises

Why not build on what you’ve learned by experimenting?

1. Temporarily set self.showToolbar = NO; in the createInputAccessoryView method of CoreDataPickerTF.m and then run the application to prove the toolbar is hidden on all picker views. Note that you can override this method in specific subclasses if you only want to hide the toolbar on specific pickers.

2. Temporarily create a multicomponent picker by creating an additional array in one of the CoreDataPickerTF subclasses. Note that you’ll have to override the numberOfComponentsInPickerView method to achieve this.