Friday, November 11, 2011

Location Services : Simulating Locations

This series discusses the iPhone Location Services.  In this post, we learn how to change the default iOS Simulator location and simulate other locations.

Default Location
In the last post, we got the LSDemo app up and running and saw a stream of updates like this in the Xcode console:
2011-11-10 11:44:15.453 LSDemo[20964:f803] newLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:44:15 AM Pacific Daylight Time
2011-11-10 11:44:15.456 LSDemo[20964:f803] oldLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:44:14 AM Pacific Daylight Time
Notice the location each time is <+37.78583400,-122.40641700>.  A simple Google Maps query reveals this corresponds to the Apple store in San Francisco:
Why is the iOS Simulator using this location, and how can we change it?  

To understand where this is coming from, launch LSDemo in the iOS Simulator.  Open the Debug > Location menu and notice that "Custom Location" is checked:
Click on it to reveal the custom location:
Yay!  Now we know why LSDemo keeps reporting a location of <+37.78583400,-122.40641700>.  This is the default custom location that comes with Xcode.


Simulating Locations
What if we would like to simulate another location?  At first glance, the choices provided by the iOS Simulator  Debug > Location menu appear limited:
To simulate a more exotic location, we could manually look up the GPS coordinates, and then paste them each time into the "Custom Location" dialog.  However, this can be tedious.  Fortunately Xcode offers another way to simulate locations.  While the LSDemo is running in iOS Simulator, click the GPS arrow icon (circled in red):
This will reveal a list of pre-programmed alternatives:
For fun, let's select Sydney, Australia.  As the LSDemo app is running in the iOS Simulator, the new location should be reflected in the console log:
2011-11-10 11:52:08.365 LSDemo[21129:f803] oldLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:52:07 AM Pacific Daylight Time
2011-11-10 11:52:09.056 LSDemo[21129:f803] newLocation=<-33.86340000,+151.21100000> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:52:09 AM Pacific Daylight Time
Notice how the location changed, from <+37.78583400,-122.40641700>  to  <-33.86340000,+151.21100000>.  (Color added for emphasis.)  A simple Google Maps query reveals this is indeed Sydney, Australia:
Yay!  We are receiving the new location updates as we expected.  

Depending on the application, a constant stream of updates may be useful.  However, if we just want a location fix, as we will for HelloWorld, the continuous updates will be overkill.  How do we stop the updates once we have our location?  We shall learn that in the next post.

Location Services : iOS Location Services Settings

This series discusses the iPhone Location Services.  In this post, we learn about the iOS Location Services Settings.

iOS Location Services Settings
In Standard Location Provider III, we hooked up the StandardLocationProvider, but we denied LSDemo use of the iPhone Location Services.  As a result, whenever we launch the app, we get an error:
2011-11-10 11:27:50.285 LSDemo[20652:f803] error=Error Domain=kCLErrorDomain Code=1 "The operation couldn’t be completed. (kCLErrorDomain error 1.)"
To fix this, click the Home button on the simulated iPhone and open the Settings app.
Click the "Location Services" setting.  

Notice the location services for the sandbox app "LSDemo" have been disabled.  Turn it On by sliding the switch:

Finally, press the Home button again and re-launch LSDemo app.  In the Xcode console, you should now see a stream of updates like this:
2011-11-10 11:33:29.432 LSDemo[20715:f803] newLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:33:29 AM Pacific Daylight Time
2011-11-10 11:33:29.433 LSDemo[20715:f803] oldLocation=(null)
2011-11-10 11:33:30.278 LSDemo[20715:f803] newLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:33:30 AM Pacific Daylight Time
2011-11-10 11:33:30.280 LSDemo[20715:f803] oldLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:33:29 AM Pacific Daylight Time
(Your locations may vary.)  Yay!  The app is behaving just as we expected.


Disabling Location Services
What happens if we disable Location Services completely?  Let's try.  Click the Home button again and open the Settings app.  This time though, disable all Location Services by sliding the switch at the top to Off:
Then re-launch LSDemo.  You will see this alert popup:
If you click "Cancel" at this point, the app will just hang.  There will be no error and no location updates will be sent.  If we re-launch the app and click "Cancel" a second time, LSDemo will be completely disabled, and the alert will stop appearing altogether.  To restore the alert popup, delete LSDemo from the iPhone simulator entirely and re-launch the app from within Xcode.

