Monday, October 24, 2011

Location Services : Standard Location Provider II

This series discusses the iPhone Location Services.  In this post, we complete the StandardLocationProvider implementation.

Algorithm Review
Recall from Standard Location Provider I that these were the steps to using the standard location service:
  • Create an instance of CLLocationManager.
    • Done
  • Configure desiredAccuracy and distanceFilter.
    • Using defaults
  • Assign a CLLocationManagerDelegate.
  • Start the service.
In the previous post we adressed the first two steps.  Now let's proceed to the last two.

Location Manager Delegate
The third step to using the standard location service is assigning a delegate object to the location manager instance.  Let's look closer at CLLocationManagerDelegate.  From the protocol's perspective, there are essentially three kinds of updates:
Notice that many kinds of location services call back through the same delegate protocol.  Since we are only prototyping the standard location service, we are only interested in location events, and so we only need to implement these callback methods:
Let's make it so StandardLocationProvider itself implements his protocol.

First, open the header file (StandardLocationProvider.h) and add the protocol to the class declaration:
@interface StandardLocationProvider : NSObject<CLLocationManagerDelegate>
Then, open the implementation  file (StandardLocationProvider.m) and add these stub methods:
/*
 * CLLocationManagerDelegate protocol.
 */
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    NSLog(@"newLocation=%@", newLocation);
    NSLog(@"oldLocation=%@", oldLocation);
}

/*
 * CLLocationManagerDelegate protocol.
 */
- (void)locationManager:(CLLocationManager *)manager
       didFailWithError:(NSError *)error
{
    NSLog(@"error=%@", error);
}
Now we have our CLLocationManagerDelegate object.  Let's assign it to the CLLocationManager instance.  Add this snippet to the StandardLocationProvider constructor:
[locationManager setDelegate:self];
We shall return to the constructor momentarily.  

Starting the Service
We could just start the location service right after assigning the delegate.  Indeed, since we're prototyping, one might wonder why not.  Let's give it a try and see what happens.  Add this snippet to the StandardLocationProvider constructor (following the delegate assignment):
[locationManager startUpdatingLocation];
At this point the constructor should look something like this:
/*
 * Constructor.
 */
- (id)init
{
    if (self = [super init]) {
        locationManager = [[CLLocationManager alloc] init];
        [locationManager setDelegate:self];
        [locationManager startUpdatingLocation];
    }
    return self;
}
Build and run the sandbox target to confirm there are no errors.

Yay!  We have completed our StandardLocationProvider implementation.  The only thing that remains now is to hook it back into the sandbox "LSDemo" app.  

Friday, October 21, 2011

Location Services : Standard Location Provider I

This series discusses the iPhone Location Services.  In this post, we begin implementing a StandardLocationProvider class.

Algorithm
In Location Services : Getting Started, we built and configured a "LSDemo" target in Xcode so that we may prototype without interfering with "HelloWorld".  Now let's start writing some code.

We will be prototyping the standard and significant-change location services.  Let's start with the standard location service.  As described in the Location Awareness Programming Guide, applications desiring the standard location service should follow these steps:
Once the service is started, the delegate object will be notified asynchronously as location data becomes available.

Standard Location Provider
Our strategy will be to encapsulate the prototype logic for each service in its own class.    Conceptually, we intend on providing a location to HelloWorld at some point.  With this in mind, let's write a "LocationProvider" class for each type of service.

Let's start with the standard location service.  Open Xcode and select File > New > New File... (or simply press ⌘N). Then select "Objective-C class" and click "Next".  This class will provide a location using the standard location service, so let's name it "StandardLocationProvider":

Click "Next", and the subsequent dialog will ask for a destination file group and build target.  By default, "HelloWorld" will be indicated for both.  However, we shall be using this class for prototyping, so set the group and target to "LSDemo" instead:

Click "Create" to finish the process.  The new class files should appear in the Project Navigator.

Location Manager
First, it's clear that we will need an instance of CLLocationManager to manipulate.  Let's make it an instance variable of StandardLocationProvider.  Open the header file (StandardLocationProvider.h)and declare the locationManager field:
#import <CoreLocation/CoreLocation.h>


@implementation StandardLocationProvider
{
    @private
    CLLocationManager *locationManager;
}

@end
As the Objective-C Programming Language explains,  the @private modifier restricts the scope of locationManager so that only instances of StandardLocationProvider can see it.  This way we do not publicly expose the internals of our implementation.  

Now open the implementation file (StandardLocationProvider.m)and define a constructor for StandardLocationProvider which instantiates CLLocationManager for the member field:
/*
 * Constructor.
 */
