Monday, September 26, 2011

Orientations I

This series describes how to improve HelloWorld to support multiple iPhone orientations. In this post, we explore the auto-rotation behavior.


Auto-Rotation
Following the steps in Deploying to your iPhone, HelloWorld now has a home on your iPhone. Saweet! However, there is still room for improvement. For example, many iPhone apps support multiple orientations. That is to say, many iPhone apps function with the phone held sideways (Landscape orientation), and some even upside down.

What about HelloWorld?  Let's see.  Fortunately the iOS Simulator includes the capability to "rotate" the virtual iPhone to test these alternate orientations.  To confirm, first make sure the Xcode scheme is set to "iPhone Simulator". Then, build and run HelloWorld as usual.  You should see something like this:

To rotate the simulation to the right, select Hardware > Rotate Right, or simply press →:
Or, to rotate left, select Hardware > Rotate Left, or simply press :

As one can see, HelloWorld has a couple issues laying out the UI in either Landscape orientation:
  • the "Hello" button seems to have disappeared, 
  • the remaining widgets are not centered. 
Keep going until the virtual iPhone is upside-down:

HelloWorld does not even seem to recognize this orientation!  (Launch the app on your iPhone to confirm these results.)

Let's solve these problems.  First, let's figure out why the upside-down orientation is ignored.  It should not be any different than upright position. Digging deeper, we find in Managing a View Controller's Interface Orientation that the shouldAutorotateToInterfaceOrientation method of UIViewController is responsible for communicating whether or not an app supports a specified orientation.  As the iPhone is rotated, this method is repeatedly invoked to see if each position is supported. Generally speaking, if this method returns YES, the UI is rotated corrspondingly.

What about HelloWorld? Let's check inside HelloWorldViewController to see how it is implemented:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
Looking at the code, it is now obvious why HelloWorld will not rotate to the upside-down position. We have been explicitly prohibiting it! Let's change the implementation to allow all orientations:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Support all orientations.
    return YES;
}
Build and run the app. Then, rotate the device to the upside-down position:

Yay! HelloWorld now responds to the upside-down orientation. Build and deploy the app to your iPhone to confirm.


Thursday, September 22, 2011

Deploying to your iPhone

This article describes the steps to upload the HelloWorld app to your iPhone and test it on actual hardware.

Testing on your iPhone
In Unit Tests IV, we completed our Logic and Application Tests.  Now, some real-world testing on an actual device is in order. Here is how to upload HelloWorld and test it on an actual iPhone:
  • Enroll in the iOS Developer Program
    • $99 annual membership fee.
  • Provision your iPhone for generic development (steps for Xcode 4.2):
    • Open the Xcode Organizer tool (Window > Organizer).
    • In the Library section, select Provisioning Profiles.
    • Check the box at the bottom for Automatic Device Provisioning.
    • Connect your iPhone to your Mac.
    • Click on your iPhone in the Devices section.
    • Click the Add to Portal button at the bottom.
    • When prompted for an iOS Distribution Certificate, click Send Request.
Wait while Xcode communicates with Apple. When it is finished, you should see a new iOS Provisioning Profile in the Library section. (You can also view your Provisioning Profile in the iOS Provisioning Portal at the Apple Developer Member Center.)

Now we can deploy HelloWorld on the device. Before disconnecting your iPhone, do the following in Xcode:
  • Open the Edit Scheme... dialog (Product > Edit Scheme... , or simply ⌘<).
  • Select your iPhone as the destination for the HelloWorld scheme
  • Build the app (⌘B).
  • Run the app (⌘R).
HelloWorld should open on your iPhone while you are still connected to Xcode. Enter your name and confirm the result. You may terminate by clicking the Stop button within Xcode. Now you should see HelloWorld installed on your iPhone:


Disconnect your iPhone at any time. Open the app and confirm it behaves as expected.


Setting the App Icon
HelloWorld is great. Before showing it off to your friends, though, let's add some polish. As one can see in the screen-shot above, the HelloWorld app icon is not "sexy" like the others. It is simply a boring white square.