This time, click "Settings" to be taken back to the Location Services screen.  Slide the switch to turn them back On, and likewise enable Location Services for LSDemo.  Click the Home button in the simulator and return to the app.  Now, you should again see the familiar stream of updates in the Xcode console:
2011-11-10 11:44:15.453 LSDemo[20964:f803] newLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:44:15 AM Pacific Daylight Time
2011-11-10 11:44:15.456 LSDemo[20964:f803] oldLocation=<+37.78583400,-122.40641700> +/- 5.00m (speed -1.00 mps / course -1.00) @ 11/10/11 11:44:14 AM Pacific Daylight Time
Yay!  Now we know what happens when Location Services are disabled.

Notice the coordinates <+37.78583400,-122.40641700>.  Where is the app getting this location, and how can we change it?  In the next post we shall find out.

Tuesday, November 1, 2011

Location Services : Standard Location Provider III

This series discusses the iPhone Location Services.  In this post, we hook the StandardLocationProvider into the LSDemo app.

LSDemo App
In Standard Location Provider II, we finished implementing the StandardLocationProvider.  Now let's hook it back into the LSDemo app so we can exercise our prototype code.

We want the prototype to execute as though it were running inside a real iPhone app.  Recall that after an app's UIViewController loads its associated views into memory, the viewDidLoad method is invoked to allow for additional initialization steps.  This is where we will instantiate our prototype.

Open the LSDemo view controller header file (ViewController.h) and declare the locationProvider field: 
#import "StandardLocationProvider.h"

@interface ViewController : UIViewController
{
  @private
    StandardLocationProvider *locationProvider;
}

@end
(Again, it is marked @private because we do not need to expose the internals of the class.)  Now open up the corresponding implementation file (ViewController.m) and instantiate the locationProvider:
- (void)viewDidLoad
{
    [super viewDidLoad];
   
    // Do any additional setup after loading the view.
    locationProvider = [[StandardLocationProvider allocinit];
}
Finally, remember to relinquish the locationProvider reference in the viewDidUnload method:
- (void)viewDidUnload
{
    [super viewDidUnload];
   
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    locationProvider = nil;
}
At this point, everything is hooked up.  Build (but don't yet launch) the sandbox app to make sure there are no errors.

iOS Location Services Alert Popup
The first time an iPhone app requests location services from iOS, an alert popup should appear asking the user to allow the service.  As the CLLocationManager documentation explains:
During its initial uses by an application, the Core Location framework prompts the user to confirm that using the location service is acceptable.
Let's verify.  Go ahead and start the LSDemo app.  The following alert popup should appear:

Yay!  Our prototype is behaving as expected.  However, rather than clicking "OK" right away, let's take a small detour.  We shall return here briefly.

Core Location Errors
What happens if we click "Don't Allow" instead?  When the user refuses an app access to location services, we expect the app to be notified via its CLLocationManagerDelegate, specifically the locationManager:didFailWithError: method.  As the documentation explains:
If the user denies your application’s use of the location service, this method reports a kCLErrorDenied error.
Let's verify.  Click the "Don't Allow" button to refuse our prototype access to location services.  The popup should go away, and something like the following should be logged to the console:
2011-11-01 10:58:52.172 LSDemo[276:f803] error=Error Domain=kCLErrorDomain Code=1 "The operation couldn’t be completed. (kCLErrorDomain error 1.)"
An error was generated, as we expected.  But is it the right error?

Let's take a closer look.  From the documentation, we know the error sent back is an instance of NSError.  The key properties of this class are its domain and a domain-specific error code.  Looking at the error message above, we can deduce the following:
  • domain = kCLErrorDomain
  • code = 1
As the Core Location Constants Reference explains, kCLErrorDomain indeed corresponds to the domain for Core Location framework errors.  The error codes for this domain are listed in the CLError enumerated type definition.  Digging deeper, we find an error code of 1 corresponds to the kCLErrorDenied element.  As the documentation confirms, this code means access to the location service was denied by the user.

Yay!  The error generated by clicking "Don't Allow" is consistent with expectations.

Now quit the simulator and try again.  All subsequent attempts to run the sandbox app will result in this error.  What's more, the popup has gone too.  How do we fix this?

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.