Views - Learning Core Data for iOS (2014) 

Learning Core Data for iOS (2014)

6. Views

A man should look for what is, and not for what he thinks should be.

Albert Einstein

Chapter 5, “Table Views,” demonstrated how to create Core Data–backed table views. During this process, the benefits of a fetched results controller were explained as one was implemented in CoreDataTVC. This helpful UITableViewController subclass was itself subclassed as you customized the Prepare and Shop tabs to display different information. The application now looks a lot more like a real iOS application, and less like an exercise in Core Data theory. This chapter shows how to pass selected managed objects from a table view to a view. A custom view will also be configured to edit the selected managed object. In the process, you’ll be shown how to configure a UITextField to allow editing of a managed object’s property values.

Overview

The most common standard iOS interface element is the UIView. Based on UIResponder, this powerful part of UIKit offers a highly customizable way to display things onscreen. Interface elements such as UIPickerView, UITextField, UIButton, and UIScrollView are all derived from UIView. Although the UITableView is great for displaying or deleting data, a UIView offers a better starting point for editing data. By adding a UIKit component such as a UITextField to a view, the user can modify object properties. Typically, a customized UIView concentrates on editing one managed object at a time, which will be the case for Grocery Dude.

When designing a UIView for editing, think about what the user might be doing outside the application when performing edits. In the case of Grocery Dude, the user will probably be pushing a grocery cart, collecting an item, or rummaging through the house determining what he or she needs to buy. This means a user only has one hand free to interact with the application. By favoring interface elements that minimize user interaction, you’ll produce an application that’s easy to use. An example of such a decision might be choosing to use a picker-view-enabled UITextField instead of a UITextField on its own. Even the choice of keyboard that’s displayed when a UITextField is tapped can go a long way to improve the user experience.


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-AfterChapter05.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.


The Target View Hierarchy

Currently, the view hierarchy has a tab bar controller at the root, with table views at the next level. Deeper into the view hierarchy is a blank Item view controller, which will become central to the editing of an item. Figure 6.1 shows a high-level overview of the target view hierarchy that will be in place by the end of this chapter.

Image

Figure 6.1 A high-level overview of the target view hierarchy

You should already be familiar with the Prepare and Shop tabs, which display the PrepareTVC and ShopTVC table views. When the + in the top-right corner of the PrepareTVC table view is tapped, a segue to the Item view controller occurs. In this segue, a new managed object needs to be created and its objectID passed to the Item view controller. When the accessory detail button is tapped on the PrepareTVC table view, the existing managed object’s objectID will be passed to the Item view controller. Note that when passing managed objects around an application, you don’t necessarily need to use the objectID unless you’re passing the object between threads. If you get into the habit of using object IDs, you don’t need to consider whether there will be threading issues.

Introducing ItemVC

A managed object from the Item entity has several properties that the user will need to edit, such as item name, quantity, and so on. To enable this functionality, a new UIViewController subclass named ItemVC will be created. This new class will be connected to various interface elements and will enable the user to edit an item.

Update Grocery Dude as follows to add a new UIViewController subclass called ItemVC:

1. Select the Grocery Dude View Controllers 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 UIViewController and Class name to ItemVC 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.

The expected result is shown in Figure 6.2.

Image

Figure 6.2 View Controller and Table View Controller subclasses


Note

Similar to CoreDataTVC, the ItemVC class name was chosen against ItemViewController to minimize the amount of repeated text throughout this book. This goes against standard Objective-C naming conventions and would otherwise be discouraged. Class names should be expressive, clear, and not ambiguous.


Keeping Reference to a Selected Item

To edit an existing item, users will select the accessory detail button for an item on the PrepareTVC or ShopTVC table view. To prepare the ItemVC view, a selectedItemID property in ItemVC will be populated with the objectID of the selected item. Listing 6.1 shows this new property among the new header file code for ItemVC.

Listing 6.1 ItemVC.h


#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
@interface ItemVC : UIViewController <UITextFieldDelegate>
@property (strong, nonatomic) NSManagedObjectID *selectedItemID;
@end


Update Grocery Dude as follows to configure the ItemVC header:

1. Replace the existing code in ItemVC.h with the code from Listing 6.1.

Passing a Selected Item to ItemVC

An item will be passed to ItemVC in Grocery Dude in two ways. For new items, the prepareForSegue method in PrepareTVC will be used. For existing items, the accessoryButtonTappedForRowWithIndexPath method of PrepareTVC and ShopTVC will be used. Both methods are shown in Listing 6.2.

Listing 6.2 PrepareTVC.m and ShopTVC.m: prepareForSegue and accessoryButtonTappedForRowWithIndexPath


#pragma mark - SEGUE
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    ItemVC *itemVC = segue.destinationViewController;
    if ([segue.identifier isEqualToString:@"Add Item Segue"])
    {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *newItem =
        [NSEntityDescription insertNewObjectForEntityForName:@"Item"
                                      inManagedObjectContext:cdh.context];
        NSError *error = nil;
        if (![cdh.context
              obtainPermanentIDsForObjects:[NSArray arrayWithObject:newItem]
              error:&error]) {
            NSLog(@"Couldn't obtain a permanent ID for object %@", error);
        }
        itemVC.selectedItemID = newItem.objectID;
    }
    else {
        NSLog(@"Unidentified Segue Attempted!");
    }
}
- (void)tableView:(UITableView *)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    ItemVC *itemVC =
    [self.storyboard instantiateViewControllerWithIdentifier:@"ItemVC"];
    itemVC.selectedItemID =
    [[self.frc objectAtIndexPath:indexPath] objectID];
    [self.navigationController pushViewController:itemVC animated:YES];
}


