Implementing the Application Logic
At
this point, we’ve defined a flash card model and two complete views,
one for creating flash cards and one for using them. All that remains to
have a basic flash card application is to implement the two
controllers.
Show Cards and Capture Results
The class header for the FlashCardsViewController
provides a roadmap for the implementation. It tells us we have to
synthesize 11 properties and implement 5 actions. You may have noticed
that we defined 12 properties in the class definition but we synthesized
only 11. That’s because we need to manually implement the getter for
the read-only currentCard property. To implement this property and get the current flash card, we use the currentCardCounter as the index into the flashCards array, checking to be sure the array isn’t empty. Add the currentCard method, shown in Listing 5, to the FlashCardsViewController.m file.
Listing 5.
-(FlashCard *) currentCard { if (self.currentCardCounter < 0) { return nil; } FlashCard *flashCard = [self.flashCards objectAtIndex:self.currentCardCounter]; return flashCard; }
|
We’ve
now created all 12 properties of our view controller, so let’s start
using them to implement the controller. When the view is first loaded,
we won’t have any flash cards yet, so we need to make sure the UI
behaves properly with no flash cards. Consider for a moment that it’s
possible to get back to this same state of having no cards when the user
deletes the last flash card. So it’s best to handle the case of no
flash cards in the normal flow of the application. Add a method to view
controller called showNextCard,
and make it able to handle populating the UI in each of the three
interesting cases: when there are no flash cards, when there is a next
flash card in the array, and when there is not a next flash card in the
array and so we need to loop back to the beginning of the array of flash
cards. Implement showNextCard as shown in Listing 6.
Listing 6.
-(void)showNextCard {
self.rightButton.enabled = NO; self.wrongButton.enabled = NO;
NSUInteger numberOfCards = [self.flashCards count];
if (numberOfCards == 0) { // UI State for no cards self.question.text = @""; self.answer.text = @""; self.cardCount.text = @"Add a flash card to get started"; self.wrongCount.text = @""; self.rightCount.text = @""; self.deleteButton.enabled = NO; self.actionButton.enabled = NO; } else { self.currentCardCounter += 1; if (self.currentCardCounter >= numberOfCards) { // Loop back to the first card self.currentCardCounter = 0; } self.cardCount.text = [NSString stringWithFormat:@"%i of %i", (self.currentCardCounter + 1), numberOfCards]; self.question.text = self.currentCard.question; self.answer.hidden = YES; self.answer.text = self.currentCard.answer; [self updateRightWrongCounters]; self.deleteButton.enabled = YES; self.actionButton.enabled = YES; } }
|
Because the showNextCard
method can set up the UI, even when we have no cards, handling the
initial load of the view is straightforward. We just need to create and initialize the array that will hold the flash cards and then call the showNextCard method. Uncomment the viewDidLoad method and implement it, as shown in Listing 7.
Listing 7.
- (void)viewDidLoad { self.flashCards = [NSKeyedUnarchiver unarchiveObjectWithFile:[self archivePath]]; self.currentCardCounter = -1; if (self.flashCards == nil) { self.flashCards = [[NSMutableArray alloc] init]; } [self showNextCard]; [super viewDidLoad]; }
|
The showNextCard
method uses outlets to set values on the labels in the UI and to enable
and disable the buttons as appropriate. Careful readers will have
noticed that it also called a method we haven’t written yet, updateRightWrongCounters. This method should provide the right text to the labels based on the counters in the current flash card. Add the method in Listing 8 to the view controller.
Listing 8.
- (void) updateRightWrongCounters { self.wrongCount.text = [NSString stringWithFormat:@"Wrong: %i", self.currentCard.wrongCount]; self.rightCount.text = [NSString stringWithFormat:@"Right: %i", self.currentCard.rightCount]; }
|
Update the FlashCardViewsController.h file in the Classes group with the two methods we just defined:
-(void)showNextCard;
-(void)updateRightWrongCounters;
There are three user actions for progressing through the set of flash cards: nextAction, markWrong, and markRight. nextAction first reveals the answer and enables the Right and Wrong buttons, and the second time nextAction is used on a card, it advances to the next card. Implement nextAction as in Listing 9.
Listing 9.
- (IBAction) nextAction { if (self.answer.hidden) { self.answer.hidden = NO; self.rightButton.enabled = YES; self.wrongButton.enabled = YES; } else { [self showNextCard]; } }
|
The markWrong and markRight
actions increment the counter for the flash card by one. They also
handle disabling the button that was pressed so that the user doesn’t
increment twice for the same card, and they allow the user to change his
mind by decrementing the previously incremented counter. Add the two
methods in Listing 10 to the FlashCardsViewController.m file:
Listing 10.
- (IBAction) markWrong {
// Update the flash card self.currentCard.wrongCount += 1; if (!self.rightButton.enabled) { // They had previously marked the card right self.currentCard.rightCount -= 1; } // Update the UI self.wrongButton.enabled = NO; self.rightButton.enabled = YES; [self updateRightWrongCounters]; }
- (IBAction) markRight {
// Update the flash card self.currentCard.rightCount += 1; if (!self.wrongButton.enabled) { // They had previously marked the card right self.currentCard.wrongCount -= 1; } // Update the UI self.wrongButton.enabled = YES; self.rightButton.enabled = NO; [self updateRightWrongCounters]; }
|
Creating New Cards
We previously created a
separate view and view controller to interact with the user and create a
new card. Add a statement to the FlashCardsViewController.h file to
import the second view controller:
#import "CreateCardViewController.h"
The addCard action that is called when the user touches the Add button instantiates an instance of our CreateCardViewController and turns control over to it with UIView’s presentModalViewController:animated method. Add the action to the FlashCardsViewController.m file as follows in Listing 11.
Listing 11.
- (IBAction) addCard { // Show the create card view CreateCardViewController *cardCreator = [[CreateCardViewController alloc] init]; cardCreator.cardDelegate = self;
[self presentModalViewController:cardCreator animated:YES]; [cardCreator release]; }
|
The addCard IBAction will now show our second view, but we still need to implement the controller for this view. The class header we created for CreateCardViewController tells us we have to synthesize three properties and implement the two actions. The actions simply need to call back to the CardCreateDelegate
when the user presses the Save or Cancel buttons. Click the
CreateCardViewController.m file in the Classes group and update the file
as in Listing 12.
Listing 12.
-(IBAction) save { [self.cardDelegate didCreateCardWithQuestion: question.text answer: answer.text]; }
-(IBAction) cancel { [self.cardDelegate didCancelCardCreation]; }
|
Now we need to implement the card delegate in the FlashCardsViewController.
If the callback indicates the user canceled, then we need to dismiss
only the modal view. When the delegate indicates a new card needs to be
saved, we also must create a new FlashCard
instance. After we create it, we need to insert the new flash card into
the array of cards at the current spot or at the end of the array if we
are on the last card. Then we show the next card in the array (which
will always be the new card we just added). Click the
FlashCardsViewController.m file in the Classes group, and add the two
methods shown in Listing 13 to implement the CreateCardDelegate protocol.
Listing 13.
-(void) didCancelCardCreation { [self dismissModalViewControllerAnimated:YES]; }
-(void) didCreateCardWithQuestion:(NSString *)thisQuestion answer:(NSString *)thisAnswer {
// Add the new card as the next card FlashCard *newCard = [[FlashCard alloc]initWithQuestion: thisQuestion answer:thisAnswer]; if (self.currentCardCounter >= [self.flashCards count]) { [self.flashCards addObject:newCard]; } else { [self.flashCards insertObject:newCard atIndex:(self.currentCardCounter + 1)]; }
// Show the new card [self showNextCard]; [self dismissModalViewControllerAnimated:YES];
}
|
Click the
FlashCardsViewController.h file in the Classes group. To import the
CreateCardViewController interface file, modify the class’s @interface to indicate that we’ve implemented the CreateCardDelegate protocol.
#import <UIKit/UIKit.h>
#import "FlashCard.h"
#import "CreateCardViewController.h"
@interface FlashCardsViewController : UIViewController <CreateCardDelegate> {
Delete Cards
Because we wrote our showNextCard
method to be flexible, deleting a card is just a matter of deleting the
current card from the array and showing the next card with showNextCard. showNextCard
can handle any of the circumstances that may result from this, such as
there being no cards in the array or needing to loop back to the
beginning of the array:
- (IBAction) deleteCard {
[self.flashCards removeObjectAtIndex:currentCardCounter];
[self showNextCard];
}
We’ve now put together a working flash card application that is not too shabby (see Figure 6).
At this point, the FlashCards application does have one fatal flaw:
When the application moves to the background (the iOS equivalent of
“quitting”), all the flash cards the user painstakingly created are
gone! In the next section, we rectify this using object archiving for
data persistence.