To set the app icon for HelloWorld, open the project again and navigate to the "Summary" settings in Xcode:
The iOS Human Interface Guidelines specify the app icon should be a 57x57 PNG file. Right-click the empty "App Icon" and then "Select File" to choose an icon for the HelloWorld app. Build and deploy the app to your iPhone again. Now HelloWorld should have a shiny new icon:


Yay! Launch the app and confirm it behaves as expected.


Thursday, September 15, 2011

Unit Tests IV

This series discusses some approaches to unit testing HelloWorld. In this post, we describe how to maintain test independence. It concludes the Unit Test series.


Test Independence
In Unit Tests III, we implemented an Application Test for HelloWorld. However, there is still one glaring problem with testButtonClickUpdatesLabel -- it does not clean up after itself. Look again:
- (void)testButtonClickUpdatesLabel
{
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];
// Downcast to specific type.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;
// Simulate user entering name in text field.
NSString * const name = @"Ebirah";
UITextField *textField = [hwViewController textField];
[textField setText:name];

// Simulate button click (quick & dirty).
[hwViewController changeGreeting:nil];
// Now check the label value.
UILabel *label = [hwViewController label];
NSString *labelValue = [label text];
NSString *expectedValue = [NSString stringWithFormat:@"Hello, %@!", name];
STAssertEqualObjects(labelValue, expectedValue, nil);
}
Subsequent tests will start with the text field and label already set to @"Ebirah" and @"Hello, Ebirah!", respectively. Testing is most effective when test cases are independent of one another. Otherwise, results from one test can contaminate the others.


To illustrate, let's add another test. Consider the question posed at the end of the Hello World post: What happens if you leave the name field empty? The answer is, the app should display @"Hello, World!". Let's write a test to confirm this:


- (void)testEmptyNameUsesWorld
{

// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];

// Downcast to specific type.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;
// Skip entering name.
// Simulate button click (quick & dirty).
[hwViewController changeGreeting:nil];
// Now check the label value.
UILabel *label = [hwViewController label];
NSString *labelValue = [label text];
NSString *expectedValue = @"Hello, World!";
STAssertEqualObjects(labelValue, expectedValue, nil);

}
Now we have two tests, both of which assume the UI begins in a clean state, and neither of which clean up after themselves. Build and run the tests again. Notice one will fail:
testEmptyNameUsesWorld (...) failed: 'Hello, Ebirah!' should be equal to 'Hello, World!'
testEmptyNameUsesWorld fails because the assumption of a clean UI has been violated -- by testButtonClickUpdatesLabel. In other words, the second test fails because it was contaminated by the first. The text field, which was assumed to be empty, actually still contains @"Hello, Ebirah!".


One way to fix this is to add some cleanup code to testButtonClickUpdatesLabel:
- (void)testButtonClickUpdatesLabel
{
...
// Clear textfield.
[textField setText:@""];


// Clear label.
[label setText:@""];
}
This would seem to solve the problem for the HelloWorld example. Indeed, paste it in as illustrated and the tests will pass.


Unfortunately, this solution is sub-optimal. First, it is brittle because it depends upon the order of test execution. It assumes testEmptyNameUsesWorld comes after testButtonClickUpdatesLabel, which may not always be the case. Second, as more tests are implemented, this code is likely to be duplicated. Duplicated code increases maintenance.


A better solution would be to initialize the UI to a clean state prior to running each test, and encapsulate all the "initialize" logic in a single method. Even better would be if we had some way to do this automatically. Fortunately, the SenTestingKit framework (part of OCUnit, and baked into Xcode 4) answers this challenge: the setUp method. When implemented, the logic in this method is automatically invoked prior to each test, as its name suggests. (In addition, the SenTestingKit framework also recognizes the tearDown method, which is automatically invoked after each test.)


For HelloWorld, the following setUp implementation should suffice:


- (void)setUp
{
[super setUp];
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];
// Downcast to specific type.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;
// Clear textfield.
UITextField *textField = [hwViewController textField];
[textField setText:@""];
// Clear label.
UILabel *label = [hwViewController label];
[label setText:@""];
}
Build and run the tests. Confirm they both now pass.


Final refactoring
One final point could be made about duplicated logic in the HelloWorldAppTests. If you notice, all three methods (setUp and the two test cases) use the same code to obtain their reference to the HelloWorldViewController:
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];

