Saturday, 26 April 2014

Editing [Table Manners]

I’m not going to lie to you; editing is hard. Thats not to say you cant tackle it, and you’re going to add editing to MyStuff. But dont fret, you already have a huge head start. The table view and collection classes do most of theheavy lifting, and most of the code you need to write to support table editing has already been included in your app, thanks to the Master Detail project template. Theres still code you need to write, but mostly you need tounderstand whats already been written and how the pieces fit together.

Editing tables can be reduced to a few basic tasks:

n     Creating and inserting a new item into the table

n     Removing an item from the table 
n     Reorganizing items in a table

n     Editing the details of an individual item

Your app will allow new items to be added, existing items to be removed, and the details of an item to be edited. By default, items in a table cant be reordered. You can enable that feature if you need to, but you wont here.

iOS has a standard interface for deleting and reordering items in a table. You can individually delete items by swiping the row, as shown on the left in 
Figure 5-25, or you can tap the Edit button and enter editing mode. In editingmode, tapping the minus button next to a row will delete it. Tapping the Done button returns the table view to regular viewing. iOS also provides a standard “plus” button for you to use to trigger adding a new item.


Figure 5-25. Table editing interface

These interfaces are part of the table view classes. The only work you need to do is to set up the interface objects to trigger these actions. You’ll start by providing the code to add new objects, then I’ll describe the set up thatenables editing of your table, and finally you’ll write the code to edit the properties of a single MyWhatsit object.


Inserting and Removing Items

Inserting a new item into your list is a two-step process:

1.       Create the new object and add it to your collection.

2.       Inform that table view that you added a new object, and where.

The Master Detail template includes an action method, -insertNewObject:, that does this. The template code, however, doesnt know about your data model so you’ll need to make some small adjustments to create thecorrect kind of object.

In the MSMasterViewController.m implementation file, find the -insertNewObject: method. The template code looks something like this:

-  (void)insertNewObject:(id)sender
{
if (!things)
things = [[NSMutableArray alloc] init]; [things  insertObject:[NSDate date] atIndex:0];
NSIndexPath  *indexPath = [NSIndexPath   indexPathForRow:0 inSection:0]; [self.tableView  insertRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}

The first two lines lazily create your NSMutableArray collection. This handles the case where this is the first object being added to your collection; prior to this point, you might not have a collection array.

The next line satisfies the first step of adding a new item to the table. It creates a new object and adds it at the beginning of the collection (by inserting it at index 0). The only problem is, its the wrong kind of object.Replace that line of code with the following:

static unsigned  int itemNumber  = 1;
NSString *newItemName = [NSString  stringWithFormat:@"My Item %u",itemNumber++]; MyWhatsit *newItem = [[MyWhatsit  alloc] initWithName:newItemName  location:nil]; [things insertObject:newItem atIndex:0];

Your code generates a unique name for the new item (starting with “My Item 1”), uses that name to create a new MyWhatsit object, and inserts that new object into the collection.

The rest of the code remains the same. You’re still inserting your object at the beginning of the collection, so the code that tells the table view that doesnt need to change.

Now, you may be wondering when, and how, the -insertNewObject: message gets sent. After all, you dont send it anywhere and its not an object created in any of the Interface Builder files. The answer to that question can befound in the next section.

Enabling Table Editing

To allow any row in your table to be deleted (via the standard iOS editing features, that is) your data source object must tell the table view that its allowed. If you dont, iOS wont permit that row to be deleted. Your data sourcedoes this via its optional -tableView:canEditRowAtIndexPath:  method. The Master Detail template provided one for you:

-  (BOOL)tableView:(UITableView   *)tableView  canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}

The method provided by the template allows all rows in your table to be editable. By default, “editable” means it can be deleted. If you dont want a row to be editable, return NO.

If -tableView:canEditRowAtIndexPath: returns YES for a row, iOS allows the swipe gesture to delete the row. If you also want to enable “editing mode” for the entire list (where minus signs appear in each row), you hook that up in the navigation bar, provided by the UITableViewController (which your MSMasterViewController inherits). iOS provides all the needed button objects, and most of
the behavior, that you need. All you have to do is turn them on. In your MSMasterViewController
implementation, find the -viewDidLoad method. The beginning of the method should look like this:

