Search
Follow
Recent Comments

Entries in notification (3)

Monday
Sep132010

Repeating an iOS local notification 

when I wrote about local notifications one thing that I left out was the ability to schedule a repeating notification. One of the reasons I did not bother to mention the ability to set a repeat interval is that the function is very limited. At least it is with iOS 4.1 at the time of writing. To show what can and can not be done I will modify the original sample app to include the ability to set a repeat interval.

Setting the repeat interval

The modified UI for the sample app now looks as below. A segmented control has been added to allow a repeat schedule to specified. The possible options are to repeat every minute, hourly, daily or weekly. If you think this is a limited set of options you will see that this not only down to my limited UI design skills but also because the possible options supported by local notifications are limited.

Other than modifying the XIB file the code changes to implement the repeat interval are minor. The method that creates the local notification (scheduleNotification) has the following additional lines:

    NSInteger index = [scheduleControl selectedSegmentIndex];
    switch (index) {
        case 1:
            notif.repeatInterval = NSMinuteCalendarUnit;
            break;
        case 2:
            notif.repeatInterval = NSHourCalendarUnit;
            break;
        case 3:
            notif.repeatInterval = NSDayCalendarUnit;
            break;
        case 4:
            notif.repeatInterval = NSWeekCalendarUnit;
            break;
        default:
            notif.repeatInterval = 0;
            break;
    }

 

The local notification object, which is of type UILocalNotification has a property named repeatInterval which determines the repeat interval for the delivery of the notification. The Apple documentation does not really provide much in the way of explanation or example but the repeatInterval is of type NSCalendarUnit which if you check the definition can take one of the following values:

  • NSEraCalendarUnit
  • NSYearCalendarUnit
  • NSMonthCalendarUnit
  • NSDayCalendarUnit
  • NSHourCalendarUnit
  • NSMinuteCalendarUnit
  • NSSecondCalendarUnit
  • NSWeekCalendarUnit
  • NSWeekdayCalendarUnit
  • NSWeekdayOrdinalCalendarUnit
  • NSQuarterCalendarUnit

These are mostly self explanatory. If you set the repeatInterval to NSDayCalendarUnit then the notification will repeat every day at the same time as the initial notification. The definition of intervals such as week, month, quarter, etc. is actually dependent on the calendar used. You can specify a different calendar from the current calendar by setting the repeatCalendar property to an alternate NSCalendar object. In practise I am not sure you will ever find a practical reason to set an alternate calendar.

Limitations

The main limitation of notifications is that the user interface does not provide any way to cancel a repeating notification. So if you create a local notification with an hourly repeat interval it will repeat every hour until the user launches your app and gives you the chance to cancel it programatically. Note that the notification repeats even if the user selects the close/cancel button in the notification alert dialog.

The other obvious limitation is that you can only use one of the NSCalendarUnit repeat intervals. So you can have a repeat interval of one minute or one hour or one day but not five minutes or three hours or two days.

All things considered the ability to set a repeat interval for a local notification is of very limited use as currently implemented. In general I would say the handling of notifications is an area where Apple could make substantial improvements in a future release of iOS. Enhancing the UI to allow the user to view multiple notifications and snooze or cancel a repeating notification would make the feature much more useful. It is interesting to note that the Apple clock app has the ability to snooze an alarm but there is no obvious way to create a similar application with the current version of the public iOS frameworks.

Example code

Not much has changed but you can get the updated Xcode project for the example app here.

Sunday
Aug082010

Checking network connectivity when displaying iPhone iAds

There was a comment to my post about adding iAds to an application that got me thinking about an improvement to the way I handle the iAd BannerView. The comment was about moving the app to the background, entering airplane mode and then moving the app back to the foreground. That is just one example of the more general problem of changing network connectivity. The problem comes after you have successfully requested and received an Ad and shown the banner view. If at that point network connectivity is lost the banner view is left on screen. If the user clicks on the Ad banner they get a network connection error which is not exactly a great user experience.

You could reason that losing network connectivity after you have displayed the ad banner is not very likely and just live with the problem - after all the app still works, the user just gets an error message. Note that you need to have network connectivity to see the add in the first place as it is only moved onscreen when the request for an ad completes with success. However with iOS 4 the chances of network connectivity changing whilst your app is running is much more likely. An app can spend days suspended in the background before being brought back to the foreground. In that time the user can be in a completely different location with or without network connectivity. Leaving the ad banner onscreen when there is no network connection wastes some screen space than can be put to better use and as I said earlier hardly represents the best user experience we can provide.

Using Reachability to Check Network Connectivity

To summarise the problem I want to be able to control when an iAd banner is displayed or hidden based on whether the device has network connectivity. Thus if the device has no network connectivity I do not bother to request an Ad since it will return an error anyway. Likewise if I am showing the banner and lose connectivity I remove it.