When a new item is inserted as a part of the Add Item Segue, an immediate call to obtain a permanent ID for the object is made. This prevents issues down the track when multiple contexts, iCloud, and web service integration are introduced.

Update Grocery Dude as follows to configure the PrepareTVC and ShopTVC implementations:

1. Add #import "ItemVC.h" to the top of PrepareTVC.m and ShopTVC.m. This is required so these views know what ItemVC is in order to transition to it.

2. Add the code from Listing 6.2 to the bottom of PrepareTVC.m before @end.

3. Add the code from Listing 6.2 to the bottom of ShopTVC.m before @end.

4. Delete the prepareForSegue method from ShopTVC.m. It’s not required in this class because new items aren’t created on the Shop tab.

Configuring the Scroll View and Text Fields

At first, the focus will be on the UITextField objects used to edit the name and quantity values. In the next chapter, custom UITextField objects with the ability to present a UIPickerView will be added. Those special UITextField objects will be used to select an item’s unit.name,locationAtHome.storedIn, and locationAtShop.aisle from a picker. The target layout of the Item view for this chapter is shown in Figure 6.3. All of the fields shown will be contained within a Scroll View to allow scrolling up and down within the view. The buttons will lead to other views used to add and edit units, home locations, and shop locations.

Image

Figure 6.3 The target Item View Controller

The easiest way to create properties linked to a view is to configure the interface elements on the storyboard. You can then drag them into the appropriate class header or implementation file, depending on whether they should be public or private properties.

Update Grocery Dude as follows to add a Scroll View:

1. Select Main.storyboard.

2. Select the Item view controller and untick Extend Edges Under Top Bars using Attributes Inspector (Option+image+4).

3. Drag a Scroll View onto the existing Item view, centering it exactly so it takes up the whole view.

4. While the Scroll View is selected, click Editor > Resolve Auto Layout Issues > Add Missing Constraints. This ensures the Scroll View will stretch when the device is rotated.

5. If it’s hidden, show the Document Outline by clicking Editor > Show Document Outline.

6. While the Scroll View is selected, click Editor > Reveal in Document Outline and then check constraints were applied to the Scroll View, as shown in Figure 6.4.

Image

Figure 6.4 A new Scroll View using Auto Layout

Update Grocery Dude as follows to add the Name and Quantity text fields:

1. Drag a Text Field anywhere onto the new Scroll View and then configure it as follows using Attributes Inspector (Option+image+4):

image Set Font to System Bold 17.0.

image Set Text Alignment to Center.

image Set Placeholder Text to Name.

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

image Set Capitalization to Sentences.

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

2. Configure the Height of the text field to 48 using Size Inspector (Option+image+5).

3. Duplicate the text field by holding Option while dragging downward.

4. Configure the duplicated text field as follows using Attributes Inspector (Option+image+4):

image Set Placeholder Text to Qty.

image Set Keyboard to Decimal Pad.

image Tick Clear when editing begins.

5. Configure the Width of the Qty text field to 60 using Size Inspector (Option+image+5).

6. Widen the Name text field to the edge guides and then arrange the text fields to the edge guides, as shown in Figure 6.5.

Image

Figure 6.5 The Name and Qty text field arrangement

7. Select both text fields at once (image+click) and then click Editor > Resolve Auto Layout Issues > Add Missing Constraints. This ensures the text fields resize when the device is rotated. The expected result is shown in Figure 6.5.

To make the connection from the scroll view and text fields into code, the Custom Class of the Item view controller needs to be set. After that’s done, you’ll be able to drag these user interface elements straight into the ItemVC header using the Assistant Editor.

Update Grocery Dude as follows to configure the Item view:

1. Select the Item View Controller.

2. Set both the Custom Class and Storyboard ID of the Item View Controller to ItemVC using Identity Inspector (Option+image+3). The Storyboard ID will be used to reference this view when the detail disclosure is tapped on the Items table view. The expected result is shown in Figure 6.6.

Image

Figure 6.6 The Item View Controller uses the ItemVC class

Update Grocery Dude as follows to create properties for the Name and Qty text fields:

1. With the Item View Controller selected, show the Assistant Editor (Option+image+Return).

2. Set the Assistant Editor to Automatic > ItemVC.h, as shown in the top right of Figure 6.7.

Image

Figure 6.7 Creating properties from storyboard elements

3. If it’s hidden, show the Document Outline by clicking Editor > Show Document Outline.

4. Expand ItemVC - Item Scene, as shown in Figure 6.7 on the left.

5. Hold down Control and drag a line from the Scroll View to ItemVC.h before @end and then set the property Name to scrollView, as shown in the middle of Figure 6.7. Double-check the Type is UIScrollView and the Storage is Strong.

6. Hold down Control and drag a line from the Line Style Text Field - Name to ItemVC.h before @end. Set the new property Name to nameTextField. Double-check the Type is UITextField and the Storage is Strong.

