预计阅读本页时间:-
Chapter 4
Creating a User Input Form
WHAT YOU LEARN IN THIS CHAPTER:
- Creating a model object and adding properties
- Building an interactive user interface
- Saving and retrieving data
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
You can find the wrox.com code downloads for this chapter at www.wrox.com/go/begiosprogramming on the Download Code tab. The code is in the chapter 04 download and individually named according to the names throughout the chapter.
In the last chapter you learned how to create a simple iOS application. Though some applications display only information, most require a way for the user to add and edit data. In this chapter, you continue building the Bands app by giving the user a way to add a band and save it.
If you have created desktop applications or web apps, you’re familiar with data input forms. You are also familiar with the objects or classes that represent this data within the code. Typically, you present the user with an interface including text fields, switches, and selectors they can use to enter and manipulate the data objects. iOS applications are no different. In Visual Studio you add user interface objects to a dialog or window and then double-click them to associate methods that handle events as the user manipulates the data. Xcode handles this a bit differently, although the concepts are the same.
The first step is adding the model object, which represents a band.
INTRODUCING THE BAND MODEL OBJECT
As discussed in Chapter 2, “Introduction to Objective-C,” iOS applications use the Model View Controller design pattern. In the Bands app, the model represents a band. Eventually, you’ll have multiple bands represented by the model, so the first step is creating a class that encapsulates all the properties of a band within the application.
The band object needs the following properties:
- Name — The name of the band.
- Notes — Any notes the user would like to attach to the band.
- Rating — How the user rates the band on a scale of 1–10.
- Touring Status — Whether the band is currently touring or if it is disbanded.
- Have Seen — Whether the user has seen the band in concert.
Creating the Band Model Object
The WBABand class will represent the Band model object. The name of the class follows Apples naming convention by adding a three letter prefix to the beginning of the class name. The prefix is a combination of the company name Wrox and the Bands app name, as Apple suggests. You can read more about Apple naming conventions in the Apple Developer Library at https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CodingGuidelines/CodingGuidelines.html.
TRY IT OUT: Creating the WBABand Class
- In Xcode, open the Bands project you created in Chapter 3.
- Select File ⇒ New ⇒ File; then select Objective-C class, as shown in Figure 4-1.
FIGURE 4-1
- In the next dialog, name the class WBABand and set its subclass to NSObject, as shown in Figure 4-2.
FIGURE 4-2
- Save the file with the rest of the project files, and ensure it’s added to the Bands target. Click Create.
How It Works
You created a new class named WBABand, which is a subclass of NSObject. As discussed in Chapter 2, NSObject is the base class for almost all classes in iOS applications. This enables you to use instances of WBABand in NSArrays, which you cover in Chapter 5, “Using Table Views.”
Creating Enumerations
Before you add the properties to the WBABand class you need to declare an enumeration to represent the three different states of touring that a band can have: Touring, Not Touring. and Disbanded.
Enumerations are common in most programming languages. They enable you to declare a type, which consists of named elements. The elements represent simple integers but enable you to use their names in the code to make it readable.
TRY IT OUT: Creating an Enumeration
- In Xcode, select the WBABand.h file from the Project Navigator.
- In the code editor, add the following code to the top of the file after the imports section:
typedef enum {
WBATouringStatusOnTour,
WBATouringStatusOffTour,
WBATouringStatusDisbanded,
} WBATouringStatus; - Save the file and compile the application by selecting Project ⇒ Build from the menu to ensure there are no errors.
How It Works
By adding the typedef enum to the WBABand.h file, you have created a new type named WBATouringStatus, which you can use throughout the code by importing the WBABand.h file. The typical naming convention for enumerations is to start with the same prefix abbreviation you are using for class names followed by the name of the enumeration type with the differentiating value at the end. This helps the readability of your code both for yourself and for any other developer who may work on the application. By default the elements are assigned their integer values based on their placement in the list. For the WBATouringStatus, WBATouringStatusOnTour has a value of 0, WBATouringStatusOffTour has a value of 1, and WBATouringStatusDisbanded has a value of 2.
Adding Properties to the Band Model Object
Now that you have declared the WBATouringStatus enumeration, you have all the types you need to add all the properties of the WBABand class to the code. For the properties to be accessible in the code, add them as properties to the WBABand.h class, as described in the following Try It Out.
TRY IT OUT: Adding Properties to a Class
- Select the WBABand.h file from the Project Navigator.
- Add the following code to the interface:
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *notes;
@property (nonatomic, assign) int rating;
@property (nonatomic, assign) WBATouringStatus touringStatus;
@property (nonatomic, assign) BOOL haveSeenLive; - Save the file and compile the application by selecting Project ⇒ Build from the menu to ensure there are no errors.
How It Works
As you learned in Chapter 2, properties enable you to add member variables to an Objective-C class. The code you added creates all the member variables for the WBABand class. By declaring the enumeration types in the previous section, you could declare a property using that type in the class interface.
You now have the WBABand class created and ready to be used as the model for the Bands app. The next step you learn is how to build a user interface to allow your users to add objects and edit them.
BUILDING AN INTERACTIVE USER INTERFACE
In the previous chapter you added a UILabel to a UIView and set its properties in Xcode using the Attributes Inspector. This is known as setting properties at design time. All user interface objects can be created and set this way, but they cannot be changed in code. To refer to the user interface objects in code you need to learn about the IBOutlet keyword.
Learning About IBOutlet
The IBOutlet keyword stands for Interface Builder Outlet. Xcode uses this keyword to connect objects in the code to objects in the user interface. With the Model-View-Controller design pattern, a UIView is controlled by a UIViewController. The Single View Application template you used to create the Band project included the ViewController class, which is set as the UIViewController for the UIView in the Storyboard. This is where you declare the IBOutlet objects you want to connect to the UIKit objects you add to the UIView, as shown in the following Try It Out.
NOTE When referring to user interface objects, this book will use the name you see in Xcode where appropriate but otherwise will refer to them by their UIKit names. For instance, when you add a new user interface object from the Object library or interact with the Storyboard hierarchy, the user interface objects are labeled by common names such as Label or Text Field in Xcode. In those situations the book will refer to them as Label or Text Field. In most other situations they will be referred to by their UIKit names. If a Try It Out connects an IBOutlet property in code to a UIKit object the How It Works section will use the property name.
TRY IT OUT: Connecting an IBOutlet
- In Xcode, drag a UILabel onto the UIView, use the Interface Builder guidelines to align it at the top and center of the UIView, and then set its text to Band.
- Select the ViewController.h file from the Project Navigator.
- Add the following code to the interface section:
@interface ViewController : UIViewController
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@end - Return to the Main.storyboard, and select the View Controller from the Storyboard hierarchy on the left of the editor.
- Control-drag the line that is shown to the UILabel until both the View Controller and the UILabel are highlighted and connected, as shown in Figure 4-3.
FIGURE 4-3
- Release the mouse button; then select titleLabel from the Outlets dialog.
- Select the ViewController.m file from the Project Navigator.
- Add the following code to the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"titleLabel.text = %@", self.titleLabel.text);
} - Run the application in the simulator. You see titleLabel.text = Band in the console.
How It Works
In the ViewController class interface you declared a UILabel property with the IBOutlet keyword and named it titleLabel. You then used Interface Builder to connect the titleLabel in the code to the UILabel in the UIView. Finally, you printed the text property of the titleLabel to the console at runtime showing the connection was successfully made.
NOTE IBOutlet properties are always created as weak properties instead of strong. This is because the Storyboard is the owner of the object. The code only needs a weak reference to the object.
Using UITextField and UITextFieldDelegate
UILabel is one of the most basic of UIKit objects; however, the bands app needs to enable users to type in text of their own for the band name. For a single line of text you use a UITextField. Keeping with the Model-View-Controller design pattern, the UITextField will ask its controller how it should act. It does this using the UITextFieldDelegate protocol. In the Bands app, the controller for the UITextField is the ViewController class, so it needs to implement the UITextFieldDelegate, as you will see in the following Try It Out.
TRY IT OUT: Adding a UITextField
- In the Main.storyboard, add a new UILabel to the UIView and use the Interface Builder guidelines to align it to the left side of the UIView. Then set its text to Name:.
- Find and drag a new Text Field from the Objects library to the UIView, and align it under the Name UILabel, stretched to the left and right guidelines of the UIView, as shown in Figure 4-4.
FIGURE 4-4
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
#import "WBABand.h"
@interface ViewController : UIViewController <UITextFieldDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@end - Return to the Main.storyboard, and select the View Controller from the Storyboard hierarchy on the left of the editor.
- Connect the nameTextField to the UITextField following the same steps as in the previous section.
- Select the UITextField in the UIView. Then use the same Control-drag procedure, and drag the line back to the View Controller in the Storyboard hierarchy.
- Release the mouse button; then select delegate in the dialog.
- Select the ViewController.m file from the Project Navigator.
- Add the following code to the viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"titleLabel.text = %@", self.titleLabel.text);
self.bandObject = [[BandObject alloc] init];
} - Add the following code to the implementation:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
self.bandObject.name = self.nameTextField.text;
[self.nameTextField resignFirstResponder];
return YES;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
self.bandObject.name = self.nameTextField.text;
[self saveBandObject];
[self.nameTextField resignFirstResponder];
return YES;
} - Run the application in the iPhone 4-inch simulator, and select the UITextField. The software keyboard becomes visible, as shown in Figure 4-5.
FIGURE 4-5
- Enter My Band in the UITextField; then tap the Return key on the software keyboard. The text is entered into the UITextField and the software keyboard is hidden.
How It Works
In the ViewController.h file you added an IBOutlet property for the UITextField named nameTextField along with a WBABand property named bandObject to represent the model. You also declared that the ViewController class implements the UITextFieldDelegate protocol.
Using Interface Builder you added a UILabel and UITextField to the UIView so the user can enter the name of a band. After connecting the UITextField to the nameTextField, you then set its delegate as the ViewController class. You could do this because you declared that the ViewController class implements the UITextFieldDelegate.
Finally, you added code to the ViewController class implementation. In the viewDidLoad method you added code to initialize the bandObject. Then you added methods that implement the UITextFieldDelegate.
The UITextFieldDelegate protocol enables you to handle events that get triggered as the user interacts with the nameTextField. The first method of the protocol you implemented is the textFieldShouldBeginEditing: method, which tells the system that the nameTextField should become the first responder. Being the first responder means that it is the first object to handle any events raised by user interaction. Because you have made a UITextField the first responder, the system shows the software keyboard. You’re not doing any data verification in this code, so you should always return YES. In other applications you may want to validate other pieces of data and prevent the UITextField from becoming the first responder. In those cases, you would return NO.
The second UITextFieldDelegate protocol method you implemented was the textFieldShouldReturn: method. This method gets triggered when the user taps the Return key on the keyboard. In that implementation, the first thing you did was set the name property of the bandObject. The next line resigns the nameTextField as the first responder. Because the first responder is no longer a UITextField, the system hides the keyboard.
The last method of the UITextFieldDelegate protocol you implemented was the textFieldShouldEndEditing: method. This gets called when another object attempts to become the first responder. Its implementation is the same as textFieldShouldReturn:.
COMMON MISTAKES If the keyboard does not hide when you test your app and you press the Return key, make sure you have implemented the textFieldShouldReturn: method and that it resigns the nameTextField as the first responder. Also make sure that the delegate of the nameTextField has been connected to the ViewController class. Missing either of those will cause the keyboard to remain onscreen.
Using UITextView and UITextViewDelegate
For the Bands app, each band has notes associated with it that the user can type in. These notes can be multiple lines. For text that needs multiple lines you use a UITextView. A UITextView is similar in implementation to a UITextField. It asks its controller how it should act through the UITextViewDelegate protocol. Just like the UITextField, the ViewController class will be the delegate for the UITextView.
TRY IT OUT: Adding a UITextView
- In the Main.storyboard, add a new UILabel to the UIView using Interface Builder guidelines to align it to the left side of the UIView. Then set its text to Notes:.
- Find and drag a new Text View from the Objects library onto the UIView. Align it under the Notes UILabel and stretch it to the left and right guidelines of the UIView; then set its height to 90 pixels.
- In the Attributes Inspector, change the background color of the UITextView to Light Gray.
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@end - Return to the Main.storyboard, and select the View Controller from the Storyboard hierarchy on the left side of the editor.
- Connect the notesTextView to the UITextView following the same steps as in the previous section.
- Connect the delegate of the notesTextView to the ViewController using the same steps as in the previous section.
- Select the ViewController.m file from the Project Navigator.
- Add the following code to the implementation:
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
return YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
self.bandObject.notes = self.notesTextView.text;
[self.notesTextView resignFirstResponder];
return YES;
} - Run the application in the iPhone 4-inch simulator. When you tap the notesTextView, the keyboard appears and enables you to enter text.
- Tap the Return button to add a line break to the text.
How It Works
The UITextView and UITextViewDelegate are similar to the UITextField and UITextFieldDelegate. The textViewShouldBeginEditing: method of the UITextViewDelegate protocol tells the system that the notesTextView should become the first responder, which will show the keyboard. The other UITextViewDelegate protocol method you implemented was the textViewShouldEndEditing: method, which gets triggered when another object wants to become the first responder. In its implementation, you set the notes property of the bandObject with the text entered in the notesTextView then resign the notesTextView as the first responder.
The difference between a UITextField and a UITextView is that the Return key in a UITextView adds a line break to the text instead of triggering a delegate method. This presents a problem. How does the user tell a UITextView they are done entering text?
Using UIButton and IBAction
The simplest way for the user to tell a UITextView they are done entering text is to add a UIButton, which when touched resigns the UITextView as the first responder. A UIButton, however, does not have a corresponding UIButtonDelegate. Instead you have to connect the touch event to a method in your code. To make that method visible in Interface Builder you use the IBAction keyword, which is short for Interface Builder Action.
TRY IT OUT: Adding a UIButton
- Select the Main.storyboard from the Project Navigator to open Interface Builder.
- Find and drag a new Button from the Object library, and align it on the right side of the UIView in line with the Notes UILabel.
- In the Attributes Inspector, change the text of the UIButton to Save.
- Also in the Attributes Inspector, uncheck the Enabled checkbox.
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
- (IBAction)saveNotesButtonTouched:(id)sender;
@end - Select ViewController.m, and add the following code to the implementation:
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView
{
self.saveNotesButton.enabled = YES;
return YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
self.bandObject.notes = self.notesTextView.text;
[self.notesTextView resignFirstResponder];
self.saveNotesButton.enabled = NO;
return YES;
}
- (IBAction)saveNotesButtonTouched:(id)sender
{
[self textViewShouldEndEditing:self.notesTextView];
} - Return to the Main.storyboard, and connect the saveNotesButton to the UIButton.
- Control-drag back to the View Controller in the Storyboard hierarchy, and select saveNotesButtonTouched: from the dialog.
- Run the application in the iPhone 4-Inch simulator. When you select the notesTextView, the saveNotesButton becomes enabled. When you tap the saveNotesButton, it becomes disabled and the keyboard is hidden.
How It Works
By creating the IBAction method saveNotesButtonTouched:, you could connect it to the touch event of the saveNotesButton. You also used the enabled attribute of the saveNotesButton to indicate to the user that it’s associated with the notesTextView. When the saveNotesButton is enabled and tapped, the saveNotesButtonTouched: method is called, which resigns the notesTextView as the first responder hiding the keyboard.
Using UIStepper
Many times while building a user interface, you need a user interface object that increases or decreases an integer. You can use a UIStepper for this. A UIStepper is a simple control with a minus button on the left and a plus button on the right. Tapping either adjusts its value up or down. You can use a UIStepper to represent the rating property of the bandObject.
TRY IT OUT: Adding a UIStepper
- In the Main.storyboard add a new UILabel and use the Interface Builder guidelines to align it on the left side of the UIView. Then set its text to Rating:.
- Find and drag a new Stepper from the Object library, and use the Interface Builder guidelines to align it on the left side of the UIView underneath the Rating UILabel.
- In the Attributes Inspector set the minimum value to 0, the maximum value to 10, the current value to 0, and the step value to 1.
- Add another UILabel to the UIView, and align it on the Interface Builder guidelines to the right side of the UIView and vertically centered with the UIStepper; then set its text to 0, as shown in Figure 4-6.
FIGURE 4-6
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
@end - Select ViewController.m and add the following code to the implementation:
- (IBAction)ratingStepperValueChanged:(id)sender
{
self.ratingValueLabel.text =
[NSString stringWithFormat:@"%g", self.ratingStepper.value];
self.bandObject.rating = (int)self.ratingStepper.value;
} - Return to Main.storyboard, and connect the ratingStepper to the UIStepper and the ratingValueLabel to UILabel on the right side of the UIView.
- Connect the ratingStepperValueChanged: method to the ratingStepper.
- Run the application in the iPhone 4-Inch simulator. When you tap the plus and minus buttons of the ratingStepper, its value displays in the ratingValueLabel, as shown in Figure 4-7.
FIGURE 4-7
How It Works
A UIStepper has properties for its minimum and maximum values along with the amount the value should be “stepped” when the user taps the plus and minus buttons. By setting these values you configured the ratingStepper to go to a value as high as 10 and as low as 0, changing by a value of 1 on each step. You also created an IBAction method to connect the code to the Value Changed event of the ratingStepper. When the ratingStepper value changes, the ratingStepperValueChanged: is triggered and updates the text of the ratingLabel reflecting the current value. Because the value of a UIStepper is a double, you need to cast it to an int before setting the rating property of the bandObject.
Using UISegmentedControl
The next property of the WBABand class that you add to the interface is the Touring State. For this you use a UISegmentedControl. The UISegmentedControl has the same look as the UIStepper you added in the last section except you have control over how many segments it has and what the text or image of those segments should be. Each segment acts as its own button and can either stay selected when tapped or it can be “momentary” like the buttons in the UIStepper control. For the Touring State, use a UISegmentedControl that stays selected.
TRY IT OUT
- In the Main.storyboard, add a new UILabel using Interface Builder guidelines to align the label on the left side of the UIView. Then set its text to Touring Status:.
- Find and drag a new Segmented Control from the Object library, and use the Interface Builder guidelines to stretch it to both the left and right sides of the UIView underneath the Touring Status UILabel.
- In the Attributes Inspector, set the number of segments to 3.
- Set the Title of Segment 0 to On Tour.
- Use the segment selector to choose Segment 1, and set its title to Off Tour.
- Use the segment selector to choose Segment 2, and set its title to Disbanded.
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
@end - Select ViewController.m, and add the following code to the implementation:
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender
{
self.bandObject.touringStatus =
self.touringStatusSegmentedControl.selectedSegmentIndex;
} - Go back to Main.Storyboard, and connect the touringStatusSegmentedControl to the UISegmentedControl.
- Connect the touringStatusSegmentedControl to the tourStatusSegmentedControlValueChanged: method.
- Run the application in the iPhone 4-Inch simulator. As you tap the segments of the touringStatusSegmentedControl, you see them become selected while deselecting the others, as shown in Figure 4-8.
FIGURE 4-8
How It Works
You added a UISegmentedControl to the UIView and set it to have three segments correlating to the three touring status values in the WBATouringStatus enumeration. You then connected it to the touringStatusSegmentedControl in the ViewController class. Like a UIButton, a UISegmentedControl does not have a corresponding delegate protocol, so in order to know when the user interacts with it you added an IBAction method named tourStatusSegmentedControlValueChanged:. It gets triggered when the selected segment changes. In its implementation you can use the selectedSegmentIndex property to set the touringStatus property of the bandObject because both the segments and the WBATouringStatus enumeration are 0 based.
Using UISwitch
The last property of the WBABand class you need to add to the user interface is the haveSeen property using a UISwitch. A UISwitch is what you would expect it to be: a user interface object that is either on or off. It too does not have a corresponding delegate, so you need one more IBAction method to know when the user interacts with it.
TRY IT OUT: Adding a UISwitch
- In the Main.storyboard, add a new UILabel using Interface Builder guidelines to align it on the left side of the UIView. Then set its text to Have Seen:.
- Find and drag a new Switch from the Objects library, and add it to the UIView, aligning it with the vertical center of the Have Seen UILabel and the right side of the UIView.
- Select ViewController.h from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
@property (nonatomic, weak) IBOutlet UISwitch *haveSeenLiveSwitch;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender;
@end - Select ViewController.m from the Project Navigator, and add the following code to the implementation:
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender
{
self.bandObject.haveSeenLive = self.haveSeenLiveSwitch.on;
} - Return to Main.Storyboard, and connect the haveSeenLiveSwitch property and haveSeenLiveSwitchValueChanged: method to the UISwitch.
- Run the application in the iPhone 4-inch simulator. You can toggle the haveSeenLiveSwitch off and on to change the value of the haveSeen property of the bandObject, as shown in Figure 4-9.
FIGURE 4-9
How It Works
In the Storyboard you added a new UISwitch to the UIView. You then declared an IBOutlet named haveSeenLiveSwitch and an IBAction method named haveSeenLiveSwitchValueChanged:, that you connected to the UISwitch. In the implementation you set the haveSeenLive property of the bandObject to the value of the haveSeenLiveSwitch.
SAVING AND RETRIEVING DATA
Giving the user the ability to enter data into your app is great but not useful unless you can save the data, retrieve it, and present it back to the user. There are many ways to do this in an iOS application, but the simplest is to use NSUserDefaults. The documented use for NSUserDefaults is to save preferences for an application, but because of its simplicity, it’s often used to save small amounts of data as well. You can use it to save and retrieve an instance of the WBABand class.
Implementing the NSCoding Protocol
Before you can save an instance of the WBABand class, you need to declare that the WBABand class implement the NSCoding protocol and then add the protocol’s two methods, initWithCoder: and encodeWithCoder:, to the WBABand class implementation. These methods give the class a way to encode and decode itself so that it can be archived and saved to persistent storage.
TRY IT OUT: Implementing the NSCoding Protocol
- Select the WBABand.h file from the Project Navigator, and add the following code to the interface:
@interface WBABand : NSObject <NSCoding>
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *notes;
@property (nonatomic, weak) int rating;
@property (nonatomic, weak) WBATouringStatus touringStatus;
@property (nonatomic, weak) BOOL haveSeenLive;
@end - Select the WBABand.m file from the Project Navigator, and add the following code to the implementation:
static NSString *nameKey = @"BANameKey";
static NSString *notesKey = @"BANotesKey";
static NSString *ratingKey = @"BARatingKey";
static NSString *tourStatusKey = @"BATourStatusKey";
static NSString *haveSeenLiveKey = @"BAHaveSeenLiveKey";
@implementation WBABand
-(id) initWithCoder:(NSCoder*)coder
{
self = [super init];
self.name = [coder decodeObjectForKey:nameKey];
self.notes = [coder decodeObjectForKey:notesKey];
self.rating = [coder decodeIntegerForKey:ratingKey];
self.touringStatus = [coder decodeIntegerForKey:tourStatusKey];
self.haveSeenLive = [coder decodeBoolForKey:haveSeenLiveKey];
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:self.name forKey:nameKey];
[coder encodeObject:self.notes forKey:notesKey];
[coder encodeInteger:self.rating forKey:ratingKey];
[coder encodeInteger:self.touringStatus forKey:tourStatusKey];
[coder encodeBool:self.haveSeenLive forKey:haveSeenLiveKey];
}
@end
How It Works
The NSCoding protocol gives an instance of a class a way to encode itself for archiving as well as initializing itself from an archive. In the interface of the WBABand class you declared that it implements this protocol by adding it to its protocol list. The protocol has two methods, encodeWithCoder: and initWithCoder: that you then added to the WBABand class implementation. Both of these methods take an instance of an NSCoder object as an argument which does the actual archiving and unarchiving of the data. How the archiving and unarchiving is implemented in the NSCoder object is not important to the WBABand class. All it needs to do is call the various encode and decode methods of it to package up its member variables using a key-value paring. The primitive data types all have their own encode and decode methods. For the integer and enumeration member variables of the WBABand class you use the encodeInteger:forKey: and decodeIntegerForKey: methods. For the haveSeenLive boolean property you use the encodeBool:forKey: and decodeBoolForKey: methods. For member variables that are instances of NSObject you use the encodeObject:forKey: and decodeObjectForKey: methods. The keys are always an NSString instance. You declared all of the keys as static NSString instances at the beginning of the WBABand.m file before the actual implementation of the class. With these two methods of the NSCoding protocol implemented, the WBABand class is ready to be saved in persistent storage.
Saving Data
To save an instance of the WBABand class to persistent storage you will use the standardUserDefaults, which is a global instance of the NSUserDefaults class. It works a bit like NSCoder in that it uses a key-value paring to save both primitive types and NSObject instances to disk. In order to save an instance of the WBABand class it first needs to be archived into an NSData object, which is an object-oriented wrapper around a byte buffer. To do this you use the NSKeyedArchiver class, which is a subclass of NSCoder. When you call the archiveDataWithRootObject: method it will call the encodeWithCoder: method you implemented in the WBABand class to create an NSData archive. The archive is then stored in the standardUserDefaults using the setObject:forKey: method.
TRY IT OUT: Saving Data Using NSUserDefaults
- Select the ViewController.h file from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
@property (nonatomic, weak) IBOutlet UISwitch *haveSeenLiveSwitch;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender;
- (void)saveBandObject;
@end - Select the ViewController.m file from the Project Navigator, and add the following code before the implementation:
#import "ViewController.h"
static NSString *bandObjectKey = @"BABandObjectKey";
@implementation ViewController - Add the following code to the ViewController implementation:
- (void)saveBandObject
{
NSData *bandObjectData =
[NSKeyedArchiver archivedDataWithRootObject:self.bandObject];
[[NSUserDefaults standardUserDefaults]
setObject:bandObjectData forKey:bandObjectKey];
} - Add a call to the saveBandObject method after setting any of the properties in the previous methods:
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
self.bandObject.name = self.nameTextField.text;
[self saveBandObject];
[self.nameTextField resignFirstResponder];
return YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
self.bandObject.notes = self.notesTextView.text;
[self saveBandObject];
[self.notesTextView resignFirstResponder];
self.saveNotesButton.enabled = NO;
return YES;
}
- (IBAction)ratingStepperValueChanged:(id)sender
{
self.ratingValueLabel.text = [NSString stringWithFormat:@"%g",
self.ratingStepper.value];
self.bandObject.rating = (int)self.ratingStepper.value;
[self saveBandObject];
}
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender
{
self.bandObject.touringStatus =
self.touringStatusSegmentedControl.selectedSegmentIndex;
[self saveBandObject];
}
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender
{
self.bandObject.haveSeenLive = self.haveSeenLiveSwitch.on;
[self saveBandObject];
} - Build the project and make sure there are no errors.
How It Works
You first declared a new method in the ViewController class interface named saveBandObject. In its implementation you archive the bandObject using the archivedDataWithRootObject: method of the NSKeyedArchiver. You then set the archive in the standardUserDefaults using the setObject:forKey: method. The key is an NSString named bandObjectKey that you declared as static in the ViewController.m file. The standardUserDefaults saves to disk at periodic intervals so you do not need to do anything more to get the object written to disk. Finally, you added calls to saveBandObject to all of the IBAction methods to make sure what gets written to disk reflects any changes the user made.
Retrieving Saved Data
Retrieving stored data from NSUserDefaults is basically the reverse of saving. You retrieve the object from standardUserDefaults using the objectForKey: method and the same bandObjectKey. This returns you back the NSData archive. You then use the unarchiveObjectWithData: method of the NSKeyedUnarchiver class to unarchive the data and get back the WBABand instance.
TRY IT OUT: Retrieving Data from NSUserDefaults
- Select the ViewController.h file from the Project Navigator, and add the following code to the interface:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
@property (nonatomic, weak) IBOutlet UISwitch *haveSeenLiveSwitch;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender;
- (void)saveBandObject;
- (void)loadBandObject;
- (void)setUserInterfaceValues;
@end - Select the ViewController.m file from the Project Navigator, and add the following code to the implementation:
- (void)loadBandObject
{
NSData *bandObjectData = [[NSUserDefaults standardUserDefaults]
objectForKey:bandObjectKey];
if(bandObjectData)
self.bandObject =
[NSKeyedUnarchiver unarchiveObjectWithData:bandObjectData];
}
- (void)setUserInterfaceValues
{
self.nameTextField.text = self.bandObject.name;
self.notesTextView.text = self.bandObject.notes;
self.ratingStepper.value = self.bandObject.rating;
self.ratingValueLabel.text = [NSString stringWithFormat:@"%g",
self.ratingStepper.value];
self.touringStatusSegmentedControl.selectedSegmentIndex =
self.bandObject.touringStatus;
self.haveSeenLiveSwitch.on = self.bandObject.haveSeenLive;
} - Modify the viewDidLoad method with the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSLog(@"titleLabel.text = %@", self.titleLabel.text);
[self loadBandObject];
if(!self.bandObject)
self.bandObject = [[BandObject alloc] init];
[self setUserInterfaceValues];
} - Run the application in the iPhone 4-inch simulator, and enter some data.
- Restart the application. The data you entered will be reloaded.
How It Works
In the ViewController interface you declared two new methods named loadBandObject and setUserInterfaceValues. In the implementation of the loadBandObject you first attempt to retrieve an NSData archive from the standardUserDefaults using the bandObjectKey. If there is no archive this call will return nil. If there is, the code then calls the unarchiveObjectWithData: method of the NSKeyedUnarchiver class to unarchive the WBABand instance and set the bandObject property.
In the viewDidLoad method of the ViewController you added a call to loadBandObject, and a check to see if the bandObject property was set using archived data. If the bandObject is nil, the code instantiates a new instance of the WBABand class. Finally you added a call to the setUserInterfaceValues method whose implementation sets up the user interface using the member values of the bandObject.
Deleting Saved Data
To delete data you have saved to standUserDefaults, all you need to do is set the object for the key to nil. Adding delete to the user interface is a little more difficult. Before you delete any data you need to verify with user that they actually want it deleted. The best way to do this in an iOS application is to use a UIActionSheet, which has a “destructive” button that will be red when presented to the user. This way user knows they are about to permanently delete the data.
A UIActionSheet also has its own delegate. When a user taps a button in a UIActionSheet it tells its delegate about it using the actionSheet:clickedButtonAtIndex: method of the UIActionSheetDelegate protocol. For the Bands app the ViewController class will act as the delegate so it needs to implement this protocol.
TRY IT OUT: Deleting Data from NSUserDefaults
- In the Main.storyboard, add a new UIButton to the bottom of the UIView, set its text to Delete, and add an auto layout constraint to anchor the button to the bottom of the view, as shown in Figure 4-10.
FIGURE 4-10
- Select ViewController.h from the Project Navigator, and add the following code:
@interface ViewController : UIViewController <UITextFieldDelegate,
UITextViewDelegate, UIActionSheetDelegate>
@property (nonatomic, strong) WBABand *bandObject;
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UITextView *notesTextView;
@property (nonatomic, weak) IBOutlet UIButton *saveNotesButton;
@property (nonatomic, weak) IBOutlet UIStepper *ratingStepper;
@property (nonatomic, weak) IBOutlet UILabel *ratingValueLabel;
@property (nonatomic, weak) IBOutlet UISegmentedControl
*touringStatusSegmentedControl;
@property (nonatomic, weak) IBOutlet UISwitch *haveSeenLiveSwitch;
- (IBAction)saveNotesButtonTouched:(id)sender;
- (IBAction)ratingStepperValueChanged:(id)sender;
- (IBAction)tourStatusSegmentedControlValueChanged:(id)sender;
- (IBAction)haveSeenLiveSwitchValueChanged:(id)sender;
- (IBAction)deleteButtonTouched:(id)sender;
- (void)saveBandObject;
- (void)loadBandObject;
- (void)setUserInterfaceValues;
@end - Select the WBABand.m file from the Project Navigator, and add the following code to the implementation:
- (IBAction)deleteButtonTouched:(id)sender
{
UIActionSheet *promptDeleteDataActionSheet = [[UIActionSheet alloc]
initWithTitle:nil delegate:self cancelButtonTitle:@"Cancel"
destructiveButtonTitle:@"Delete Band" otherButtonTitles:nil];
[promptDeleteDataActionSheet showInView:self.view];
}
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(actionSheet.destructiveButtonIndex == buttonIndex)
{
self.bandObject = nil;
[self setUserInterfaceValues];
[[NSUserDefaults standardUserDefaults] setObject:nil forKey:bandObjectKey];
}
} - Run the application in the iPhone 4-inch simulator. When you tap the Delete button the UIActionSheet will be presented asking if you want to delete the band, as shown in Figure 4-11. Tapping delete will delete the data from standardUserDefaults.
FIGURE 4-11
How It Works
To allow the user to delete a WBABand instance from the standardUserDefaults, you first added a new UIButton to the UIView. In the ViewController interface you added a new IBAction method named deleteButtonTouched: and connected it to the new UIButton.
The implementation of the deleteButtonTouched: method creates a new UIActionSheet instance named promptDeleteDataActionSheet. A UIActionSheet can have a Cancel button, a Destructive button, and any number of other buttons you would like to add. All of these buttons are optional. For the promptDeleteDataActionSheet you set the text of the Cancel button to Cancel and the text of the destructive button to Delete Band. You also set the ViewController as its delegate.
To know which button the user clicked, you implemented the actionSheet:clickedButtonAtIndex: method of the UIActionSheetDelegate protocol. In its implementation you used the destructiveButtonIndex property of the actionSheet argument and compared it to the buttonIndex argument. If they are equal, you know the user selected the Delete Band button, so the code uses the same bandObjectKey to set the object in standardUserDefaults to nil.
SUMMARY
In this chapter you implemented the WBABand class that will be the model for the Bands app, including the code needed to save and retrieve instances of it from persistent storage. You also learned how to use various UIKit objects to build a user interface, as well as how to use the IBOutlet and IBAction keywords as well as delegates to both set and retrieve values from the user interface. These are all important lessons in building an iOS application. They may be a bit much to fully grasp at this point, but you are well on your way! The next chapter expands on these lessons by adding multiple bands to the model, listing them in a table, and navigating between the table and the user interface you just finished creating.
EXERCISES
- What keyword do you use to connect a UIKit property in a class to a UIKit object in Interface Builder?
- What keyword do you use to connect an event of a UIKit object in Interface Builder to a method in a class?
- What does it mean to be the first responder?
- What protocol do you implement in a class that allows it to be used with the NSKeyedArchiver class?
WHAT YOU LEARNED IN THIS CHAPTER
TOPIC KEY CONCEPTS
Creating the WBABand class iOS applications are built using the Model-View-Controller design pattern. For the Bands app the WBABand class is the model for a Band object.
Using IBOutlets To connect user interface objects in Interface Builder to code you use the IBOutlet keyword. IBOutlet stands for Interface Builder Outlet.
Showing and hiding the software keyboard The software keyboard of an iOS devices is how a user inputs text into an app. The system knows when to show and hide the keyboard depending on what user interface object is the first responder. The first responder in an app is the first object that has the option to handle a user interaction.
Implementing IBActions Methods that get triggered when a user interaction event occurs use the IBAction keyword. IBAction stands for Interface Builder Action.
Storing model objects in NSUserDefaults There are many ways to save data to persistent storage in an iOS app. The simplest way is to use NSUserDefaults.