- (id)init {
    if (self = [super init]) {
        locationManager = [[CLLocationManager alloc] init];
    }
    return self;
}
Build and run the sandbox target to confirm there are no errors.

Configuration
The second step to using the standard location service is configuring the desired accuracy and distance filter on the location manager.  The specific member properties of CLLocationManager are:
  • desiredAccuracy
    • Measured in meters (double).
    • Represents maximum allowed margin of error (+/-) for location measurements.
    • Default value is kCLLocationAccuracyBest.
  • distanceFilter
    • Measured in meters (double).
    • Represents minimum distance location must change to trigger another notification.
    • Default value is kCLDistanceFilterNone.
These settings are where one tunes the standard location service to tailor an application's specific needs.  Since we are still prototyping, our needs are not yet clear; let's use the defaults for now and return to this later.

Build and run the sandbox target to confirm there are no errors.  We shall continue implementing StandardLocationProvider in the next post.

Friday, October 14, 2011

Location Services : Getting Started

This series discusses the iPhone Location Services.  In this post, we set up Xcode so we can begin prototyping.

Introduction
One useful feature of mobile devices is their location-awareness.  Once an application knows your location, it can tailor its services to be more relevant.  Let's make HelloWorld location aware.

The Location Awareness Programming Guide provides a good starting point for learning about the Core Location framework.  In addition to region- and heading-monitoring, the framework offers a couple of ways to get a user's current location:
  • Standard location service
    • Configurable accuracy
    • Power sensitive
    • GPS hardware enabled
    • Configurable notification intervals
  • Significant-change location service
    • Low accuracy
    • Low power 
    • Cell radio only
    • Notifications only for significant changes 
Its not clear yet which we want for HelloWorld.  Let's dive in and see how they work, and what they offer.

Prototyping Target
When prototyping, it is essential that we iterate quickly and independently of the application.  We don't want any unrelated code or dependencies getting in the way.  That way, we can focus our attention on the task at hand and keep variables from being introduced.  

Often the best strategy to accomplish this is to encapsulate the prototype logic in its own class and then execute that code directly, bypassing any other application code.   So we can still iterate rapidly, we shall create a separate "sandbox" target in Xcode that is independent of HelloWorld.  Then, when our prototyping is done, we can very simply include the finished product in the HelloWorld target.  

Start by creating a new target (File > New > New Target... menu):
We want the sandbox target to include all the usual Xcode features (especially iOS Simulator), so let's create an empty iPhone app and use that.  Select "Single View Application" and then click "Next".  Name the target "LSDemo" or something.  Also, since this target is for prototyping, we shall not be writing unit tests, so feel free to un-check the "Include Unit Tests" option.  Finally, click "Finish":

Yay!  Now we have an empty iPhone target containing the following:
  • Stub AppDelegate class
  • Stub ViewController class
  • Empty storyboard
  • Supporting Files
    • Includes main.m
We shall use this dummy application to test our prototyping code.  But first we need to create another scheme.

Prototyping Scheme
When we created the HelloWorld project (see the Hello World post), Xcode also provided us with a default HelloWorld scheme.  However, as we rapidly prototype our sandbox code, we do not wish to be encumbered by the full HelloWorld app.  To accommodate this, lets give LSDemo its own scheme.  

Open the New Scheme... dialog (Product > New Scheme... menu):

Notice it automatically guesses "LSDemo" as the default build target and group name.  Click "OK" to create the scheme.  The new LSDemo scheme will be automatically selected in the Xcode toolbar.  Build and run the sandbox target as you would HelloWorld.  The iOS Simulator should appear with the empty app inside:

Yay!  We shall use this scheme to launch the sandbox build target as we prototype.  

Core Location framework
Finally, we must include the Core Location framework in our project.  Otherwise, Xcode will not be able to find any of the classes in the Core Location library.  Open the project properties and select the "LSDemo" build target.  Then open the Build Phases panel and expand the "Link Binary With Libraries" group:

This shows all the frameworks currently used for the "LSDemo" target.  Click the "+" button and choose "CoreLocation.framework" from the list of libraries:

Click "Add" to complete the transaction.  Confirm the Core Location framework now appears in the "Link Binary With Libraries" group.

A brief word about organizing your project.  Notice when we added "CoreLocation.framework", Xcode places it at the root of the project:
(Circled in red.)  Feel free to drag it down to the "Frameworks" group:
(Circled in red.)  This will not modify any existing associations.  Rather, it merely changes where "CoreLocation.framework" appears in the Project Navigator.  

Yay!  Now it is time to start playing with some actual code.

Sunday, October 9, 2011

Deployment Info

In this post we discuss the iPhone Deployment Info settings for HelloWorld, namely the Launch Image and the Supported Interface Orientations.