7. Hold down Control and drag a line from the Line Style Text Field - Qty to ItemVC.h before @end. Set the new property Name to quantityTextField. Double-check the Type is UITextField and the Storage is Strong.


Note

Normally it is recommended that you only expose properties in the header file when they need to be accessed from outside the class. In order to assist reader focus, the code in this book goes against this recommendation and keeps the implementation files as empty as possible. Note also that it is possible to adopt a protocol privately within the interface directive of an implementation file. Again, Grocery Dude will instead place such code in the header file for clarity.


There should now be a scrollView, nameTextField, and quantityTextField property in ItemVC.h.

ItemVC Implementation

The ItemVC implementation file is categorized into sections similar to the way PrepareTVC and ShopTVC are. ItemVC will initially have the following sections:

image INTERACTION will contain methods responsible for hiding the keyboard when the background is tapped. It will also have methods that return the user to the PrepareTVC table view whenever a new Done button is tapped.

image DELEGATE: UITextField will contain methods available to classes adopting the UITextFieldDelegate protocol. They will be leveraged to set an item name according to the contents of the text field and to ensure zero-length names aren’t accepted.

image VIEW will contain methods responsible for ensuring each interface element is refreshed with appropriate data from the persistent store.

image DATA will contain methods responsible for ensuring items always have a home location and shop location object set, even when the location is unknown.

Interaction

The code from the INTERACTION section will be used in all view controllers in Grocery Dude. There are three methods in this section. The done method will be linked to a new Done button. When tapped, this button will pop the ItemVC view controller, which returns the user to the previous table view. The hideKeyboardWhenBackgroundIsTapped method configures a UITapGestureRecognizer ready to respond to the view background being tapped. Whenever the view background is tapped, the hideKeyboard method ends view editing, which hides the keyboard. Listing 6.3 shows the code involved.

Listing 6.3 ItemVC.m: INTERACTION


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

#pragma mark - INTERACTION
- (IBAction)done:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self hideKeyboard];
    [self.navigationController popViewControllerAnimated:YES];
}
- (void)hideKeyboardWhenBackgroundIsTapped {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    UITapGestureRecognizer *tgr =
    [[UITapGestureRecognizer alloc] initWithTarget:self
                                            action:@selector(hideKeyboard)];
    [tgr setCancelsTouchesInView:NO];
    [self.view addGestureRecognizer:tgr];
}
- (void)hideKeyboard {
if (debug==1) {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.view endEditing:YES];
}
@end


Update Grocery Dude as follows to configure the INTERACTION section:

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

2. Replace all existing code in ItemVC.m with the code from Listing 6.3 and then save the class file (image+S).

3. Select Main.storyboard.

4. Drag a Bar Button Item onto the top-right corner of the Item view.

5. Set the Bar Button Item Identifier to Done using Attributes Inspector (Option+image+4). The expected result is shown in Figure 6.8.

Image

Figure 6.8 A new Done button

6. Hold down Control and drag a line from the new Done button to the yellow circle at the bottom of the Item view and then select Sent Actions > done.

DELEGATE: UITextField

The code for the DELEGATE: UITextField section will implement the textFieldDidBeginEditing and textFieldDidEndEditing methods, which are optional UITextFieldDelegate protocol methods. These methods will be called whenever a text field gains or loses focus. When a text field gains focus, new items have their name set to a zero-length string. When a text field loses focus, it is an opportune time to update the selected managed object with the contents of the text field that just lost focus. Listing 6.4 shows the code involved.

Listing 6.4 ItemVC.m: DELEGATE: UITextField


#pragma mark - DELEGATE: UITextField
- (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 = @"";
        }
    }
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    Item *item =
    (Item*)[cdh.context existingObjectWithID:self.selectedItemID error:nil];

    if (textField == self.nameTextField) {
        if ([self.nameTextField.text isEqualToString:@""]) {
            self.nameTextField.text = @"New Item";
        }
        item.name = self.nameTextField.text;
    }
    else if (textField == self.quantityTextField) {
        item.quantity =
        [NSNumber numberWithFloat:self.quantityTextField.text.floatValue];
    }
}


Update Grocery Dude as follows to implement the DELEGATE: UITextField section:

1. Add #import "AppDelegate.h" and #import "Item.h" to the top of ItemVC.m.

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

View

The VIEW section will have four methods:

image The refreshInterface method refreshes the interface using the values of the selected managed object, provided there is one.

image The viewDidLoad method configures the view as a text field delegate. It also calls a method from the INTERACTION section responsible for hiding the keyboard when the view background is tapped.

image The viewWillAppear method calls refreshInterface whenever the view is about to appear, which ensures fresh data is visible. It also shows the keyboard immediately when a new item is being created.

image The viewDidDisappear method saves the context whenever the view disappears. Although it’s not strictly necessary to save the context every time a view disappears, it’s a good idea to persist data regularly in case the device loses power. When iCloud and Web Service integration is introduced, saving also becomes more important because it ensures changes are updated outside of the application.

Listing 6.5 shows the code involved.

Listing 6.5 ItemVC.m: VIEW