The network state of an iPhone can be be monitored using the SCNetworkReachability interface of the System Configuration framework. Apple provides the Reachability sample application as an example on how to implement the framework which makes it trivial to add to an existing app. The example app was recently updated for iOS 4.0 so if you already have a copy it may be worth checking you are using the latest version (v2.2).

To get started add the reachability code and SystemConfiguration.Framework to the example app I previously used to demonstrate implementing iAds. Copy the Reachability.h and Reachability.m files from the Apple Reachability sample app and use the Add -> Existing Files and Add->Existing Frameworks options in Xcode to add the files and framework to the project:

Reachability Class Setup

The Reachability class provides a convenient wrapper to the SCNetworkReachability interface. To start using it we just need to add an ivar to the application delegate to hold a reachability object and then start the notifier when the application first launches (existing code not shown):

//  tabnavAppDelegate.h

@class Reachability;

@interface TabnavAppDelegate : NSObject <UIApplicationDelegate, UITabBarControllerDelegate> {
    ...
    ...
    Reachability *netReach;
}
...
...
@property (nonatomic, retain) Reachability *netReach;

 

//  tabnavAppDelegate.m
#import "Reachability.h"

@implementation TabnavAppDelegate

@synthesize netReach;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
                                                  (NSDictionary *)launchOptions { 

    self.netReach = [[Reachability reachabilityWithHostName: @"apple.com"] retain];
    [netReach startNotifier];
    ...
    ...
}

- (void)dealloc {
    [netReach release];
    ...
    ...
}

 

The reachabilityWithHostName class method takes the name of a host to check - apple.com in this case - and returns a reachability object that we can use to check the network status. The reachability class can also send notifications when the reachability status changes. The startNotifier method instructs the reachability class to start sending reachability changed notifications.

Listening for Reachability Changed Notifications

With the reachability class initialised we can use it in our view controller to check the reachability status and control when to show and hide the iAd BannerView. First I will add a helper method to query the Reachability object to determine if we currently have network connectivity:

- (BOOL)networkReachable {
	
    TabnavAppDelegate *delegate = (TabnavAppDelegate *)[[UIApplication sharedApplication]
                                                        delegate];
    Reachability *netReach = [delegate netReach];

    NetworkStatus netStatus = [netReach currentReachabilityStatus];
    BOOL connectionRequired = [netReach connectionRequired];
	
    if ((netStatus == ReachableViaWiFi) ||
        ((netStatus == ReachableViaWWAN) && !connectionRequired)) {

        return YES;
    }

    return NO;
}

 

This method first retrieves the reachability object we setup in the application delegate and then uses the currentReachabilityStatus and connectionRequired methods to determine network connectivity. There are several possible situations since the device may be connected via WiFi or via the cellular network. Also the cellular network may be available but not actually connected.

We can now use this method to determine the network status when our view controller first loads. If we already have network connectivity we can go ahead and create the iAd BannerView immediately. In addition we will initialise an observer to listen for reachability changed notifications generated by the Reachability class. Each time the network connectivity of the device changes we will get a call to the method reachabilityChanged that we specified when creating the observer:

- (void)viewDidLoad {

    // If the network is reachable create the iAd banner, otherwise we
    // wait until the network becomes reachable
    BOOL reachable = [self networkReachable];
	
    if (reachable) {
        [self createBannerView];
    }

    // Listen for reachability changes
    [[NSNotificationCenter defaultCenter] addObserver:self
                                          selector:@selector(reachabilityChanged:) 
                                          name:kReachabilityChangedNotification
                                          object:nil];
}

 

Note that whenever you add an observer for a notification you need to ensure to remove the observer when your view controller is deallocated:

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    ...
    ...
}

 

To finish up we just need to implement the reachabilityChanged method that is called whenever a reachability notification is received:

- (void)reachabilityChanged:(NSNotification *)note {

    BOOL reachable = [self networkReachable];
	
    if (reachable && (self.bannerView == nil)) {
        [self createBannerView];
    }

    if (!reachable && self.bannerView) {
       [self hideBanner];
       [self releaseBanner];
    }
}

 

This method determines the current reachability status using the helper method we created earlier. Then if the network has just become reachable and we do not currently have a bannerView it is created. Alternatively if network connectivity has just been lost and we currently have a bannerView it is hidden and released. This means that during the life of the application we will add and remove the bannerView as the network connectivity changes.

There are several different strategies we could have used here that would achieve the same result from the users perspective. For example, we could always create the bannerView in viewDidLoad and then call hideBanner and showBanner in reachabilityChanged instead of creating and releasing each time.

To test that everything is working you can try it on a network connected device. Once the test iAd banner appears you can disconnect the network connection using the settings application (or use airplane mode) and you should see the Ad disappear. Likewise when you restore connectivity you should eventually see a new ad banner.

