Chapter 14: Notifications

The Notification pattern is called the Observer pattern outside of the Cocoa community. Head First Design Patterns covers the Observer pattern, and I did its example in C++. You’ve probably used yourself, since it’s covered in Hillegass’s Cocoa Programming for Mac OS X, Chapter 14.

This looks like a long post, but only the first fifth or so is the post itself. The rest is my code, for those who’d like to see how I used Buck & Yacktman’s code.

The C++ version of this uses an Observer class, so that observing objects must be sub-classes of Observer. The Cocoa version is more flexible, since NSNotificationCenter uses the heterogeneous storage pattern to keep track of its observers, so they are not limited to any pre-defined classes. The observed objects simply create an NSNotification object and post it to an NSNotificationCenter, typically the default NSNotificationCenter, provided by the Cocoa framework. (Come to think of it, the default notification center is probably a Singleton).

I re-worked the Head First Design Patterns example, a weather station, in Cocoa. The program uses temperature, pressure and humidity data for three different purposes. The Cocoa version has a window for the user to enter data, and three other windows that use the data. One displays the current data, the second displays a weather prediction, and the third one maintains statistics of the data. Each of these three windows has its own window controller, which is the observer (aka receiver of notifications).

The point of the Notification or Observer pattern is to decouple the design of the data collection class from the data processing classes. The data collection class only needs to be able to post a notification, and doesn’t even need to know how many observers will get the notification. The data processing classes only need to know how to understand the notification, and don’t need to know anything else about the data collecting class.

The programming went well, but the debugging proved problematic. The observers weren’t updating their windows, and using the debugger didn’t give me any obvious clues. Going through the code again by comparing the book’s code to the the version I typed in didn’t reveal anything unusual. Going over my own code, I realized that I had declared storage for an instance of MYNotificationCenter, and used that instance when declaring the observers. Then when posting notifications, I used the default notification center, which was empty.

So this implementation of a Singleton is flawed, because it allowed me to create a second instance. I also got a number of warnings during compilation, mostly related to the static strings.

The warning-generation static strings were for the weather notification name and the keys for temperature, pressure and humidity. As noted in the post on Chapter 11, this lets the compiler find the typos for you (the string varaible named wrongCApitalization gets flagged by the compiler, but the string constant @"wrongCApitalization" slips though unnoticed). Since the observers didn’t necessarily use all of those strings, the compiler gave a warning, even though all of them were used somewhere. I suppressed the warnings by using the strings in an NSLog statement in each routine that would have gotten a warning. Does anybody have any better suggestions on how to suppress such warnings?

Besides the files from Buck & Yacktman, I had eight files for my four classes: Ch14_NotificationsAppDelegate, CurrentController, StatsController and ForecastController. Here they are in combined *.h, *.m pairs.

Ch14_NotificationsAppDelegate operates the window where the user types in the weather data and posts the notification when the user clicks the “Send Data” button. There is some coupling between this class and the observers, since this class has to register the observers, but that occurs in a single method, applicationDidFinishLaunching:, which gets called once. I could avoid this coupling by adding a new controller class to operate the Sensors window, which woudn’t need to know anything about the observers, while Ch14_NotificationsAppDelegate registered the observers.

//
//  Ch14_NotificationsAppDelegate.h
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@class MYNotificationCenter;

static NSString *TemperatureKeyString = @"temperature";
static NSString *PressureKeyString = @"pressure";
static NSString *HumidityKeyString = @"humidity";
static NSString *WeatherNotificationString = @"weather notification";

@interface Ch14_NotificationsAppDelegate : NSObject <NSApplicationDelegate> {
    NSWindow *sensorWindow;
	NSWindow *forecastWindow;
	NSWindow *statsWindow;
	NSWindow *currentWindow;
	
	NSObject *forecastController;
	NSObject *statsController;
	NSObject *currentController;
	
	NSTextField *temperatureField;
	NSTextField *pressureField;
	NSTextField *humidityField;
}

@property (assign) IBOutlet NSWindow *sensorWindow;
@property (assign) IBOutlet NSWindow *forecastWindow;
@property (assign) IBOutlet NSWindow *statsWindow;
@property (assign) IBOutlet NSWindow *currentWindow;

@property (assign) IBOutlet NSObject *forecastController;
@property (assign) IBOutlet NSObject *statsController;
@property (assign) IBOutlet NSObject *currentController;

@property (assign) IBOutlet NSTextField *temperatureField;
@property (assign) IBOutlet NSTextField *pressureField;
@property (assign) IBOutlet NSTextField *humidityField;

-(IBAction)sendData:(id)sender;

@end

//
//  Ch14_NotificationsAppDelegate.m
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "Ch14_NotificationsAppDelegate.h"
#import "MYNotificationCenter.h"
#import "MYNotification.h"

@implementation Ch14_NotificationsAppDelegate

@synthesize sensorWindow;
@synthesize currentWindow;
@synthesize statsWindow;
@synthesize forecastWindow;

@synthesize currentController;
@synthesize statsController;
@synthesize forecastController;

@synthesize temperatureField;
@synthesize pressureField;
@synthesize humidityField;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	MYNotificationCenter *myNotificationCenter = [MYNotificationCenter defaultCenter];
	[myNotificationCenter addObserver:statsController
							 selector:@selector(updateStats:)
								 name:WeatherNotificationString
							   object:self];
	[myNotificationCenter addObserver:currentController
							 selector:@selector(updateWeather:)
								 name:WeatherNotificationString
							   object:self];
	[myNotificationCenter addObserver:forecastController
							 selector:@selector(updateForecast:)
								 name:WeatherNotificationString
							   object:self];
	
}