-  (void)viewDidLoad
{
[super  viewDidLoad];
self.navigationItem.leftBarButtonItem = self.editButtonItem; UIBarButtonItem *addButton  = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;

The first line calls the superclasss -viewDidLoad: method, so the superclass can do whatever it needs to do when the view objects load.

The next line creates the Edit button you see on the left side of the navigation bar (see Figure 5-23). It sets the left-hand button to the view controllers editButtonItem. The editButtonItem property is a preconfiguredUIBarButtonItem object thats already set up to start and stop the edit action for its table.

The button to create and insert a new item requires a little more set up, but not much. The next line creates a new UIBarButtonItem. It will have the standard iOS “+” symbol (UIBarButtonSystemItemAdd). When the user tapsit, it will send an -insertNewObject: message to this object (self). The last line adds the new toolbar button to the right-hand side of the navigation bar.

Thats it! This is the code that adds the Edit and + buttons to your tables navigation bar. The Edit button takes care of itself, and you configured the + button to send your controller object the -insertNewObject: messagewhen its tapped. In the previous section, you rewrote
-insertNewObject: to insert the correct kind of object.

Its time to try it out. Set your scheme back to the iPhone Simulator and run your app. Try swiping row, or using the Edit button. Add some new items by tapping the + button. Your efforts should look like those in Figure 5-23.

Theres one last detail that you should be aware of. When adding a new object, your code created the object, added it to your data model, and then told the table view what you’d done. When deleting a row, the table view is deciding what row(s) to delete. So how does the actual MyWhatsit
object get removed from the things array? That happens in this data source delegate method, which
was already written for you:

-  (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if  (editingStyle == UITableViewCellEditingStyleDelete)  [things  removeObjectAtIndex:indexPath.row]; [tableView  deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
 else if  (editingStyle == UITableViewCellEditingStyleInsert)  {
// ... insert item  here   ...
}
}

When a user edits a table and decides to delete (or insert) a row, that request is communicated
to your data source object by sending it this message. Your data source object must examine the editingStyle parameter to determine whats happening—a row is being deleted, for example—and take appropriate action. Theaction to take when a row is deleted is to remove the corresponding MyWhatsit object from the array and let the table view know what you did.

Thats all of the code needed to edit your table. Now its time to put the last big piece of the puzzle into place: editing the details of a single item.

Editing Details

To edit the details of an item, you’re going to need to:

1.       Create a view where the user can see all of the details.

2.       Set the values of that view with the properties of the selected item in the table.

3.       Record changes to those values.

4.       Update the table with the new information.

The good news is that you’ve already done half of this work. You already modified the MSDetailViewController to display the name and location properties of a MyWhatsit object, and you added code to fill in the text fields with theproperty values of the selected item (-configureView). Now you just have to add some code to do the next two steps, and your app is nearly done.

Start with the iPhone interface—because the iPad interface is going to work a little differently. Create an action that will respond to changes made to the name and location text fields. Start by adding a method prototype toMSDetailViewController.h:

-  (IBAction)changedDetail:(id)sender;

In the MSDetailViewController.m implementation file, add the actual method:

-  (IBAction)changedDetail:(id)sender
{
if (sender==self.nameField)
self.detailItem.name = self.nameField.text; else if (sender==self.locationField)
self.detailItem.location = self.locationField.text;
}

This action method will be received when either the name or location text field is edited. Its not obvious which of the fields caused the message to be sent, so the code compares the sender parameter (the object thatcaused the action message to be sent) against your two text field connections. If one is a match, you know which text field sent the message and can update the appropriate MyWhatsit property with the new value.

Connect the Did  End Editing message of the two text fields to this action in Interface Builder. Select the Main_iPhone.storyboard file. Select the name property text field. Using the connections inspector, connect its Editing Did End event to the -changedDetail: action of the Detail View Controller (your MSDetailViewController), as shown in Figure 5-26. Repeat with the location
text field.


Figure 5-26. Connecting Editing Did End to changedDetail: action

Now when you edit one of the text fields in the detail view, it will change the property values of the original object, updating your data model. Give it a try.

Make sure your scheme is still set to an iPhone simulator and run your app. Your items appear in the list, shown on the left in Figure 5-27.


Figure 5-27. Testing detail editing

Tapping the Gort item shows you its details. Edit the details of the first row. In the example in
Figure 5.27, I’m changing its name to “Gort statue” and its location to “living room.” Clicking the My Stuff button in the navigation bar returns you to the list. But wait! The Gort MyWhatsit object wasnupdated.

Or was it? You could test this theory by setting a debugger breakpoint in -changedDetail: to see if it was sent (it was). No, the problem is a little more insidious. With your cursor (or finger, if you’re testing this on a real device),drag the list up so it causes the Gort row to disappear briefly underneath the navigation toolbar, as shown on the left in Figure 5-28.



Figure 5-28. Redrawing the first row


Release your mouse/finger and the list snaps back. Notice that the first row now shows the updated values. Thats because your -changedDetail: method changed the property values, but you never told the table view object, so itdidnt know to redraw that row. You need to fix that.

Observing Changes to MyWhatsit

In Chapter 8 I’ll explain the rationale behind data model and view object communications. For now, all you need to know is that when the properties of a MyWhatsit object change, the table view needs to know about it so it canredraw that row.

In theory, this is an easy problem to solve: when the MyWhatsit property is updated, a message needs to be sent to the table view to redraw the table, just like you did when you added or removed an object. In practice, itsa little trickier. The problem is that neither the MyWhatsit object nor MSDetailViewController have a direct connection to the table view object of the MSMasterViewController view. While theres nothing stopping you from adding one and connecting it in Interface Builder or programmatically, in this case theres a cleaner solution.

Theres a software design pattern called the observer pattern. It works like this:

1.       Any object interested in knowing when something happens registeras aobserver.

2.       When something happens, the object responsible posts a notification.

3.       The iOS notification center distributes that notification to all interested observers.

The real beauty of this arrangement is that neither the observers nor the objects posting notifications have to know anything about each other. You’ll use notifications to communicate changes in MyWhatsit objects to theMSMasterViewController. The first step is to design a notification and have MyWhatsit post it at the appropriate time.


Posting Notifications

In your MyWhatsit.h interface file, add this method prototype:

-  (void)postDidChangeNotification;

Towards the top of the file, add this constant definition:

#define  kWhatsitDidChangeNotification                 @"MyWhatsitDidChange"

Switch to your MyWhatsit.m implementation file, and add the method:

-  (void)postDidChangeNotification
{
[[NSNotificationCenter defaultCenter]  
postNotificationName:kWhatsitDidChangeNotification object:self];
}

When received, this method will post a notification named kWhatsitDidChangeNotification. The object of the notification is itself. The name of the notification can be anything you want, you just want to make sure its unique so it isnt confused with a notification used by another object.

Back in your -changedDetail: method (MSDetailViewController.m), add one additional line to the end of the method:

[self.detailItem postDidChangeNotification];

Now whenever you edit the details of your MyWhatsit object, it will post a notification that it
changed. Any object interested in that fact will receive that notification. The very last step is to make
MSMasterViewController observe this notification. 

Observing Notifications

The basic pattern for observing notifications is:

1.       Create a method to receive notification messages.

2.       Become an observer for the specific notification(s) your object is interested in.

3.       Process any notifications received.

4.       Stop observing notifications when you dont need them anymore, or before your object is destroyed.

The first step is simple enough. In your MSMasterViewController.m implementation file, add a
-whatsitDidChangeNotification: method. Start by adding a method prototype at the end of the
@interface  MSMasterViewController () section:

-  (void)whatsitDidChangeNotification:(NSNotification*)notification;

Then, towards the bottom of the @implementation  section, add the actual method:


-  (void)whatsitDidChangeNotification:(NSNotification*)notification
{
NSUInteger  index   = [things indexOfObject:notification.object]; if (index!=NSNotFound)
{
NSIndexPath  *path   = [NSIndexPath   indexPathForItem:index inSection:0]; [self.tableView  reloadRowsAtIndexPaths:@[path]
withRowAnimation:UITableViewRowAnimationNone];
}
}

All notification messages follow the same pattern: -(void)myNotification:(NSNotification*
)theNotificationYou can name your method whatever you want, but it must expect a single
NSNotification object as its only parameter.

The notification parameter has all of the details about the notification. Often you dont care, particularly if your object only wants to know that the notification happened and not exactly why. In this case, you’re interested in theobject property of the notification. Every notification has a name and an object its associated with—often its the object that caused the notification.

The first line of your method looks for the notifications object in your things collection. If the object is a MyWhatsit object in your collection, the -indexOfObject: method will return its index in the collection. If not, it will return theconstant NSNotFound.

If the object is in your table (index!=NSNotFound), then the next two lines of code create an NSIndexPath to the location of that object in your table and then tells the table view to reload (redisplay) that row, sansanimation.

The end result is that whenever the detail view changes a MyWhatsit object, this method will cause the corresponding row of your table to redraw, showing that change. Now you just have to register MSMasterViewControllerwith the notification center so it will receive this message.



Locate the -awakeFromNib method. Right after the code you added to create the test array of things, add this statement:

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(whatsitDidChangeNotification:) name:kWhatsitDidChangeNotification
object:nil];

This message tells the notification center to register this object (self) to receive the
given message (whatsitDidChangeNotification:) whenever a notification with the name kWhatsitDidChangeNotification is posted for any object (nil).

Registering to be a notification observer is very flexible. By passing the nil constant for either the name or object parameters in -addObserver:selector:name:object:, you can request to receive notifications with a given name, for aspecific object, or both. The following table shows the effect of the name and object parameters when becoming an observer.

Notification observer matching

name:
object:
Notifications received

@"Name"

@"Name"nil
nil
objectnilobjectnil

Receive only notifications named @"Name" for the object object
Receive all notifications named @"Name" for anyobject
Receive every notification for the object objectReceive every notification (not recommended)

In this situation, you want to be notified when any MyWhatsit object is edited. Your code then looks at the specific object to determine if its interesting. In other situations, you’ll want to receive notifications only when a specificobject sends a specific notification, ignoring similar notifications from unrelated objects.

Just as important as registering to receive notifications, is to unregister when your object should no longer receive them. For this app, theres no point at which the notifications are irrelevant, but you still must make sure thatyour object is no longer an observer before its destroyed. Leaving a destroyed object registered to receive notifications is a notorious cause of app crashes in iOS. So make absolutely sure your object is removed from thenotification center before it ceases to exist.

Its really easy to ensure this, so you dont have any excuses for not doing it. Just above your
-awakeFromNib method, add this method:

-  (void)dealloc
{
[[NSNotificationCenter defaultCenter]  removeObserver:self];
} 
The -dealloc message is sent to your object just before it is destroyed. In it, you should clean up any “loose ends” that wouldnt be taken care of automatically. This statement tells the notification center that this object is nolonger an observer for any notification. You dont even have to remember what notifications or objects you’d previously ask to observe; this message will undo them all.

Run your app in an iPhone simulator again. Edit an item and return back to the list. This time your changes appear in the list!

Modal vs. Modeless Editing

You’re in the home stretch. In fact, you’re so close to the finish line that you can almost touch it.
Theres only one vexing detail to fix: the iPad interface.

The iPhone interface uses, what software developers call, a model interface: when you tap a row to edit an item, you’re transported to a screen where you can edit its details (editing mode), and then you exit that screen andreturn to the list (browsing mode).

The iPad interface doesnt work like that. Particularly in landscape orientation, you can jump between the master list and the detail view at will. This means you can start editing a title or location and then switch immediately toanother item in the list. This is called a modeless interface.

While this makes for a fluid user experience, its a disaster for your app. If you go to the Main_iPad.storyboard file and connect the Editing Did End events of the name and location text fields to the -changedDetail: message, as you didin the iPhone interface, you can see that it doesnt work.

Go ahead; give it a try. I’ll wait.

Its because the editing of the text field never gets a chance to “end” before you can change to another MyWhatsit object in the list. Fortunately, theres an easy out. Instead of connecting the Editing Did  End events to -changedDetail:, connect the Editing Changed events instead. This event is a “lower level” event thats sent whenever the user makes any change in the text field. Now the rows in the table view will update as the user edits thedetails. 

Little Touches

Polish your app by giving it an icon, just as you did for the EightBall app in the previous chapter. Locate the Learn  iOS Development  Projects folder you downloaded in Chapter 1. Inside the Ch folder you’ll find the MyStuff (Icons) folder. Select the images.xcassets item in the navigator, and then select the AppIcon image group. Drag all of the image files from the MyStuff  (Icons) folder and drop them into the group. Xcode will sort them out.

Your app is finished, but I’d like to take a moment to direct you to other table-related topics.

No comments:

Post a Comment