#pragma mark - VIEW
- (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;
    }
}
- (void)viewDidLoad {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidLoad];
    [self hideKeyboardWhenBackgroundIsTapped];
    self.nameTextField.delegate = self;
    self.quantityTextField.delegate = self;
}
- (void)viewWillAppear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self refreshInterface];
    if ([self.nameTextField.text isEqual:@"New Item"]) {
        self.nameTextField.text = @"";
        [self.nameTextField becomeFirstResponder];
    }
}
- (void)viewDidDisappear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    [cdh saveContext];
}


Update Grocery Dude as follows to implement the VIEW section:

1. Add the code from Listing 6.5 to the bottom of ItemVC.m before @end.

2. Ensure the persistent store is empty by deleting the application from the device (or simulator).

3. Click Product > Clean.

4. Run the application and then click + on the Prepare tab to create a new item.

5. Set the item Name to Coffee and the Quantity to 2 and then press Done.

As you’re returned to the Items table view, you won’t be able to see the new item. This is because it isn’t in an appropriate table view section. If you could see the item and then tapped it, it would disappear. This issue occurs because there’s no home location set. This issue also occurs on the Shop tab, in this case because there’s no shop location set. These locations are critical to the section layout of each table view. As a safeguard against this issue, two new methods will be added to a new DATA section in ItemVC.

Data

The DATA section will have two methods that rely on different fetch request templates. Each fetch request template will be configured to look for a home or shop location with the exact name ..Unknown Location... The leading dots will ensure that this location is located at the top of the table view, due to the sort descriptor of the fetch.

Update Grocery Dude as follows to configure the new fetch request templates:

1. Select Model 6.xcdatamodel.

2. Add a fetch request called UnknownLocationAtHome.

3. Add a fetch request called UnknownLocationAtShop.

4. Configure the UnknownLocationAtHome fetch request template to fetch all LocationAtHome objects where storedIn is ..Unknown Location.., as shown in Figure 6.9. You’ll need to click the + to add fetch criteria.

Image

Figure 6.9 ..Unknown Location.. fetch request templates

5. Similarly, configure the UnknownLocationAtShop fetch request template to fetch all LocationAtShop objects where aisle is ..Unknown Location...

Here are the DATA section methods that will use these fetch request templates:

image The ensureItemHomeLocationIsNotNull method is responsible for setting an item’s home location to ..Unknown Location.. whenever item.locationAtHome is nil.

image The ensureItemShopLocationIsNotNull method is responsible for setting an item’s shop location to ..Unknown Location.. whenever item.locationAtShop is nil.

Listing 6.6 shows the code involved.

Listing 6.6 ItemVC.m: DATA


#pragma mark - DATA
- (void)ensureItemHomeLocationIsNotNull {
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 (!item.locationAtHome) {
            NSFetchRequest *request =
            [[cdh model]
                  fetchRequestTemplateForName:@"UnknownLocationAtHome"];
            NSArray *fetchedObjects =
            [cdh.context executeFetchRequest:request error:nil];

            if ([fetchedObjects count] > 0) {
                item.locationAtHome = [fetchedObjects objectAtIndex:0];
            }
            else {
                LocationAtHome *locationAtHome =
                [NSEntityDescription
                 insertNewObjectForEntityForName:@"LocationAtHome"
                          inManagedObjectContext:cdh.context];
                NSError *error = nil;
                if (![cdh.context obtainPermanentIDsForObjects:
                 [NSArray arrayWithObject:locationAtHome] error:&error]) {
                     NSLog(@"Couldn't obtain a permanent ID for object %@",
                                                                    error);
                }
                locationAtHome.storedIn = @"..Unknown Location..";
                item.locationAtHome = locationAtHome;
            }
        }
    }
}
- (void)ensureItemShopLocationIsNotNull {
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 (!item.locationAtShop) {
            NSFetchRequest *request =
            [[cdh model]
                  fetchRequestTemplateForName:@"UnknownLocationAtShop"];
            NSArray *fetchedObjects =
            [cdh.context executeFetchRequest:request error:nil];

            if ([fetchedObjects count] > 0) {
                item.locationAtShop = [fetchedObjects objectAtIndex:0];
            }
            else {
                LocationAtShop *locationAtShop =
                [NSEntityDescription
                 insertNewObjectForEntityForName:@"LocationAtShop"
                          inManagedObjectContext:cdh.context];
                NSError *error = nil;
                if (![cdh.context obtainPermanentIDsForObjects:
                 [NSArray arrayWithObject:locationAtShop] error:&error]) {
                    NSLog(@"Couldn't obtain a permanent ID for object %@",
                                                                   error);
                }
                locationAtShop.aisle = @"..Unknown Location..";
                item.locationAtShop = locationAtShop;
            }
        }
    }
}


The first thing the two new methods do is check that the selected managed object isn’t nil. If it’s not, a cdh pointer to the application delegate’s CoreDataHelper instance is established. The cdh pointer is then used to create an item pointer to the managed object using its objectID. If the relevant shop or home location object already exists, nothing else happens. If it doesn’t exist, an appropriate ..Unknown Location.. object is set as its home or shop location. If the ..Unknown Location.. object itself doesn’t exist, it is created for the first time and assigned to the item.

Update Grocery Dude as follows to implement the DATA section:

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

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

3. Add the code from Listing 6.6 to the bottom of ItemVC.m before @end.