Launch Images
In Orientations III, we introduced the "Supported Device Orientations" setting on the Summary panel for the HelloWorld project. Recall that it helps iOS determine the orientation the app should be launched in. However, it didn't seem all that useful, because once it is running, HelloWorld automatically rotates to whatever position the iPhone is held in.

The brief period while the app is launching is essentially "dead time" for the app. No code is being executed, and by default iOS will display simple black screen. We can use this time to throw up a brief "splash screen" to let the user know HelloWorld is loading. This is also called the Launch Image.*

The "Launch Images" setting for HelloWorld appears on the Summary panel as well:
The iOS Human Interface Guidelines specify the launch image should be a 320 x 480 PNG file.  Right-click the empty "Launch Image" and then "Select File" to choose an image for the HelloWorld app.  Xcode will save the file as Default.png and place it at the root of your project folder.  

Build and run the app in iOS Simulator:
Yay!  HelloWorld has a shiny new launch image.  Confirm that after a moment the "spash screen" disappears and the app launches as expected.  If the image flashes too quickly, you may slow down the launch by selecting Debug > Toggle Slow Animations from the iOS Simulator menu.

Supported Interface Orientations
So what about those "Supported Device Orientations"?  Remember the buttons on the Summary panel correspond to the values of the "Supported Interface Orientations" array on the Info panel.  Let's try de-selecting "Portrait" on the Summary panel and see what happens.  The two "Landscape" values are all that should remain:
In order, these are the values of the array:
  • Landscape (left home button)
  • Landscape (right home button)
Click an item drop-down to see the alternate values:
Notice the "Portrait" values refer to the Home button on the top or bottom, while the "Landscape" values refer to appearing on the left or the right.  It is useful then to speak in terms of these interface orientations:
  • BHB = Bottom home button
  • THB = Top home button
  • LHB = Left home button
  • RHB = Right home button
Build and run the app in iOS Simulator.  The virtual iPhone will start in the LHB position:
This makes sense since LHB is the first entry in the "Supported Interface Orientations" array.  The Status Bar is in the correct position, but notice the Launch Image did not rotate.  

Let's continue.  Press the virtual Home button to stop HelloWorld.  The virtual phone should rotate to the BHB position automatically.  Launch HelloWorld again:
This time the image is upright, but the Status Bar appears on the left.  Proceeding with the exploration, press the Home button to stop the app again, and then rotate the iPhone to RHB position.  Launch HelloWorld again:
This time the Status Bar is upside-down and the image still hasn't rotated.  Finally, repeat the process to launch HelloWorld from the THB position:
The image is upside-down and the Status Bar off to the side.  (Remember HelloWorld will correctly auto-rotate after the launch sequence ends.)

A few lessons can be gleaned from this exercise:  
  • Status Bar only regards first element of the "Supported Interface Orientations" array.**
    • If other positions are listed, they are ignored -- even if phone is positioned correspondingly.
    • iOS Simulator also uses first element to determine virtual iPhone rotation.
  • Launch Image disregards "Supported Interface Orientations".
    • Fixed in Portrait (BHB) orientation relative to the Home button.
    • Will not auto-rotate to match other positions.
Take our example.  Even though RHB is listed as the second "Supported Interface Orientation", the Status Bar always launched in LHB mode, even when held in RHB.  And the image remained fixed.

Bearing these in mind, it is easy to see that we only want HelloWorld to launch in the BHB position.  Furthermore, we do not need to worry about any of the others.  Re-enable "Portrait" on the Summary panel and disable the rest.  Build and run the app in iOS Simulator.  It will launch with BHB position:
Rotate to RHB and launch again:
And then to THB and launch again:
Finally around to LHB and launch again:
Yay!  HelloWorld now launches correctly in all orientations.


*Officially, the Launch Image guidelines state that a "splash screen" should be avoided.
**If the "Supported Interface Orientations" array is empty, BHB is the default launch orientation.

Saturday, October 8, 2011

Orientations III

This concludes the series on how to support multiple iPhone orientations. In this post we discuss some advanced Xcode features.


Simulated Metrics
In Orientations II we configured HelloWorld to work in any orientation.  However, making changes to the interface and then building and running the app each time to see how it looks in Landscape mode will get cumbersome as the UI gets more complex.  We could really iterate quickly on the UI if we had some way to see Landscape orientation within the Xcode Interface Builder, rather than needing to build & launch the iOS Simulator and then rotating the phone.  

Fortunately there is a way to do this.  To see, open the MainStoryboard.storyboard file again.  Then, select "Hello World View Controller" in the Document Outline, and then open the Attributes Inspector for the view controller.  Finally, expand the "Simulated Metrics":
The "Orientation" drop-down is shown above, circled in red.  Select the "Landscape" option to rotate the interface to Landscape position:
Yay!  Now we can test changes to the HelloWorld interface without having to build and launch the app every time.  Restore the original setting by selecting "Inferred" from the drop-down.