Handling Background App Switching

One other enhancement I considered adding was to release the BannerView when the app is switched to the background and recreate it when moving back to the foreground. This is easy enough to implement by having our view controller listen for UIApplicationDidEnterBackgroundNotification and UIApplicationDidBecomeActiveNotification. However when I checked the background memory usage after releasing the bannerView I found it made very little difference so I have left this out of the sample app.

All Done

That’s all for now you can find the updated sample app here. Let me know what you think.

Saturday
Jul312010

Adding Local Notifications with iOS 4

The Apple Push Notification Service (APNS) was released way back in iOS 3.0. That was only just over a year ago but given the pace of development in the mobile OS market that now seems like ancient history. APNS was a first tentative attempt by Apple to address the demand for background processes. It allows client applications to receive updates from a server without having to be running. Apple takes care of pushing the notification to the device and the user can then decide whether to launch the application or not.

Local Notifications

The big problem with APNS is that it requires you to run a server somewhere that interacts with APNS to push a notification to a device. For applications that already have a client-server architecture such as an IM client this is no big deal. There are also a number of third parties who can provide a cost efficient service for small scale users who do not want the trouble of running their own server. However for many applications this is still way too complicated.

The release of iOS 4 brought a number of new features aimed at adding multitasking support one of which was local notifications. I am not sure if you can really say that local notifications count towards multitasking but when compared to APNS they are much simpler to use. There is no central service in fact you do not even need an internet connection to schedule and deliver notifications to an application.

The Example App - RemindMe

To provide a working example of how to add local notifications to an iOS 4 app I have a created a very simple sample application. The user interface is very simple and will not win any design awards. The basic idea is that you enter a message, set the date time and then click the “Set” button to schedule a reminder. At the designated time a message pops up on the screen to remind you.

I will spare you the basic setup of the app, you can download the code and look at that yourself if you are interested. What I want to focus on is the creation of the local notifications and the way the app interacts with iOS when they are delivered.

Creating a Local Notification

The set button is wired up to run a method called scheduleNotification in the view controller which uses the UILocalNotification class to schedule a notification. The code looks as follows:

- (void)scheduleNotification {
	
    [reminderText resignFirstResponder];
    [[UIApplication sharedApplication] cancelAllLocalNotifications];

    Class cls = NSClassFromString(@"UILocalNotification");
    if (cls != nil) {
		
        UILocalNotification *notif = [[cls alloc] init];
        notif.fireDate = [datePicker date];
        notif.timeZone = [NSTimeZone defaultTimeZone];
		
        notif.alertBody = @"Did you forget something?";
        notif.alertAction = @"Show me";
        notif.soundName = UILocalNotificationDefaultSoundName;
        notif.applicationIconBadgeNumber = 1;
		
        NSDictionary *userDict = [NSDictionary dictionaryWithObject:reminderText.text 
                                                forKey:kRemindMeNotificationDataKey];
        notif.userInfo = userDict;
		
        [[UIApplication sharedApplication] scheduleLocalNotification:notif];
        [notif release];
    }
}

I will walk through the code a few lines at a time highlighting the key points:

Cancel existing notifications

    [reminderText resignFirstResponder];
    [[UIApplication sharedApplication] cancelAllLocalNotifications];

The first couple of lines remove the onscreen keyboard if it is being displayed for the text field (reminderText) and also cancel any existing local notifications. For this app we will only allow a single outstanding notification but iOS 4 will allow you to schedule multiple notifications.