// Downcast to specific type.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;
A better solution is to encapsulate this logic in its own method which all other methods can share. For example:
- (HelloWorldViewController *)getHelloWorldViewController
{
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];


// Downcast to specific type.
return (HelloWorldViewController *)rootViewController;
}
Now, all methods that need to can simply use the following code snippet:
HelloWorldViewController *hwViewController = [self getHelloWorldViewController];
Make sure the getHelloWorldViewController definition appears prior to all methods that invoke it, otherwise you will get build errors.

Unit Tests III

This series discusses some approaches to unit testing HelloWorld. In this article, we implement some simple Logic Tests and Application Tests for the HelloWorld app.

(Note: If you do not already have a 'Tests' group or target within your project, additional configuration will be required that is beyond the scope of this post. As a workaround, create a new project, and make sure 'Include Unit Tests' is checked during the setup process.)

Logic Tests

In Unit Tests II, we factored out a HelloWorldModel class. Let's expand upon this and write some Logic Tests for it. By convention, the Logic Tests for a class called XxxYyyZzz are named XxxYyyZzzTests. So, following the steps outlined in Unit Tests I, create a new Logic Test class named HelloWorldModelTests:




The new test class will contain a skeleton test method.

Generally speaking, the Logic Tests for a class comprise test cases for methods of that class. Tests are written for each individual method, verifying their behavior matches expectations. By convention, if a class has a method called doSomething, a test for that method will be named testDoSomething.

In our case, the HelloWorldModel has a method called initWithName, so our test case will be named testInitWithName. We want to confirm that the name passed to the initWithName constructor is used correctly:
- (void)testInitWithName
{
NSString * const name = @"Godzilla";
HelloWorldModel *model = [[HelloWorldModel alloc] initWithName:name];
STAssertEquals(name, [model userName], nil);
}
The accessors for the userName property can also be tested:

- (void)testSetUserName
{
NSString * const name = @"Mothra";
HelloWorldModel *model = [[HelloWorldModel alloc] init];
[model setUserName:name];
STAssertEquals(name, [model userName], nil);
}
Build and run the tests to make sure they pass. As more methods are added to HelloWorldModel, more test cases should be added to HelloWorldModelTests.

Application Tests
Now let's implement an Application Test for HelloWorld. Typically, Application Test classes have the suffix AppTests. Following the steps outlined in Unit Tests I, create a new Application Test class called HelloWorldAppTests:



As before, the new test class will contain a stub test method.

Generally speaking, Application Tests check the integration of the various classes, rather than focusing on individual methods as Logic Tests do. For UI-driven apps like those on the iPhone, we would like to confirm that specific user actions update the UI in well-defined ways.

Naming conventions for Application Tests are more flexible, but should still be descriptive and start with the phrase test. In the case of HelloWorld, one requirement is that entering a name and clicking the button updates the label text accordingly. Let's call the test case testButtonClickUpdatesLabel:
- (void)testButtonClickUpdatesLabel
{
// Get reference to root viewcontroller.
id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
UIViewController *rootViewController = [[yourApplicationDelegate window] rootViewController];

// Downcast so we may access HelloWorld properties.
HelloWorldViewController *hwViewController = (HelloWorldViewController *)rootViewController;

// Simulate user entering name in text field.
NSString * const name = @"Ebirah";
UITextField *textField = [hwViewController textField];
[textField setText:name];

// Simulate button click (quick & dirty).
[hwViewController changeGreeting:nil];
// Now check the label value.
UILabel *label = [hwViewController label];
NSString *labelValue = [label text];
NSString *expectedValue = [NSString stringWithFormat:@"Hello, %@!", name];
STAssertEqualObjects(labelValue, expectedValue, nil);
}
This test is slightly more verbose but still quite straightforward. The trick is to obtain a reference to the app's UIViewController so that one can then get references to the individual components of the UI, and manipulate them programmatically. Build and run the tests. As written, they should pass fine.

There is still room for improvement. (For example, instead of the "quick & dirty" code used in the test to simulate the button click, we could get a reference to the button itself and fake a "touch event".)* However, there is one glaring problem that needs to be addressed first. What will happen as we add more test cases to HelloWorldAppTests?

*[uiButton sendActionsForControlEvents: UIControlEventTouchUpInside];