Thursday, May 5, 2011

iPhone SDK: Testing Network Reachability

Check Network Resources Before Use

reachability-alert

Using Facebook-connect for iPhone SDK to post stories to Facebook is a great feature to add to your iPhone application. But what happens if the user has no access to the network? If you don’t check the network, the answer is nothing. This leads to user confusion, and it will prevent your app from being approved for the App Store.
So, how do you check? Apple provided a sample application called Reachability which provides the answer. I’ll demonstrate here. In several blog posts people felt that the Reachability sample was overkill for their needs. If you agree, here’s a link to a recipe from The iPhone Developer’s Cookbook that provides an alternate solution. Even if you don’t plan on using it, I recommend reading through the code in the Reachability example.

Project Setup

In an earlier post I used the Facebook-Connect for iPhone SDK to post stories to Facebook. I’ll use the same project to demonstrate how to check that Facebook is reachable. Here’s the project with the API Keys and Template Bundle IDs removed. fbconnect-iphone.zip I’ll include a link to the final project at the end.
Download the Reachability project too. I’ll be importing a class from it to check the network connection status.



Adding the SystemConfiguration Framework

sysconfig
The reachability class uses the SystemConiguration Framework to check network reachability. Adding the framework to the project is easy.
  1. Open Connect.xcodeproj if you haven’t already.
  2. In the Groups & Files area, control click on the Frameworks folder and select Add => Existing Frameworks.
  3. Look in the Frameworks folder for the SystemConfiguration.framework folder and select it.
  4. Click Add to add the framework to your project.

Add the Reachability Class

Now add the Reachability Class from Apple’s sample.
  1. Control click on the Source folder in the project and select Add => Existing Files.
  2. Navigate to the Reachability project, click Classes, and select the files: Reachability.h and Reachability.m.
  3. Click Add, then select Copy items into destination group’s folder (if needed)
  4. Click Add again.

Using the Reachability Class

Now that the framework and class have been added to the project, put them to work in the code. Create a variable to track the internet connection status, and two methods in the SessionViewController.h file.
  1. Open the SessionViewController.h file and import the Reachability class:
    #import "Reachability.h"
  2. Add a variable to track the status:
    NetworkStatus internetConnectionStatus;
  3. Add a property to hold the network status:
    @property NetworkStatus internetConnectionStatus;
  4. Add these two methods:
    - (void)reachabilityChanged:(NSNotification *)note;
    - (void)updateStatus;
At this point, the SessionViewController interface should look like this:
#import "FBConnect/FBConnect.h"
#import "PermissionStatus.h"
#import "Reachability.h" 

@class FBSession;

@interface SessionViewController : UIViewController
    <FBDialogDelegate, FBSessionDelegate, FBRequestDelegate,
 PermissionStatusDelegate> {
        IBOutlet UILabel* _label;
        IBOutlet UIButton* _permissionButton;
        IBOutlet UIButton* _feedButton;
        IBOutlet FBLoginButton* _loginButton;
        FBSession* _session;
        PermissionStatus *permissionStatusForUser;
        NetworkStatus internetConnectionStatus;
}

@property(nonatomic,readonly) UILabel* label;
@property (nonatomic, retain) PermissionStatus *permissionStatusForUser;
@property NetworkStatus internetConnectionStatus;

- (void)askPermission:(id)target;
- (void)publishFeed:(id)target;
- (void)reachabilityChanged:(NSNotification *)note;
- (void)updateStatus;

@end
Finally, the implementation.
  1. Open SessionViewController.m.
  2. Sysnthesize the internetConnectionStatus variable:
    @synthesize internetConnectionStatus;
  3. Define a string for the host name of the resource:
    #define kHostName @"www.facebook.com"
  4. Make the viewDidLoad method look like this:
    - (void)viewDidLoad {
        //Use the Reachability class to determine if the internet can be reached.
        [[Reachability sharedReachability] setHostName:kHostName];
        //Set Reachability class to notifiy app when the network status changes.
        [[Reachability sharedReachability] setNetworkStatusNotificationsEnabled:YES];
        //Set a method to be called when a notification is sent.
        [[NSNotificationCenter defaultCenter] addObserver:self 
    selector:@selector(reachabilityChanged:) 
    name:@"kNetworkReachabilityChangedNotification" object:nil];
            [self updateStatus];
        [_session resume];
        _loginButton.style = FBLoginButtonStyleWide;
    }
    I want to receive notifications if the network status changes, so I used the setNetworkStatusNotificationsEnabled:YES method. However, I found that the reachabilityChanged method wasn’t called when the network status changed. I’ll refactor later to fix the problem.
  5. Implement the new methods:
    - (void)reachabilityChanged:(NSNotification *)note {
        [self updateStatus];
    }
    
    - (void)updateStatus
    {
        // Query the SystemConfiguration framework for the state of the 
    device's network connections.
        self.internetConnectionStatus    =
     [[Reachability sharedReachability] internetConnectionStatus];
        if (self.internetConnectionStatus == NotReachable) {
            //show an alert to let the user know that they can't connect...
            UIAlertView *alert = [[UIAlertView alloc] 
    initWithTitle:@"Network Status" 
    message:@"Sorry, our network guro determined that the network is not available.
     Please try again later." delegate:self cancelButtonTitle:nil 
    otherButtonTitles:@"OK", nil];
            [alert show];
        }  else {
            // If the network is reachable, make sure the login button is enabled.
            _loginButton.enabled = YES;
        }
    }
    To update the user of the status, I display an alert with a message. When status notifications are sent later, either enable the login button or display the alert.
  6. Add the alert delegate method:
    #pragma mark AlertView delegate methods
    - (void)alertView:(UIAlertView *)alertView 
    clickedButtonAtIndex:(NSInteger)buttonIndex {
        _loginButton.enabled = NO;
        [alertView release];
    }
    If the network isn’t available, disable the loginButton. In this app nothing can be done without a connection, so additional information would be in order.