Ensuring backwards compatibility

    Class cls = NSClassFromString(@"UILocalNotification");
    if (cls != nil) {

        UILocalNotification *notif = [[cls alloc] init];

As an example of how to allow the code to compile and run on iOS 3.x I have protected the allocation of the UILocalNotification object using NSClassFromString. If you only need to support iOS 4 you can avoid this and allocate the object directly:

        UILocalNotification *notif = [[UILocalNotification alloc] init];

Setting the fire date and time

        notif.fireDate = [datePicker date];
        notif.timeZone = [NSTimeZone defaultTimeZone];

The fire date is the date and time when the system should deliver the notification. For our example app we just use the NSDate object returned by the date picker. If you specify a date that is in the past (or nil) the notification is delivered immediately. The fire date is evaluated using the timeZone property of the UILocalNotification object. If you do not set the timezone the fire data is considered to be GMT time. Generally for alarm clock style reminders you want to use the local time zone when you create the notification. This means that if you set a fire date of 09:00 and then move to a different time zone the alarm will still fire at 09:00 in the new time zone.

Controlling the alert

        notif.alertBody = @"Did you forget something?";
        notif.alertAction = @"Show me";

You control the alert message displayed when the notification is delivered by setting the alertBody and alertAction properties. If your app supports multiple languages you should of course make these localized strings. The alertBody sets the actual message text displayed in the alert, the alertAction property sets the label of the alert button:

One other nice touch is that the alertAction text is also shown as part of the unlock slider if the device is locked when the notification is delivered. The default “slide to unlock” message is changed so that the alertAction text replaces the word unlock. The system also takes care of changing the case of the initial letter so in our example we end up with “slide to show…”.

If you do not want to show the alert action button you can disable it by setting the hasAction property to NO. Assuming you have set alertBody this will still cause the alert to be shown with a single OK button. Note that without the action button there is no option for the user to launch the application from the alert. (If you do not want to see the alert at all you should set alertBody to nil.)

Setting the alert sound

        notif.soundName = UILocalNotificationDefaultSoundName;

You can choose to play a sound when the alert is displayed. The sound file must be part of the application bundle and cannot be more than 30 seconds long. In this case we just use the default system sound. Apple guidelines state that you should only play a sound when also displaying an alert or changing the application icon badge. This makes sense if you think how confusing it would be for the user hearing the device play a sound with no other indication as to which application is the target of the notification.

Setting the application badge

        notif.applicationIconBadgeNumber = 1;

In addition to showing an alert and playing a sound when the application is delivered you can also set the application icon badge. If you want to increment the existing badge number you can get the current value from the application instance [[UIApplication sharedApplication] applicationIconBadgeNumber].

Sending custom user data

        NSDictionary *userDict = [NSDictionary dictionaryWithObject:reminderText.text
                                               forKey:kRemindMeNotificationDataKey];
        notif.userInfo = userDict;

If you need to pass some custom data you can add it a dictionary that is included with the notification. In this example we use it to send the reminder text that the user entered into the text field. We will retrieve this custom data when the notification is delivered.

Scheduling the notification

        [[UIApplication sharedApplication] scheduleLocalNotification:notif];
        [notif release];

Finally after going though (nearly) all the options you call a method on the application instance to actually schedule the notification after which we can release the allocated notification object.

Handling the Notification

There are three different situations to consider when handling the delivery of a notification to the application:

  • application is not running (either in the foreground or background)
  • application is in the background (suspended or running a background task)
  • application is already running in the foreground (active)

Note that Apple documentation is confusing in that it does not distinguish between the situations where the application is not running at all and when it is in the background.

Application not running

If the application is not actually running when the notification is delivered the system displays the alert if configured with the alertAction button. If the user taps the button the application is launched and the application delegate method application:didFinishLaunchingWithOptions: is called.

Note if your application currently uses applicationDidFinishLaunching you should convert to the newer method in order to access the launch options.

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    Class cls = NSClassFromString(@"UILocalNotification");
    if (cls) {
        UILocalNotification *notification = [launchOptions objectForKey:
                            UIApplicationLaunchOptionsLocalNotificationKey];

        if (notification) {
           NSString *reminderText = [notification.userInfo 
                            objectForKey:kRemindMeNotificationDataKey];
           [viewController showReminder:reminderText];
        }
    }

    application.applicationIconBadgeNumber = 0;

    [window addSubview:viewController.view];
    [window makeKeyAndVisible];

    return YES;
}

As before we first check to see if the UILocalNotification class exists to maintain backward compatibility with iOS 3.x versions. Then we check the launchOptions dictionary to see if it contains a local notification object. If it does we retrieve our custom data from the userInfo dictionary and pass it to the view controller to show the reminder text to the user.

Application is in the background

In the application is the background (either running a background task or suspended) when the notification is delivered the system displays the alert dialog as before. If the users taps the action button the application is brought to the foreground and the application delegate method didReceiveLocalNotification: will be called and passed the notification object:

- (void)application:(UIApplication *)application 
        didReceiveLocalNotification:(UILocalNotification *)notification {

    application.applicationIconBadgeNumber = 0;
    NSString *reminderText = [notification.userInfo
                              objectForKey:kRemindMeNotificationDataKey];
    [viewController showReminder:reminderText];
}

The handling is in this case similar to the situation when the application is launched. The custom data is retrieved from the userInfo dictionary contained in the notification and passed to our viewController method for display to the user.

Application in the foreground

The situation when the application is already running in the foreground is very similar to when it is in the background with the exception that the system does not display the notification alert to the user. The application delegate will have its didReceiveLocalNotification: method called as when it was running in the background. If you want to distinguish between these two situations you can check the application state:

- (void)application:(UIApplication *)application 
        didReceiveLocalNotification:(UILocalNotification *)notification {
	
    UIApplicationState state = [application applicationState];
    if (state == UIApplicationStateInactive) {

        // Application was in the background when notification
        // was delivered.
    }

Downloading the example code

If you would like to download the complete Xcode project for this example you can find it here. I hope it proves useful.