Tuesday, September 29, 2009

 

little bits of context-free iPhone code

What it says on the label.

First, an example of how to handle Settings in an iPhone app. The way I do it is, I have a "Settings" class, with lots of class get-and-set methods, so at any time you can just call "[Settings getLanguage]"; then, I have a SettingsViewController, to change them. (Yes, you can register them to be changed in the iPhone's Settings app, but since you have to leave your app for that, this is annoying.)

Best of all, you don't have to use Core Data. Instead you can use the even simpler NSUserDefault class, like so:


@interface Settings : NSObject {

}
+(int)getListMax;
+(void) setListMax:(int)value;
+(NSString*)getLanguage;
+(void) setLanguage:(NSString*)language;
+(BOOL) doDownload;
+(void) setDoDownload:(BOOL)yesno;

@implementation Settings

//This method does all the work
+(id) getSettingFor:(NSString*)key withDefault:(id)defaultValue {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
id value = [defaults objectForKey:key];
if (value==nil) {
[defaults setObject:defaultValue forKey:key];
return defaultValue;
}
return value;
}

+(int) getListMax {
return [[Settings getSettingFor:@"listMax" withDefault:[NSNumber numberWithInt:500]] intValue];
}
+(void) setListMax:(int)value {
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithInt:value] forKey:@"listMax"];
}

+(NSString*) getLanguage {
return [Settings getSettingFor:@"language" withDefault:@"en"];
}
+(void) setLanguage:(NSString*)language {
[[NSUserDefaults standardUserDefaults] setObject:language forKey:@"language"];
}

+(BOOL) doDownload {
return [[Settings getSettingFor:@"doDownload" withDefault:[NSNumber numberWithBool:YES]] boolValue];
}
+(void) setDoDownload:(BOOL)yesno {
[[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:yesno] forKey:@"doDownload"];
}

[...no dealloc, as we never create an instance]



Note that you easily lazy-initialize all your defaults in code, and then let the user overwrite them. How, you ask? Via the SettingsViewController. Which you could create using the Layout Manager; but I prefer to do it programmatically, with a TableViewController, as it looks slicker much easier to add a Setting that way. Also, it lets me show you an example of my TableViewCell pattern. And while we're at it, a UIActionSheet example too. Voila:



@implementation SettingsViewController

- (void)viewDidLoad {
[super viewDidLoad];
}

//Note that we save the settings as we leave the view
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[NSUserDefaults standardUserDefaults] synchronize];
}