Supported Device Orientations
In Deploying to your iPhone, recall we used Xcode to set the "App Icon" for HelloWorld.  This setting appeared within the Summary panel for the HelloWorld project.  At the time, you may have noticed the "Supported Device Orientations" setting just above it:
The "Supported Device Orientations" buttons represent each of the four iPhone orientations, and can be toggled on and off independently of each other.  They correspond to the "Supported interface orientations" in the Info panel:
Notice that this setting is actually an array of values.  The order in which the buttons are toggled will change the order of the elements in this array.  To confirm, return to the Summary panel and un-select all the "Supported Device Orientations". Then re-select a few and confirm the order of the "Supported interface orientations" changes accordingly.

What is this used for?  As described in Information Property List Keys: UIKit Keys, the system uses the "Supported Device Orientations" setting in conjunction with the current device position to choose the initial orientation in which to launch the application.  In other words, the list specifies a ranking of orientations to attempt upon launch.  (This applies to apps running in iOS Simulator as well as on a physical device.)  After launch, the application will handle rotation itself via the shouldAutorotateToInterfaceOrientation method, as explained in Orientations I.

We shall try some different launch orientations in the next post.


Tuesday, October 4, 2011

Orientations II

This series describes how to improve HelloWorld to support multiple iPhone orientations. In this post we explore Autoresizing.


Autoresizing
In Orientations I, we enabled auto-rotation for the HelloWorld UI in all orientations.  Now it is time to revisit the issues with the Landscape orientation.  Take another look:
The "Hello" button is off-screen, and what remains is off-center.  We could manually re-draw the UI for every orientation, and in a future article we shall do so.  In the meantime though, the app would be simpler if there were some way to configure HelloWorld to automatically "squeeze" and center the UI when in the Landscape position.  Fortunately for us, there is: autoresizing.

Autoresizing is a feature of the UIKit Framework which allows nested UIVIew(s) to automagically resize themselves when needed.  As described in Handling Layout Changes Automatically Using Autoresizing Rules, the size and position of embedded subviews will adjust to account for changes to their parent view. (The adjustments to the subviews trigger similar layout changes for their own child subviews, and so on.)  This setting is exposed in the code as the autoresizeSubviews property of the UIView, and is set to YES by default. When enabled, the selected view will automatically layout its subviews.

This property is also displayed visually in the Xcode IDE. To see, click on the MainStoryboard.storyboard file in the Project Navigator. Then, select "View" in the Document Outline (expand "Hello World View Controller" if necessary). Finally, open the Attribute Inspector for the view:
The Autoresize Subviews checkbox is circled in red.  As one can see, it is enabled for HelloWorld's main view.  So why doesn't the interface automatically adjust to fit the Landscape position?

Although autoresizing is enabled, it is not the entire story.  As Configuring Your Views to Support Multiple Orientations explains, when autoresizing of a parent view is enabled, the bounds of the child views are modified in accordance with their "autoresizing mask".  This is exposed in the code as the autoresizingMask property of the UIVIew, and takes values from the UIViewAutoresizing constants.

The autoresizing mask can also be manipulated visually in InterfaceBuilder.  To see, open the MainStoryboard.storyboard file again.  Then, select "Button" in the Document Outline  (expand "View" if necessary).  Finally, open the Size Inspector for the button:
The Autosizing box contains a visual representation of the current mask settings:
Solid red lines represent "struts" that fix the object to the boundary, while faint dashed lines represent "spings" that stretch as the boundary moves.  Click on them to toggle on and off.  Mouse over the Autosizing box to animate the Example box and see how the struts and springs affect autoresizing behavior.

For HelloWorld, we want the button to "float" up to the visible area when the phone is rotated.  Toggle off all the "struts" so that the Autosizing box looks like this:
Let's test our changes.  Build and run the app, and rotate the iPhone right to test the Landsape position:
Excellent.  The "Hello" button is visible again.  However, the textfield and label still retain their original fixed positions.  Open the Size Inspector for each and remove their Autosizing struts as well.  Build and run the app again, and rotate to Landscape position:
Awesome.  The textfield is still a little off-center though.  Beneath the Autosizing box there is a drop-down labelled Arrange:
Select the textfield and then choose "Center Vertically".  Just to be sure, center the label and button vertically as well.  Build and run the app again to test these changes.
Yay!  Now HelloWorld works beautifully in both Portrait and Landscape positions.  Build and deploy the app to your iPhone to confirm.