4. Add the following code to the viewWillAppear method of ItemVC.m before [self refreshInterface]:

[self ensureItemHomeLocationIsNotNull];
[self ensureItemShopLocationIsNotNull];

5. Add the following code to the viewDidDisappear method of ItemVC.m before cdh is declared:

[self ensureItemHomeLocationIsNotNull];
[self ensureItemShopLocationIsNotNull];

6. Run the application again, tap the accessory detail button next to the Coffee item, and then tap Done. The expected result is shown in Figure 6.10. If you miss the disclosure indicator and tap the item before its location can be set, it may disappear. If this happens, just run the application again.

Image

Figure 6.10 ..Unknown Location.. has its own section.

Units, Home Locations, and Shop Locations

Users must be able to add and edit units, home locations, and shop locations. The technique to achieve this goal will be similar to the technique used to add and edit items. This means there will be a table view with a custom CoreDataTVC subclass and an additional view for editing each of these object types.

Update Grocery Dude as follows to add the generic table view and view:

1. Select Main.storyboard.

2. Drag a new Table View Controller onto the storyboard to the right of the existing Item view.

3. Select the Prototype Cell of the new Table View Controller and set its Reuse Identifier to Cell using Attributes Inspector (Option+image+4).

4. With the new Table View Controller selected, click Editor > Embed In > Navigation Controller.

5. Drag a Bar Button Item on to the top left of the new Table View Controller.

6. Set the new Bar Button Item Identifier to Done.

7. Drag a Bar Button Item onto the top right of the new Table View Controller.

8. Set the new Bar Button Item Identifier to Add.

9. Drag a new View Controller onto the storyboard to the right of the new Table View Controller.

10. Hold down Control and drag a line from the Prototype Cell of the new Table View Controller to the new View Controller and then select Selection Segue > push.

11. Set the Storyboard Segue Identifier of the new segue to Edit Object Segue.

12. Hold down Control and drag a line from the new Table View Controller’s Add (+) button to the new View Controller and then select Action Segue > push.

13. Set the Storyboard Segue Identifier of the new segue to Add Object Segue.

14. Drag a Bar Button Item onto the top right of the new View Controller.

15. Set the new Bar Button Item Identifier to Done.

16. Drag a Text Field anywhere onto the new View Controller and then configure it as follows using Attributes Inspector (Option+image+4):

image Set Font to System Bold 17.0.

image Set Text Alignment to Center.

image Set Placeholder Text to Name.

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

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

17. Configure the Height of the text field to 48 using Size Inspector (Option+image+5).

18. Widen and position the Name text field to the edge guides, as shown on the right of Figure 6.11.

Image

Figure 6.11 Generic controllers

The generic controllers are ready, so it’s time to use them as the basis of the add/edit controllers for units, home locations, and shop locations.

Update Grocery Dude as follows to replicate the generic controllers:

1. Select together the generic Navigation ControllerTable View Controller, and View Controller built in the previous steps and then click Edit > Duplicate.

2. Drag the duplicated controllers above the original controllers.

3. Repeat the duplication procedure and arrange the controllers as shown in Figure 6.12.

Image

Figure 6.12 Generic controllers, in triplicate

You should have nine new controllers, as shown in Figure 6.12. The top three will be for units, the middle three for home locations, and the bottom three for shop locations. The controllers will be reached via three new buttons positioned on the Item view. Until the buttons are implemented, there may be a warning in Xcode regarding how these new controllers are unreachable, which you can safely ignore for the time being.

Adding and Editing Units

To reach the new controllers, new buttons are required in the Item view. The first button required is the Add Units button. This button will segue to a Units table view, which is embedded in a Navigation Controller. The Units table view will behave like the Items table view in that it will have an associated view for editing the selected object (in this case, a unit).

Update Grocery Dude as follows to add the button icons:

1. Download and extract the button icons from http://www.timroadley.com/LearningCoreData/Icons_ItemVC.zip.

2. Select the Images.xcassets asset catalog.

3. Drag the new icons into the asset catalog, as shown in Figure 6.13.

Image

Figure 6.13 New button icons

Update Grocery Dude as follows to add the Add Unit button:

1. Select Main.storyboard.

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

image Set the Type to Custom.

image Delete the text that reads “Button.”

image Set the Image to add_units.

3. Configure the Button as follows using Size Inspector (Option+image+5):

image Ensure Width is 48.

image Ensure Height is 48.

4. Hold down Control and drag a line from the new button to the Navigation Controller that leads to the top generic table view controller built in the previous steps and then select Action Segue > modal.

5. Set the Navigation Item Title of the table view controller that the new modal segue leads to as Units.

6. Select the Prototype Cell of the Units table view controller and set the Reuse Identifier to Unit Cell using Attributes Inspector (Option+image+4).

7. Set the Navigation Item Title of the View Controller that the Units table view controller leads to as Unit.

8. Drag, center, and widen to fit a new Text View beneath the existing Name text field on the Unit view controller, configuring it as follows:

image Set Text content to Enter a unit of measurement, E.g. ‘ml’, ‘pkt’ or ‘items’.

image Set Color to Light Grey Color.

image Set Font to System Bold 16.0.

image Set Alignment to Centered.