#pragma mark -
#pragma mark TableView methods

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
//# of settings
return 3;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row==0) { //list max
LabelTextFieldCell *cell = (LabelTextFieldCell *)[tableView dequeueReusableCellWithIdentifier:@"LabelTextField"];
if (cell == nil)
cell = [[[LabelTextFieldCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"LabelTextField"] autorelease];

cell.fieldName = @"listMax";
cell.fieldDisplayName.text = @"List Max";
cell.fieldValue.tag=indexPath.row;
cell.fieldValue.keyboardType = UIKeyboardTypeURL;
cell.fieldValue.autocapitalizationType = UITextAutocapitalizationTypeNone;
cell.fieldValue.delegate=self;
cell.fieldValue.text=[[NSNumber numberWithInt:[Settings getListMax]] stringValue];
}
else if (indexPath.row==1) { // do download
SwitchCell *cell = (SwitchCell *)[tableView dequeueReusableCellWithIdentifier:@"Switch"];
if (cell == nil)
cell = [[[SwitchCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Switch"] autorelease];

cell.fieldName = @"doDownload";
cell.fieldDisplayName.text = @"Download pages";
cell.mySwitch.tag=indexPath.row;
cell.mySwitch.on=[Settings doDownload];
cell.currentController = self; //we do this so we can respond to changes
}
else if (indexPath.row==3) { //language
ButtonCell *cell = (ButtonCell *)[tableView dequeueReusableCellWithIdentifier:@"Button"];
if (cell == nil)
cell = [[[ButtonCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"Button"] autorelease];

cell.fieldName=@"language";
cell.fieldDisplayName.text=@"Language";
cell.button.tag=indexPath.row;
[cell.button setTitle:[Settings getLanguageName] forState:UIControlStateNormal];
[cell.button addTarget:self action:@selector(changeLanguage) forControlEvents:UIControlEventTouchUpInside];
}
else
return nil;
}

#pragma mark -
#pragma mark UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}

- (void)textFieldDidEndEditing:(UITextField *)textField {
if (textField.tag==0) {
//TODO: check that it's a valid int!
[Settings setListMax:[textField.text intValue]];
}
}

#pragma mark -
#pragma mark Switch

-(void) valueChanged:(UISwitch*)aSwitch {
if (aSwitch.tag==1) {
[Settings setDoDownload:aSwitch.on];
}
}

#pragma mark -
#pragma mark Button

-(void) changeLanguage {
UIActionSheet *action = [[UIActionSheet alloc]
initWithTitle:@"Select Language"
delegate:self
cancelButtonTitle:nil
destructiveButtonTitle:nil
otherButtonTitles:nil];

for (NSString* language in [Util getValidLanguageNames]) {
[action addButtonWithTitle:language];
}

[action showInView:self.view];
}

#pragma mark -
#pragma mark UIActionSheetDelegate

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
[Settings setLanguage:[[Util getValidLanguageValues] objectAtIndex:buttonIndex]];

UIButton *button = (UIButton*) [self.tableView viewWithTag:6];
[button setTitle:[actionSheet buttonTitleAtIndex:buttonIndex] forState:UIControlStateNormal];

[actionSheet dismissWithClickedButtonIndex:buttonIndex animated:YES];
[actionSheet release];
}

[...dealloc etc...]



That's pretty straightforward; build a table view, populate it with table view cells, and respond to the actions in the cells. But how do the cells work? Well, first look at their (theoretically abstract) common parent superclass, ITRTableViewCell:



@interface ITRTableViewCell : UITableViewCell {
NSString *fieldName;
UILabel *fieldDisplayName;
UIViewController *currentController;
}

@property (nonatomic, retain) NSString *fieldName;
@property (nonatomic, retain) UILabel *fieldDisplayName;
@property (nonatomic, assign) UIViewController *currentController; //weak reference


@implementation ITRTableViewCell

@synthesize fieldName, fieldDisplayName, currentController;

-(id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
[super initWithFrame:frame reuseIdentifier:reuseIdentifier];

self.fieldDisplayName = [[UILabel alloc] init];
fieldDisplayName.textAlignment = UITextAlignmentLeft;
fieldDisplayName.adjustsFontSizeToFitWidth=YES;
fieldDisplayName.minimumFontSize=8;
fieldDisplayName.numberOfLines=2;
fieldDisplayName.font = [UIFont systemFontOfSize:14];

return self;
}

[...dealloc etc...]


which will make more sense when you look at its use in a subclass:


@interface LabelTextFieldCell : ITRTableViewCell {
UITextField *fieldValue;
}

@property (nonatomic, retain) UITextField *fieldValue;


@implementation LabelTextFieldCell

@synthesize fieldValue;

-(id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
[super initWithFrame:frame reuseIdentifier:reuseIdentifier];

[self.contentView addSubview:fieldDisplayName];

self.fieldValue = [[UITextField alloc] init];
fieldValue.clearsOnBeginEditing = NO;
fieldValue.enablesReturnKeyAutomatically = YES;
fieldValue.returnKeyType = UIReturnKeyNext;
fieldValue.autocapitalizationType = UITextAutocapitalizationTypeSentences;
fieldValue.autocorrectionType = UITextAutocorrectionTypeNo;
fieldValue.enablesReturnKeyAutomatically = YES;
fieldValue.backgroundColor = [self getBackgroundColor];
fieldValue.textColor = [self getTextColor];
fieldValue.textAlignment = UITextAlignmentLeft;
fieldValue.font = [UIFont systemFontOfSize:14];
fieldValue.borderStyle = UITextBorderStyleBezel;
[self.contentView addSubview:fieldValue];

return self;
}

-(void)layoutSubviews {
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame;

frame = CGRectMake(boundsX+10, 0, 140, 30);
fieldDisplayName.frame = frame;

frame = CGRectMake(boundsX+150, 0, 160, 30);
fieldValue.frame = frame;
}

[...dealloc...]


Ya see? The superclass defines the name on the left; the subclass defines the input on the right with which the user interacts. Here are the ButtonCell and SwitchCell implementations:


@implementation ButtonCell

@synthesize button;

-(id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
[super initWithFrame:frame reuseIdentifier:reuseIdentifier];

[self.contentView addSubview:fieldDisplayName];

self.button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.backgroundColor = [self getBackgroundColor];
button.titleLabel.textColor = [self getTextColor];
button.titleLabel.textAlignment = UITextAlignmentLeft;
button.titleLabel.font = [UIFont systemFontOfSize:14];
[self.contentView addSubview:button];

return self;
}

-(void)layoutSubviews {
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame;

frame = CGRectMake(boundsX+10, 0, 140, 30);
fieldDisplayName.frame = frame;

frame = CGRectMake(boundsX+150, 0, 160, 30);
button.frame = frame;
}

[...dealloc...]

@implementation SwitchCell

@synthesize mySwitch;

-(id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
[super initWithFrame:frame reuseIdentifier:reuseIdentifier];

[self.contentView addSubview:fieldDisplayName];

self.mySwitch = [[UISwitch alloc] init];
mySwitch.backgroundColor = [self getBackgroundColor];
[mySwitch addTarget:self action:@selector(switchAction:) forControlEvents:UIControlEventValueChanged];
[self.contentView addSubview:mySwitch];

return self;
}

-(void)layoutSubviews {
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame;

frame = CGRectMake(boundsX+10, 0, 140, 30);
fieldDisplayName.frame = frame;

frame = CGRectMake(boundsX+210, 0, 110, 30);
mySwitch.frame = frame;
}

-(void)switchAction:(UISwitch*) sender {
[currentController performSelector:@selector(valueChanged:) withObject: sender];
}

[...dealloc...]


Note the callbacker on the SwitchCell, not necessary on ButtonCell because we can set its target action when we create it.


There you go: a Settings architecture, and a programmatic TableViewController with a useful custom TableViewCell architecture and three examples of same. Use as you like.


And now, an actual algorithm I wrote. An algorithm! I know! Real programming! Hence my pride. The problem was, given a list of points that might have a few spurious outliers, get rid of those outliers and build a zoomed-in region that excludes them, to show in an MKMapView. My solution, in a class called Locator:


+(MKCoordinateRegion)getRegionForAnnotations:(NSArray*)annotations {

NSMutableArray *latitudes = [NSMutableArray arrayWithCapacity:[annotations count]];
NSMutableArray *longitudes = [NSMutableArray arrayWithCapacity:[annotations count]];

for (id entry in annotations) {
double myLat = [entry coordinate].latitude;
double myLong = [entry coordinate].longitude;
[latitudes addObject:[NSNumber numberWithDouble:myLat]];
[longitudes addObject:[NSNumber numberWithDouble:myLong]];
}

//OK, we've got the box that includes *all* the annotations
//Now we get rid of outliers aka bad address finds.
NSArray *sortedLatitudes = [latitudes sortedArrayUsingSelector:@selector(compare:)];
NSArray *sortedLongitudes = [longitudes sortedArrayUsingSelector:@selector(compare:)];

NSRange latLongest = [Locator getLongestContiguousRangeIn:sortedLatitudes];
NSRange lonLongest = [Locator getLongestContiguousRangeIn:sortedLongitudes];

NSNumber* minLat = [sortedLatitudes objectAtIndex:latLongest.location];
NSNumber* maxLat = [sortedLatitudes objectAtIndex:latLongest.location+latLongest.length-1];
NSNumber* minLon = [sortedLongitudes objectAtIndex:lonLongest.location];
NSNumber* maxLon = [sortedLongitudes objectAtIndex:lonLongest.location+lonLongest.length-1];
MKCoordinateRegion regionToShow = [Locator getRegionForMinLat:minLat minLong:minLon maxLat:maxLat maxLong:maxLon];
return regionToShow;
}

+(NSRange) getLongestContiguousRangeIn:(NSArray*)numbers {
NSRange longest; longest.location=0; longest.length=0;
NSRange range; range.location=0; range.length=0;
for (int i=0; i<[numbers count]; i++) {
double thisLat = [[numbers objectAtIndex:i] doubleValue];
double lastLat = i>0 ? [[numbers objectAtIndex:i-1] doubleValue] : thisLat;
if (thisLat-lastLat<=(double)1.0)
range.length++;
else {
range.location=i;
range.length=0;
}
if (range.length>longest.length)
longest=range;
}
return longest;
}

+(MKCoordinateRegion) getRegionForMinLat:(NSNumber*)minLat minLong:(NSNumber*)minLong maxLat:(NSNumber*)maxLat maxLong:(NSNumber*)maxLong {
CLLocationCoordinate2D regionCenter;
regionCenter.latitude=([minLat doubleValue]+[maxLat doubleValue])/2;
regionCenter.longitude=([minLong doubleValue]+[maxLong doubleValue])/2;

MKCoordinateSpan regionSpan;
regionSpan.latitudeDelta=[maxLat doubleValue]-[minLat doubleValue];
regionSpan.longitudeDelta=[maxLong doubleValue]-[minLong doubleValue];

MKCoordinateRegion region;
region.center=regionCenter;
region.span = regionSpan;
return region;
}


Quite elegant and efficient, if I do say so myself. It would make for an interesting interview question, too.

Labels: , , , , , , , , , , , , ,


Wednesday, September 9, 2009

 

Weird, man, weird

So I have switched to the Google Toolbox iPhone unit-test framework, and it mostly works a dream. You plug it in as per those instructions, write some tests, switch to your test target, hit "Build", and it runs your unit tests in the build phase, and highlights test failures in the same way that XCode normally highlights compile errors.

However. I have found something profoundly weird about it, which cost me a bunch of today.

Suppose your unit test is failing, and you want to debug it. Well, you can; but to do so, you have to not just Build but Run the test target, so you have to comment out the tests that are causing the build to fail. Easy enough. Then the test target launches normally in the simulator, and you can step through with the debugger, and find out what went wrong.

However. Guess what? If you leave any breakpoints in your code, and then go back and hit Build, then the Google unit-test framework will begin to fail, in strange and inexplicable ways (mostly null values where there shouldn't be any, in my case.) Remove those breakpoints - and poof, your unit tests are working again.

May you not spend a day beating your head against this, as I did.

Labels: , , , , , , , , ,


Tuesday, September 1, 2009

 

iPhone bits and bobs

Hello there, O my droogs. Long time no talk. I've been working on an iPhone app for pay, you see, which I figure kind of limits how much code I can provide you. I do have a few notes that might be worth sharing, though:

Labels: , , , , , , , ,


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]