To test the application on the simulator, go to Network Preferences and disable your network connections. While disabled, the alert will be displayed. To test on an iPhone, put the device in Airplane mode. On an iPod, turn off the WiFi connection. I also tested on my device by walking to places where there is no Edge or WiFi network. In my tests, the reachabilityChanged method did not get called. Thanks to Shashi Prabhakar for giving me a clue about how to get it working, see his comment below.

Refactoring to Receive Reachability Changed Notifications

To get the change notifications I had to make several small changes. My initial observations when checking the remoteHostStatus instead of the internetConnectionStatus:
  1. The reachabilityChanged method was being called.
  2. I noticed that when first initialized the remoteHostStatus is always NotReachable.
  3. The internetConnectionStatus returns a positive result before the remoteHostStatus.
  4. Because I am using an alert, sometimes the alert would be displayed twice or not at all when using one status or the other.
As a result, I changed the code to track both the remoteHostStatus and the internetConnectionStatus. I also added a method to initialize the variables.
  1. Open SessionViewController.h and add a variable and property to hold the remoteHostStatus.
    NetworkStatus remoteHostStatus;
  2. Also add a method to initialize the variables.
    - (void)initStatus;
Here’s the complete interface.
#import "FBConnect/FBConnect.h"
#import "PermissionStatus.h"
#import "Reachability.h" 

@class FBSession;

@interface SessionViewController : UIViewController
    <FBDialogDelegate, FBSessionDelegate, FBRequestDelegate, 
PermissionStatusDelegate> {
        IBOutlet UILabel* _label;
        IBOutlet UIButton* _permissionButton;
        IBOutlet UIButton* _feedButton;
        IBOutlet FBLoginButton* _loginButton;
        FBSession* _session;
        PermissionStatus *permissionStatusForUser;
        NetworkStatus internetConnectionStatus;
        NetworkStatus remoteHostStatus;
}

@property(nonatomic,readonly) UILabel* label;
@property (nonatomic, retain) PermissionStatus *permissionStatusForUser;
@property NetworkStatus internetConnectionStatus;
@property NetworkStatus remoteHostStatus;

- (void)askPermission:(id)target;
- (void)publishFeed:(id)target;
- (void)reachabilityChanged:(NSNotification *)note;
- (void)updateStatus;
- (void)initStatus;

@end
Edit the implementation.
  1. Open SessionViewController.m and add the initStatus method
    -(void)initStatus {
        self.remoteHostStatus = 
    [[Reachability sharedReachability] remoteHostStatus];
        self.internetConnectionStatus    =
     [[Reachability sharedReachability] internetConnectionStatus];
    }
  2. Change the viewDidLoad method to call the initStatus method instead of the updateStatus method.
    - (void)viewDidLoad {
        //Use the Reachability class to determine if the internet can be reached.
        [[Reachability sharedReachability] setHostName:kHostName];
        //Set Reachability class to notifiy app when the network status changes.
        [[Reachability sharedReachability] setNetworkStatusNotificationsEnabled:YES];
        //Set a method to be called when a notification is sent.
        [[NSNotificationCenter defaultCenter] 
    addObserver:self selector:@selector(reachabilityChanged:) 
    name:@"kNetworkReachabilityChangedNotification" object:nil];
        [self initStatus];
        [_session resume];
        _loginButton.style = FBLoginButtonStyleWide;
    }
  3. Change the updateStatus method to check both variables.
    - (void)updateStatus
    {
        // Query the SystemConfiguration framework for
     the state of the device's network connections.
        self.remoteHostStatus = 
    [[Reachability sharedReachability] remoteHostStatus];
        self.internetConnectionStatus    = 
    [[Reachability sharedReachability] internetConnectionStatus];
        NSLog(@"remote status = %d, internet status = %d", self.remoteHostStatus, 
    self.internetConnectionStatus);
        if (self.internetConnectionStatus == 
    NotReachable && self.remoteHostStatus == NotReachable) {
            //show an alert to let the user know that they can't connect...
            UIAlertView *alert = 
    [[UIAlertView alloc] initWithTitle:@"Network Status" 
    message:@"Sorry, our network guro determined that the network is not available.
     Please try again later." delegate:self cancelButtonTitle:nil 
    otherButtonTitles:@"OK", nil];
            [alert show];
        } else {
            // If the network is reachable, make sure the login button is enabled.
            _loginButton.enabled = YES;
        }
    }
I left the log statement so that you could see how the values change as the network settings are changed. In the simulator, go to network settings and disable your connections while it is running. The reachabilityChanged method is now being called as the network status changes.
Here’s the original sample project without the API Keys and Template Bundle IDs. fbconnect-iphone-reachable.zip

No comments:

Post a Comment