image Untick Editable and Selectable.

9. With the Name Text Field and Text View selected, click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints.

If you run the application and make your way to the Item view, you can test out the new Add Unit button. There’s no code behind any of these views yet, so it’s time to create a CoreDataTVC subclass so these views function correctly. Without this code, editing the unit name has no effect, nor does pressing the Done button. This means you’ll currently get stuck in the modal popover.

Implementing UnitsTVC

The Units table view will display a list of available units. When the view loads, the table should be populated with all unit objects found in the persistent store, sorted by name. When a unit is swiped, it should be deleted. When the Done button is pressed, the units view should be dismissed.UnitsTVC will be a CoreDataTVC subclass and will implement this required functionality, as shown in Listing 6.7. All code found in this listing should be familiar because it has been similarly implemented previously.

Listing 6.7 UnitsTVC.m


#import "UnitsTVC.h"
#import "CoreDataHelper.h"
#import "AppDelegate.h"
#import "Unit.h"
@implementation UnitsTVC
#define debug 1
#pragma mark - DATA
- (void)configureFetch {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSFetchRequest *request =
    [NSFetchRequest fetchRequestWithEntityName:@"Unit"];
    request.sortDescriptors = [NSArray arrayWithObjects:
     [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES],nil];
    [request setFetchBatchSize:50];
    self.frc =
    [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                        managedObjectContext:cdh.context
                                          sectionNameKeyPath:nil
                                                   cacheName:nil];
    self.frc.delegate = self;
}

#pragma mark - VIEW
- (void)viewDidLoad {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidLoad];
    [self configureFetch];
    [self performFetch];
    // Respond to changes in underlying store
    [[NSNotificationCenter defaultCenter] addObserver:self
                                            selector:@selector(performFetch)
                                                 name:@"SomethingChanged"
                                               object:nil];
}
- (UITableViewCell*)tableView:(UITableView *)tableView
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    static NSString *cellIdentifier = @"Unit Cell";
    UITableViewCell *cell =
    [tableView dequeueReusableCellWithIdentifier:cellIdentifier
                                    forIndexPath:indexPath];
    Unit *unit = [self.frc objectAtIndexPath:indexPath];
    cell.textLabel.text = unit.name;
    return cell;
}
- (void)tableView:(UITableView *)tableView
 commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
  forRowAtIndexPath:(NSIndexPath *)indexPath {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        Unit *deleteTarget = [self.frc objectAtIndexPath:indexPath];
        [self.frc.managedObjectContext deleteObject:deleteTarget];
        [self.tableView reloadRowsAtIndexPaths:
                      [NSArray arrayWithObject:indexPath]
                              withRowAnimation:UITableViewRowAnimationFade];
    }
}

#pragma mark - INTERACTION
- (IBAction)done:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.parentViewController
          dismissViewControllerAnimated:YES completion:nil];
}
@end


Update Grocery Dude as follows to implement UnitsTVC:

1. Select the Grocery Dude Table View Controllers 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 CoreDataTVC and Class name to UnitsTVC 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 UnitsTVC.m with the code from Listing 6.7 and then save the class file (press image+S).

7. Select Main.storyboard.

8. Set the Custom Class of the Units table view controller to UnitsTVC using Identity Inspector (Option+image+3).

9. Hold down Control and drag a line from the Done button on the Units table view controller to the yellow circle at the bottom of the same view and then select Sent Actions > done.

The Units table view is now ready. The next step is to implement the code required for the Unit view controller, including the relevant segue to get to it.

Implementing UnitVC

The Unit view will allow editing of a unit’s name using a text field. When the view loads, the text field will be populated with the name of the unit that was selected on the table view. When the Done button is pressed, the view should be dismissed. UnitVC will be a UIViewController subclass and will implement this required functionality. The relevant header file is shown in Listing 6.8.

Listing 6.8 UnitVC.h


#import <UIKit/UIKit.h>
#import "CoreDataHelper.h"
@interface UnitVC : UIViewController <UITextFieldDelegate>
@property (strong, nonatomic) NSManagedObjectID *selectedObjectID;
@property (strong, nonatomic) IBOutlet UITextField *nameTextField;
@end


Update Grocery Dude as follows to add the UnitVC class:

1. Select the Grocery Dude View Controllers 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 UIViewController and Class name to UnitVC 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 UnitVC.h with the code from Listing 6.8.

Three sections will be implemented in UnitVC.m:

image The VIEW section will have three methods. The viewDidLoad method implements the standard keyboard-hiding techniques used previously in this book. The viewWillAppear method refreshes the interface and makes the only text field in the view the first responder so the keyboard is shown. The refreshInterface method populates the nameTextField with the name of the selected unit.

image The TEXTFIELD section implements an optional UITextFieldDelegate protocol method in order to update the unit.name value whenever editing finishes.

image The INTERACTION section matches the one used in ItemVC, so it will have methods responsible for hiding the keyboard when the background is tapped. It will also have methods that return the user to the UnitsTVC table view when a new Done button is tapped.

Listing 6.9 shows the code involved.

Listing 6.9 UnitVC.m


