Guessing a User’s Location on iOS

A few months ago at work we ran into an odd user experience problem. The home screen for one of our iPad apps included a small icon in the navigation bar showing the current weather. Normally it displays the weather for the user’s current location or any location they’ve saved. No problem. But what do we show the first time the app launches? At that point, they have no saved location preference and we don’t know their physical location because they haven’t yet opted-in to CoreLocation. We came up with three options.

  1. Don’t show anything, or show a generic no-location-set icon. We tried this, but our designers didn’t ilke the empty experience.
  2. Immediately prompt the user for permission to access their location as soon as the app launches. We nixed this idea, too, since we didn’t want an ugly system alert to be their first interaction with our app.
  3. Pick some standard default location until the user chooses a different one.

We actually went with option #3 and set New York City as the default location. Unfortunately we found that this confused users. Even though they hadn’t given us their location info yet, they still assumed that the icon represented their local weather forecast. Imagine seeing a sunshine icon when it’s pouring rain outside. Not good.

That led us to consider a fourth solution. Over lunch we came up with the idea of trying to infer the user’s general location based on the data available in their address book. If it worked, we could provide an approximate weather forecast on first launch without popping-up a nagging alert window.

On Mac, doing this is easy. Just query the user’s “Me” card and pull out their city or zip code. But on iOS, for privacy reasons, we don’t know which card is the user’s.

Thinking a bit more about the problem we realized that most people know lots of people who live near to them and fewer people as the distance increases. So we decided to look through the user’s address book and find the most common city, state, and zip code. The idea being that would let us infer the user’s state if nothing else.

The code for this was pretty quick to write. We built a small sample app and distributed it to everyone in the office. We were shocked to find out how well it worked. It correctly guessed the user’s appoximate location for all but one of the devices we tested it on.

In the end, however, we chose not to add this “feature” to the app. We decided, while clever, it was just a little too creepy even though we never did anything with the data. But, it was still a fun thought experiment and a nice proof of concept to spend an afternoon on.

If you’d like to see or use the code, it’s available on GitHub.

#import <Foundation/Foundation.h>
#import <AddressBook/AddressBook.h>

@interface SFBestGuess : NSObject {
    NSMutableArray *_cities;
    NSMutableArray *_states;
    NSMutableArray *_zipCodes;
}

@property (nonatomic, retain) NSMutableArray *cities;
@property (nonatomic, retain) NSMutableArray *states;
@property (nonatomic, retain) NSMutableArray *zipCodes;

- (void)guessLocation;
- (void)incrementOrCreateKey:(NSString *)key inDictionary:(NSMutableDictionary *)dict;
- (NSMutableArray *)sortedDescendingArrayFromDictionary:(NSDictionary *)dict;

@end
#import "SFBestGuess.h"

@implementation SFBestGuess

@synthesize cities = _cities;
@synthesize states = _states;
@synthesize zipCodes = _zipCodes;

- (void)dealloc {
    [_cities release], _cities = nil;
    [_states release], _states = nil;
    [_zipCodes release], _zipCodes = nil;
    [super dealloc];
}

- (id)init {
    self = [super init];

    _cities = [[NSMutableArray alloc] init];
    _states = [[NSMutableArray alloc] init];
    _zipCodes = [[NSMutableArray alloc] init];

    [self guessLocation];

    return self;
}

- (void)guessLocation {
    NSMutableDictionary *cities = [NSMutableDictionary dictionary];
    NSMutableDictionary *states = [NSMutableDictionary dictionary];
    NSMutableDictionary *zipCodes = [NSMutableDictionary dictionary];

    ABAddressBookRef addressBook = ABAddressBookCreate();
    CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
    CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);

    for(int i = 0; i < nPeople; i++) {
        ABRecordRef ref = CFArrayGetValueAtIndex(allPeople, i);

        ABMultiValueRef multi = ABRecordCopyValue(ref, kABPersonAddressProperty);
        if(multi) {
            NSArray *theArray = [(id)ABMultiValueCopyArrayOfAllValues(multi) autorelease];
            NSDictionary *theDict = [theArray objectAtIndex:0];

            NSString *city = [theDict objectForKey:@"City"];
            [self incrementOrCreateKey:city inDictionary:cities];

            NSString *state = [theDict objectForKey:@"State"];
            [self incrementOrCreateKey:state inDictionary:states];

            NSString *zip = [theDict objectForKey:@"ZIP"];
            [self incrementOrCreateKey:zip inDictionary:zipCodes];

            CFRelease(multi);
        }
    }

    CFRelease(addressBook);
    CFRelease(allPeople);

    self.cities = [self sortedDescendingArrayFromDictionary:cities];
    self.states = [self sortedDescendingArrayFromDictionary:states];
    self.zipCodes = [self sortedDescendingArrayFromDictionary:zipCodes];
}

- (void)incrementOrCreateKey:(NSString *)key inDictionary:(NSMutableDictionary *)dict {
    if(key) {
        if([dict valueForKey:key]) {
            [dict setValue:[NSNumber numberWithInt:[[dict valueForKey:key] intValue] + 1] forKey:key];
        } else {
            [dict setValue:[NSNumber numberWithInt:1] forKey:key];
        }
    }    
}

- (NSMutableArray *)sortedDescendingArrayFromDictionary:(NSDictionary *)dict {
    NSMutableArray *sortedArray = [NSMutableArray array];
    NSArray *sortedKeys = [dict keysSortedByValueUsingSelector:@selector(compare:)];

    NSString *someKey;
    NSEnumerator *e = [sortedKeys reverseObjectEnumerator];
    while(someKey = [e nextObject]) {
        NSDictionary *tmpDict = [NSDictionary dictionaryWithObjectsAndKeys:someKey, @"key", [dict valueForKey:someKey], @"count", nil];
        [sortedArray addObject:tmpDict];
    }

    return sortedArray;
}

@end

Why I Took the Job

Almost four years ago today, I moved across the country and accepted a job at Yahoo!. But one of the main reasons I took the position happened six years before that.

In the Fall of 2001 I was a Sophomore in college at MTSU. Each morning I’d roll out of bed and open my Yahoo! home page. It was the first step in my morning routine. I’d check the news, check my email, then get ready for class.

On Tuesday the 11th I woke up at 7:45. The first thing I saw on Yahoo! was a headline that a plane had crashed into one of the towers. I clicked through to the article, but it was such breaking news the whole story was only three sentences long. It had just happened.

I woke up my roommate — a pilot himself — and turned on CNN just in time for both of us to watch the second plane hit live. Neither one of us spoke about it. We just sat there in silence watching the morning unfold.

I haven’t spoken to Chris in years, but if he’s anything like me, that image turned into one of the defining moments on our way to becoming adults. And looking back, we both would have missed it if not for the news being reported on Yahoo! that morning.

And so, six years later in September 2007, I was sitting in Starbucks with my Yahoo! offer letter in hand trying to decide. I remember thinking how much Yahoo! had indirectly changed my life that day and with a thousand other small contributions since then. And now I was given the opportunity to work for them and possibly impact millions of other people, too.

That’s why I took the job.

So tech pundits can write gleefully about the fall of Yahoo! — the many missteps they took during their short corporate history. But fuck ’em. I’m proud I got to work there and with so many incredible people for three full years. And I’m sad to see Yahoo! put themselves up for sale. There are few companies around with such reach — few that can claim to have changed the lives of so many people with nothing else but a few bits over the wire.