The complexity of our application really lives in the edit workflow shown here. It’s in this workflow that we edit, delete, and change data objects.
The bulk of the complexity of the edit workflow is in the very first UIViewController. To begin with, this UITable ViewController uses static cells, as opposed to prototype cells. In this design, we have a known quantity of cells, and each cell displays a different piece of information.
By utilizing static cells, we do a significant portion of the work of the edit workflow directly in the storyboard. We avoid handling a large amount of complexity in our application when a user selects a cell. In other words, instead of having to figure out which cell was selected, determine the view controller to build, populate that view controller, present it, and so on, we can have the storyboard do the bulk of that work for us. Each cell in this UITableViewController has a segue to another UIViewController. Each of those segues is named, and we can therefore do away with a large portion of code. Our UITableViewController subclass needs to look only at the identifier of the segue and inject the right dependencies.
| - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender |
| { |
| NSString *identifier = [segue identifier]; |
| if ([identifier isEqualToString:@"editRecipeName"]) { |
| [self prepareForEditRecipeNameSegue:segue sender:sender]; |
| } else if ([identifier isEqualToString:@"selectRecipeType"]) { |
| [self prepareForSelectTypeSegue:segue sender:sender]; |
| } else if ([identifier isEqualToString:@"selectNumberOfServings"]) { |
| [self prepareForSetServingsSegue:segue sender:sender]; |
| } else if ([identifier isEqualToString:@"selectIngredients"]) { |
| [self prepareForSelectIngredientsSegue:segue sender:sender]; |
| } else if ([identifier isEqualToString:@"editDescription"]) { |
| [self prepareForDirectionsSegue:segue sender:sender]; |
| } else { |
| ALog(@"Unknown segue: %@", identifier); |
| } |
| } |
Here we see the value of using the ‑prepareForSegue: sender: method only for branching. Had we put all of the flow logic into this one method, it would easily be 100 lines of code or more and be a mess to maintain.
All of the UIViewController instances that are accessed from the edit UITableViewController fall into one of two categories: edit something or select something. Let’s look at an example of both kinds of instance. See the screenshot.
The PPRTextEditViewController is easily the most reused UIViewController. The bulk of a recipe is text, and the text is probably going to need to be edited. As a result, the process of editing in our application must be highly reusable. This is also a great opportunity to use a block callback to assist in the reusability of the PPRTextEditViewController.
| - (void)prepareForEditRecipeNameSegue:(UIStoryboardSegue *)segue |
| sender:(id)sender |
| { |
| id editRecipeNameVC = [segue destinationViewController]; |
| NSString *name = [[self recipeMO] valueForKey:@"name"]; |
| [[editRecipeNameVC textField] setText:name]; |
| |
| [editRecipeNameVC setTextChangedBlock:^ BOOL (NSString *text, |
| NSError **error) { |
| NSIndexPath *path = [NSIndexPath indexPathForRow:0 inSection:0]; |
| UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:path]; |
| [[cell detailTextLabel] setText:text]; |
| [[self recipeMO] setValue:text forKey:@"name"]; |
| |
| return YES; |
| }]; |
| } |
The most interesting part of this segue preparation is the callback. The PPRTextEditViewController is actually quite dumb. Its entire job is to consume the text entered into a text field and listen for either the Done button to be clicked or the Return key to be tapped. When either of those things happens, it takes the text from the UITextField (or the UITextView) and passes it to the callback block.
Note that the block accepts both an NSString and an NSError pointer. The parent, which defines the block, then validates the text and sets the error if there’s a problem. The PPRTextEditViewController receives a pass/fail from the block. In the event of a failure, it displays the error.
If the block returns a pass, the text view controller pops back to its parent.
With this design, we can use the PPRTextEditViewController to edit any piece of text we choose, and it doesn’t need any custom or special handling code. The only thing we need to do for each instance is set up its view in the storyboard and pass in the text that it needs to start with. By customizing the view, we also gain access to which keyboard is appropriate for each text edit, thereby making it easy to reuse this UIViewController for numeric text, email addresses, or virtually any data we can think of.
The second function of the callback block is to update the data object and the edit view. Since we’re using static cells, we must grab a reference to the existing cell and refresh it based on the text being passed into the block.
While there are a couple of “select something” items in the PPREditRecipeViewController, the ingredients selection is by far the most interesting. First, we want to display all of the existing ingredients. To do this, we need a selection view controller. Next, we want to add ingredients, which means we must add an Add Ingredient row when the user taps the Edit button. When the users select the Add Ingredient row, we then have to put them into another view controller that allows them to edit the individual components of an ingredient. In the Add Ingredient view, we then need to allow the user to set the quantity, type of ingredient, and unit of measure. The end result can be seen here:
Later, as we update and enhance our application, this workflow becomes even longer and more complicated. Fortunately, the code stays quite sane with the reuse of view controllers and the use of the storyboard.
Unfortunately, we need to build two specific view controllers for this workflow: one to handle listing the ingredients (including adding and deleting) and one for adding an ingredient. Fortunately, we can get a lot of reuse from our text edit view controller in this part of the application.