#import "UnitVC.h"
#import "Unit.h"
#import "AppDelegate.h"
@implementation UnitVC
#define debug 1
#pragma mark - VIEW
- (void)refreshInterface {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedObjectID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Unit *unit =
        (Unit*)[cdh.context existingObjectWithID:self.selectedObjectID
                                           error:nil];
        self.nameTextField.text = unit.name;
    }
}
- (void)viewDidLoad {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidLoad];
    [self hideKeyboardWhenBackgroundIsTapped];
    self.nameTextField.delegate = self;
}
- (void)viewWillAppear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self refreshInterface];
    [self.nameTextField becomeFirstResponder];
}

#pragma mark - TEXTFIELD
- (void)textFieldDidEndEditing:(UITextField *)textField {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    Unit *unit =
    (Unit*)[cdh.context existingObjectWithID:self.selectedObjectID
                                       error:nil];
    if (textField == self.nameTextField) {
        unit.name = self.nameTextField.text;
        [[NSNotificationCenter defaultCenter]
                               postNotificationName:@"SomethingChanged"
                                             object:nil];
    }
}

#pragma mark - INTERACTION
- (IBAction)done:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self hideKeyboard];
    [self.navigationController popViewControllerAnimated:YES];
}
- (void)hideKeyboardWhenBackgroundIsTapped {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    UITapGestureRecognizer *tgr =
    [[UITapGestureRecognizer alloc] initWithTarget:self
                                            action:@selector(hideKeyboard)];
    [tgr setCancelsTouchesInView:NO];
    [self.view addGestureRecognizer:tgr];
}
- (void)hideKeyboard {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [self.view endEditing:YES];
}
@end


Update Grocery Dude as follows to update the UnitVC implementation:

1. Replace all code in UnitVC.m with the code from Listing 6.9 and then save the class file (press image+S).

2. Select Main.storyboard.

3. Set the Custom Class of the Unit view controller to UnitVC using Identity Inspector (Option+image+3).

4. Hold down Control and drag a line from the Done button on the Unit view controller to the yellow circle at the bottom of the same view. Then select Sent Actions > done.

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

6. Set the Assistant Editor to Automatic > UnitVC.h, as shown at the top of Figure 6.14.

Image

Figure 6.14 Connecting the unit name text field to code

7. Hold down Control and drag a line from the unit Name text field to the existing nameTextField property found in UnitVC.h, as shown in Figure 6.14.

Segue from UnitsTVC to UnitVC

To reach UnitVC, a segue is required from UnitsTVC, as shown in Listing 6.10. This segue will pass the objectID of the selected unit to UnitVC. UnitVC will use this objectID to determine the selected unit before refreshing itself with the unit’s data.

Listing 6.10 UnitsTVC.m: prepareForSegue


#pragma mark - SEGUE
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    UnitVC *unitVC = segue.destinationViewController;
    if ([segue.identifier isEqualToString:@"Add Object Segue"])
    {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Unit *newUnit =
        [NSEntityDescription insertNewObjectForEntityForName:@"Unit"
                                      inManagedObjectContext:cdh.context];
        NSError *error = nil;
        if (![cdh.context obtainPermanentIDsForObjects:
             [NSArray arrayWithObject:newUnit] error:&error]) {
            NSLog(@"Couldn't obtain a permanent ID for object %@", error);
        }
        unitVC.selectedObjectID = newUnit.objectID;
    }
    else if ([segue.identifier isEqualToString:@"Edit Object Segue"])
    {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        unitVC.selectedObjectID =
        [[self.frc objectAtIndexPath:indexPath] objectID];
    }
    else {
        NSLog(@"Unidentified Segue Attempted!");
    }
}


Update Grocery Dude as follows to implement the segue from UnitsTVC to UnitVC:

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

2. Add #import "UnitVC.h" to the top of UnitsTVC.m.

3. Add the code from Listing 6.10 to the bottom of UnitsTVC.m before @end.

Run the application and try adding some units via the new unit views. You won’t be able to assign a unit to an item yet—that’s a goal of the next chapter.

Adding and Editing Home or Shop Locations

The home and shop location table views and views will be a near match to UnitsTVC and UnitVC. The only difference is they will support home or shop location objects instead of unit objects. To avoid a large exercise in copy and paste, premade location class files are available for download.

Update Grocery Dude as follows to add the premade location class files:

1. Download and extract the location classes from http://www.timroadley.com/LearningCoreData/LocationClasses.zip.

2. Drag the LocationsAtHomeTVC and LocationsAtShopTVC class files into the Grocery Dude Table View Controllers group. Ensure Copy items into destination group’s folder and the Grocery Dude target are ticked before clicking Finish.

3. Drag the LocationAtHomeVC and LocationAtShopVC class files into the Grocery Dude View Controllers group. Ensure Copy items into destination group’s folder and the Grocery Dude target are ticked before clicking Finish.

Configuring the Home Location Views

To use these new classes, a similar exercise to the one used to configure the unit views is now required. These steps will configure buttons, connections, and text fields associated to the home location views.

Update Grocery Dude as follows:

1. Select Main.storyboard.

2. Drag a Button anywhere into the existing Scroll View of the Item view, and then configure it as follows using Attributes Inspector (Option+image+4):

image Set the Type to Custom.

image Delete the text that reads “Button.”

image Set the Image to add_homelocations.

3. Configure the button as follows using Size Inspector (Option+image+5):

