Monday, February 8, 2010
Android vs. iPhone: A Developer's Perspective, part II
(See also Part I.)
OS features
The big winner here is Android, due to its multitasking. You can call other apps, eg a browser or a file-system picker, get results from them, and use those results in your own app; all these things are completely impossible on the iPhone, where (aside from a few special cases like iTunes) only one app may run at a time. (And even if you want to sacrifice yourself to launch another app, you can only do so in very restricted circumstances - if they have registered a URL scheme.)
The browser special-case is worth mentioning; you can include browser windows in your own app, and mine do so in a few different places - but this is often not as full-featured or convenient to the user as opening up the full Safari browser.
You can also write Android services that run in the background, and you can launch them at boot time (if the user permits) by registering for the BOOT_COMPLETED intent. What it doesn't really, provide, though, is "push" notifications (other than by faking it with eg Comet HTTP Push) which the iPhone does offer. That's the iPhone's only advantage in this category, though.
Phone features
The iPhone is locked down. You don't get direct Bluetooth API access; you do in Android. You don't get direct SMS access; you do in Android. For security reasons, they claim in Cupertino. At least (like Android) it now lets you access the proximity sensor.
But to its credit, the features it does offer are a joy to work with. In particular, the built-in camera/preview screen (for iPhone) / picture selector (for iPod Touch) is excellent, and requires all of a half-dozen lines of code to launch and respond to. The Android camera code, last I looked at it, was much more complex.
Location management is a little messy and complex on both systems, but overall Android's registration model is easier to work with than the iPhone's delegation model.
The iPhone SDK comes with this daft notion that all settings for all apps should probably be in a single System Settings screen accessible from the main menu; you can roll your own, but it's inconvenient. Android, by contrast, lets you create a settings screen by simply writing XML, no Java required unless you want to customize it. On the other hand, the iPhone simply makes "your default settings" available, whereas Android provides the possibility of multiple sets, which is doubtless more flexible and powerful but also more annoying to work with.
Accessing system and app resources (eg image files) is a little counterintuitive on Android; at compile time, it scans a predetermined bunch of directories, and automatically builds an "R" file with a bunch of final static ints, each of which uniquely identifies a resource; you then use those in code to access resources. (There's also an Android.R for built-in-resources.) This is confusing at first, but fine once you get used to it.
The iPhone makes the basics easy for you in code -
[Image imageNamed:@"myImage.png"]- but if you want to go beyond that, the whole resource-bundling thing is less than intuitive, and while I had no trouble accessing bundle resources, I never felt like I had a clear idea of what was actually going on, unlike with Android.
Screen building
No sense pussyfooting around: when it comes to actually building the screens of your app, the iPhone has a massive advantage. It provides an excellent WYSIWYG tool, and the components it offers - buttons, lists, etc - mostly just look a whole lot nicer and sexier than the Android ones.
(That said, I have two minor complaints about the iPhone UI components: 1) No drop-down options in menus - instead you either have to code an ActionView or use one of the huge screen-eating spinners. 2) The absence of a border around TextViews does not look good and just serves to confuse users.)
In general, though, the iPhone wins here. The delegate model of TableViewController gives you powerful and fine-grained control far more easily than the adapter model of ListActivity. As far as "complicated, hard-to-work-with, but useful subclasses of your basic list view" goes, I'll take the iPhone's LocalizedIndexedCollation over Android's ExpandableListView, though I sure wish both were more developer-friendly.
And then there's maps. Jeez. In iPhone, if you want to add a custom marker for a map, then in the ViewController for that screen, you just override a method and write six lines of code:
- (MKAnnotationView *)mapView:(MKMapView *)myMapView viewForAnnotation:(id)annotation {
NSString viewImageName=@"myImage.jpg";
MKAnnotationView *myView = (MKAnnotationView*)[myMapView dequeueReusableAnnotationViewWithIdentifier:viewImageName];
if (myView==nil) {
myView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:viewImageName] autorelease];
myView.image = [UIImage imageNamed:viewImageName];
}
return myView;
}
In Android, first you have to write a whole new inner class that extends ItemizedOverlay
class ListingsOverlay extends ItemizedOverlay{
private ArrayListoverlays=new ArrayList ();
public ListingsOverlay(android.graphics.drawable.Drawable defaultMarker) {
super(defaultMarker);
}
public void addOverlay(OverlayItem overlay, Drawable marker) {
super.boundCenterBottom(marker);
overlay.setMarker(marker);
overlays.add(overlay);
}
// necessary because populate() is protected
public void doPopulate() {
populate();
}
@Override
protected OverlayItem createItem(int i) {
return(overlays.get(i));
}
@Override
public int size() {
return(overlays.size());
}
and then something like this:
mapView = (MapView) findViewById(R.id.mapview);
mapOverlays = mapView.getOverlays();
mapOverlays.removeAll(mapOverlays);
Drawable drawable = getResources().getDrawable(R.drawable.map_fave);
itemizedOverlay = new ListingsOverlay(drawable);
OverlayItem overlay = new OverlayItem(point, mappable.getIDString(), mappable.getMapDetailText());
Drawable marker = MappableItem.GetMarkerForMappable(ITRMapView.this, mappable.getCategory());
itemizedOverlay.addOverlay(overlay, marker);
itemizedOverlay.doPopulate();
Menus and navigation are also easier and prettier on the iPhone; you can add sleek-looking buttons and toolbars and simply set them to call the selector of your choice, and you get a great NavigationController for iTunes-like interfaces, plus sexy animation. No real equivalent on Android.
On the other hand, Android's XML layouts, while tedious and irritating, and not as pretty or near as exact as the iPhone's WYSIWYG, do work well once you get the hang of them - and they make it much easier to support multiple screen sizes and different orientations. (My iPhone app simply doesn't do landscape orientation; my Android app handles it almost perfectly, without me ever having thought about it.) This wasn't a big deal for the iPhone until last month - but apps that previously were confident of a 320x480 screen now have to deal with the iPad.
The Internet
Accessing web services and launching in-app web views is easy and effective in both Android and iPhone. Edge to the latter, though; there are a couple of weird little bugs with Android's WebViews (though they can be worked around with ease) and the iPhone gives you both more SDK options and better documentation.
Release
Building an iPhone app is kind of scary. You're suddenly reminded that under the hood it's terrifying C++; the "Build" screens are full of dozens if not hundreds of byzantine, cryptic, intimidating options for compiling, precompiling, linking, etc., and you find yourself desperately hoping you've set up your import libraries perfectly and suddenly very careful not to touch anything.
That said, XCode works really well. (Have I mentioned that debugging is far easier with the iPhone SDK? Debugging is far easier with the iPhone SDK. With Android I usually wind up resorting to debugging with log messages.) What does not work really well is Apple's paranoid certification hegemony. God forbid that anyone run an app without going through the App Store!
So you need to go to Apple's site and futz around with it and with device IDs and create and download separate certificates for debug and release, and your temporary "provisioning" device certificates expire every three months, and while it is theoretically possible to build an app for someone else's device, email it to them, and have them install it, I have yet to actually succeed at this, despite repeated attempts. (It's somewhat easier if their device is plugged in to your machine.)
You know how it works on Android?
- You build your app.
- You sign your app. (Which Eclipse can take care of with a simple wizard.)
- Anyone in the whole world who wants to can now download and run the app.
There's a slight pitfall if you're working with Google Maps - you have to jump through hoops like Apple's to create separate debug and release Maps API Keys, and ensure you're using the right one - but by and large, it's miles easier and better than trying to finagle your way into Apple's walled garden.
Plus, if you've built an app and released it, and found some sort of subtle bug? With Android, you can fix that and have a new version up on the Android Market in five minutes. With Apple, it's ... a week? A month? Who knows? App Store approval is an infuriating black box.
Overall
They're both excellent systems. They both have their pros and cons. Overall I would rate the iPhone as better, both in terms of what you can do with it and how - but Android is superior in fundamental ways (eg multitasking and memory management) and catching up fast in terms of results. If Apple doesn't watch out, and move fast, they're going to find themselves superseded soon. Maybe this year.
Labels: Android, building, iPhone, maps, notification, preferences, push, release, resources, Settings
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:
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:
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:
which will make more sense when you look at its use in a subclass:
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:
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:
Quite elegant and efficient, if I do say so myself. It would make for an interesting interview question, too.
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 (identry 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: custom, iPhone, Map, NSUserDefaults, outliers, Settings, UIActionSheet, UIActionSheetDelegate, UIButton, UILabel, UISwitch, UITableViewCell, UITableViewController, UITextField
Subscribe to Posts [Atom]