- (IBAction)sendData:(id)sender {
	NSLog(@"Handling sendData: action...");
	NSNumber *tempValue = [NSNumber numberWithDouble:[temperatureField doubleValue]];
	NSNumber *pressValue = [NSNumber numberWithDouble:[pressureField doubleValue]];
	NSNumber *humValue = [NSNumber numberWithDouble:[humidityField doubleValue]];
	NSDictionary *weatherInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
								 tempValue, TemperatureKeyString, 
								 pressValue, PressureKeyString,
								 humValue, HumidityKeyString, nil];
	MYNotification *notification = [MYNotification alloc];
	notification = [notification initWithName:WeatherNotificationString
									   object:self
									 userInfo:weatherInfo];
	[[MYNotificationCenter defaultCenter] postNotification:notification];
	
}

@end

CurrentController operates the window that displays the current values of temperature, pressure and humidity, by creating a string that’s passed to an NSTextField.

//
//  CurrentController.h
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import "MYNotification.h"

@interface CurrentController : NSObject {
	IBOutlet NSTextField *currentWeatherField;
}

- (void)updateWeather:(MYNotification *)note;

@end

//
//  CurrentController.m
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "CurrentController.h"
#import "MYNotification.h"
#import "Ch14_NotificationsAppDelegate.h"

@implementation CurrentController

- (void)awakeFromNib {
	NSLog(@"Logging string to supress warnings: %@", WeatherNotificationString);

}

- (void)updateWeather:(MYNotification *)note {
	NSLog(@"Updating current weather");
	NSDictionary *dict = [NSDictionary dictionaryWithDictionary:[note infoDictionary]];
	NSString *theWeather;
	theWeather = [NSString 
				 stringWithFormat:@"The temperature is %3.0f degrees, with a barometric pressure of %5.2f and humidity of %3.0f%%",
				 [[dict objectForKey:TemperatureKeyString] doubleValue],
				 [[dict objectForKey:PressureKeyString] doubleValue],
				 [[dict objectForKey:HumidityKeyString] doubleValue]];
	[currentWeatherField setStringValue:theWeather];
}

@end

StatsController maintains average, maximum and minimum temperature data.

//
//  StatsController.h
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import <Cocoa/Cocoa.h>
#import "MYNotification.h"

@interface StatsController : NSObject {
	IBOutlet NSTextField *meanField;
	IBOutlet NSTextField *maxField;
	IBOutlet NSTextField *minField;
	
	int countOfTemperatures;
	double sumTemperatures;
	double maxTemperature;
	double minTemperature;
}

- (void)updateStats:(MYNotification *)note;

@end

//
//  StatsController.m
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "StatsController.h"
#import "MYNotification.h"
#import "Ch14_NotificationsAppDelegate.h"

@implementation StatsController

- (void)awakeFromNib {
	NSLog(@"Logging strings to supress warnings: %@, %@, %@", 
		  PressureKeyString, HumidityKeyString, WeatherNotificationString);

}

- (void)updateStats:(MYNotification *)note {
	NSLog(@"Updating weather statistics");
	double newTemperature = [[[note infoDictionary] objectForKey:TemperatureKeyString] doubleValue];
	if (countOfTemperatures == 0) {
		countOfTemperatures = 1;
		minTemperature = newTemperature;
		maxTemperature = newTemperature;
		sumTemperatures = newTemperature;
	} else {
		countOfTemperatures++;
		if (newTemperature < minTemperature) minTemperature = newTemperature;
		if (newTemperature > maxTemperature) maxTemperature = newTemperature;
		sumTemperatures += newTemperature;
	}
	[meanField setDoubleValue:(sumTemperatures/countOfTemperatures)];
	[maxField setDoubleValue:maxTemperature];
	[minField setDoubleValue:minTemperature];
}

@end

ForecastController makes a prediction based on the change in pressure from the previous reading, and then decides if the weather will be improving, getting cooler and rainy, or staying the same.

//
//  ForecastController.h
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import <Cocoa/Cocoa.h>

@class MYNotification;

@interface ForecastController : NSObject {
	double lastPressure;
	IBOutlet NSTextField *forecastField;
}

- (void)updateForecast:(MYNotification *)note;

@end

//
//  ForecastController.m
//  Ch14 Notifications
//
//  Created by Norm Hecht.
//  Copyright 2010 Scamp Dog Software. All rights reserved.
//

#import "ForecastController.h"
#import "MYNotification.h"
#import "Ch14_NotificationsAppDelegate.h"

@implementation ForecastController

- (void)awakeFromNib {
	lastPressure = 29.92;
	NSLog(@"Logging strings to supress warnings: %@, %@, %@", 
		  TemperatureKeyString, HumidityKeyString, WeatherNotificationString);
}

- (void)updateForecast:(MYNotification *)note {
	NSLog(@"Updating forecast");
	double newPressure = [[[note infoDictionary] objectForKey:PressureKeyString] doubleValue];
	double dP = newPressure - lastPressure;
	double eps = 1.0e-3;
	if (dP > eps) {
		[forecastField setStringValue:@"Improving weather on the way!"];
	} else if (dP < -eps) {
		[forecastField setStringValue:@"Forecast: Watch out for cooler, rainy weather."];
	} else {
		[forecastField setStringValue:@"More of the same."];
	}
	lastPressure = newPressure;

}

@end
Advertisements
This entry was posted in Debugging hints and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s