image Ensure Width is 48.

image Ensure Height is 48.

4. Hold down Control and drag a line from the new button to the Navigation Controller beneath the Units Navigation Controller, which leads to the middle three generic controllers from previous steps. Then select Action Segue > modal.

5. Set the Navigation Item Title of the Table View Controller that the new modal segue leads to as Home Locations.

6. Select the Prototype Cell of the Home Locations table view controller and set the Reuse Identifier to LocationAtHome Cell using Attributes Inspector (Option+image+4).

7. Set the Navigation Item Title of the View Controller that the Home Locations table view controller leads to as Home Location.

8. Copy the Text View from the Unit view to the same position in the Home Location view and then change the text content to Enter the location an item is expected to be found at home. E.g. ‘Pantry’ or ‘Bathroom’.

9. With the Name Text Field and Text View selected on the Home Location view, click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints.

10. Set the Custom Class of the Home Location view controller to LocationAtHomeVC using Identity Inspector (Option+image+3).

11. Set the Custom Class of the Home Locations table view controller to LocationsAtHomeTVC using Identity Inspector (Option+image+3).

12. Hold down Control and drag a line from the Done button on the Home Locations table view controller to the yellow circle at the bottom of the same view, and then select Sent Actions > done.

13. Hold down Control and drag a line from the Done button on the Home Location view controller to the yellow circle at the bottom of the same view, and then select Sent Actions > done.

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

15. Set the Assistant Editor to Automatic > LocationAtHomeVC.h using the approach demonstrated previously in Figure 6.14.

16. Hold down Control and drag a line from the Home Location Name text field to the existing nameTextField property found in LocationAtHomeVC.h. Use the approach demonstrated previously in Figure 6.14.

Configuring the Shop Location Views

To use the new shop location classes, another similar exercise is now required. These steps will configure the buttons, connections, and text fields associated to the shop location views.

Update Grocery Dude as follows:

1. Select Main.storyboard and show the Standard Editor (image+Return).

2. Drag a Button anywhere into the existing Scroll View of the Item view, and then configure it as follows using Attributes Inspector (Option+image+4):

image Set the Type to Custom.

image Delete the text that reads “Button.”

image Set the Image to add_shoplocations.

3. Configure the button as follows using Size Inspector (Option+image+5):

image Ensure Width is 48.

image Ensure Height is 48.

4. Hold down Control and drag a line from the new button to the Navigation Controller beneath the Home Locations Navigation Controller, which leads to the bottom three generic controllers from previous steps. Then select Action Segue > modal.

5. Set the Navigation Item Title of the table view controller that the new modal segue leads to as Shop Locations.

6. Select the Prototype Cell of the Shop Locations table view controller and set the Reuse Identifier to LocationAtShop Cell using Attributes Inspector (Option+image+4).

7. Set the Navigation Item Title of the view controller that the Shop Locations table view controller leads to as Shop Location.

8. Copy the Text View from the Home Location view to the same position in the Shop Location view and then change the text content to Enter the location an item is expected to be found at the shop. E.g. ‘Produce Section’ or ‘Deli’.

9. With the Name Text Field and Text View selected on the Shop Location view, click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints.

10. Set the Custom Class of the Shop Location view controller to LocationAtShopVC using Identity Inspector (Option+image+3).

11. Set the Custom Class of the Shop Locations table view controller to LocationsAtShopTVC using Identity Inspector (Option+image+3).

12. Hold down Control and drag a line from the Done button found on the Shop Locations table view controller to the yellow circle at the bottom of the same view, and then select Sent Actions > done.

13. Hold down Control and drag a line from the Done button found on the Shop Location view controller to the yellow circle at the bottom of the same view, and then select Sent Actions > done.

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

15. Set the Assistant Editor to Automatic > LocationAtShopVC.h using the approach demonstrated previously in Figure 6.14.

16. Hold down Control and drag a line from the Shop Location Name text field to the existing nameTextField property found in LocationAtShopVC.h. Use the approach demonstrated previously in Figure 6.14.

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

The means to edit units, home locations, and shop locations is now in place. Run the application to examine your handiwork!

The expected view hierarchy is shown in Figure 6.15. Arrange the new buttons down the right side of the item view to match.

Image

Figure 6.15 The Grocery Dude view hierarchy

Summary

The application is really starting to take shape, as the ability to edit items, units, home locations, and shop locations has been implemented. You’ve been shown how to pass an object between views and how to ensure the user interface is refreshed with the latest information available. In addition, you’ve also been shown how to perform a data-integrity check as a part of transitioning from a view, as demonstrated with the ensureItemHomeLocationIsNotNull and ensureItemShopLocationIsNotNull methods. Note that nothing prevents the user from inputting objects with the same name. De-duplication will be covered as a part of Chapter 15, “Taming iCloud.”

Exercises

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

1. Manually add some new items with new sections and then run the application again. (Hint: Copy the code from Listing 5.11, found in the previous chapter, to the demo method of AppDelegate.m.)

2. Select some items on the Prepare tab so they go orange. Change to the Shop tab and examine the section they’re placed in. This feature is the crux of Grocery Dude—all you have to do is tap an item and it’s already organized by its location in the Shop tab.

3. Test out the Clear feature on the Prepare and Shop tabs.