_Google Places for iOS 5 SDK

I am integrating the Google Places API into an iPhone app that I am building.  I found an existing library that uses Google Local Search but that is deprecated.  So I forked that solution and came up with Google Places Library.

As the story goes…when the application is configured to allow current location, the fun begins.

First configure your project to include the CoreLocation and the MapKit framework.  I am using a single view project for this example but it can be any project really.

In the RootViewController I need to define some delegates.  Since I will be embedding a UITableView and UISearchBar I need the following to be added: <UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate, CLLocationManagerDelegate, GooglePlacesConnectionDelegate>

UITableViewDelegate – will help populate the data on my table.

UISearchBarDelegate – will perform some interactions with the searching of places

CLLocationManagerDelegate – will get the location

GooglePlacesConnectionDelegate – will perform the main search

Once that is complete I can then add some outlets for my UI and set up some properties.

#import
#import
#import "GooglePlacesConnection.h"

@class GooglePlacesObject;

@interface RootViewController : UIViewController
{
    IBOutlet UITableView    *tableView;
    IBOutlet UIButton       *btnCancel;
    IBOutlet UISearchBar    *searchBar;

    CLLocationManager       *locationManager;
    CLLocation              *currentLocation;

    NSMutableData           *responseData;
    NSMutableArray          *locations;
    NSString                *searchString;

    GooglePlacesConnection  *googlePlacesConnection;
}

@property (nonatomic, getter = isResultsLoaded) BOOL resultsLoaded;

@property (nonatomic, retain) CLLocationManager *locationManager;
@property (nonatomic, retain) CLLocation        *currentLocation;

@property (nonatomic, retain) NSURLConnection   *urlConnection;
@property (nonatomic, retain) NSMutableData     *responseData;
@property (nonatomic, retain) NSMutableArray    *locations;

@end

Lets move on to the meat and potatoes of this app.  Within the .m file I need to initialize some of my variables and get the GoogleConnection going.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    responseData = [[NSMutableData data] init];

    [[self locationManager] startUpdatingLocation];

    [tableView reloadData];
    [tableView setContentOffset:CGPointZero animated:NO];

   // [searchBar setDelegate:self];

    googlePlacesConnection = [[GooglePlacesConnection alloc] initWithDelegate:self];

}

Do not forget to set the url connection to nil in the viewDidUnload.

Lets move to the Location Manager methods.

The first one returns a location manager or creates one if necessary

- (CLLocationManager *)locationManager
{

    if (locationManager != nil)
    {
		return locationManager;
	}

	locationManager = [[CLLocationManager alloc] init];
	[locationManager setDesiredAccuracy:kCLLocationAccuracyNearestTenMeters];
	[locationManager setDelegate:self];

	return locationManager;
}

The next method really starts the fun. In my case I am constructing a URL for the Google Places API. If you are not familiar with it you can find more about it here. The url takes the following parameters:

  • key: required – your API key
  • location: required – latitude/longitude around which to retrieve Place information. Location manager will help here
  • radius: required – distance in meters. For the initial search I have 500, for the UISearchBar I moved it out to 1000.
  • sensor: required – needs to be set to TRUE if came from a device with a location sensor
  • keyword: optional – a term to be matched if searching
  • language: optional – language codes
  • name: optional – term to be matched against the names of the Places
  • types: narrows the search to a specific type of place. Here is a list

At the end of all of this the url will look like this:

https://maps.googleapis.com/maps/api/place/search/json?location=-33.8670522,151.1957362&radius=500&types=food&name=harbour&sensor=false&key=AIzaSyAiFpFd85eMtfbvmVNEYuNds5TEF9FjIPI

- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{

    if ([self isResultsLoaded])
    {
		return;
	}

	[self setResultsLoaded:YES];

    currentLocation = newLocation;

    //What places to search for
    NSString *searchLocations = [NSString stringWithFormat:@"%@|%@|%@|%@|%@|%@|%@|%@|%@",
                                 kBar,
                                 kRestaurant,
                                 kCafe,
                                 kBakery,
                                 kFood,
                                 kLodging,
                                 kMealDelivery,
                                 kMealTakeaway,
                                 kNightClub
                                 ];

    [googlePlacesConnection getGoogleObjects:CLLocationCoordinate2DMake(newLocation.coordinate.latitude, newLocation.coordinate.longitude) andTypes:searchLocations];

}

The last methods are the setting of the objects and some clean up.

- (void)locationManager:(CLLocationManager *)manager
       didFailWithError:(NSError *)error
{
    NSLog(@"locationManager FAIL");
    NSLog(@"%@", [error description]);
}

#pragma mark -
#pragma mark NSURLConnections

- (void)googlePlacesConnection:(GooglePlacesConnection *)conn didFinishLoadingWithGooglePlacesObjects:(NSMutableArray *)objects
{

    if ([objects count] == 0) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"No matches found near this location"
                                                        message:@"Try another place name or address"
                                                       delegate:nil
                                              cancelButtonTitle:@"OK"
                                              otherButtonTitles: nil];
        [alert show];
    } else {
        locations = objects;
        [tableView reloadData];
    }
}

- (void) googlePlacesConnection:(GooglePlacesConnection *)conn didFailWithError:(NSError *)error
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error finding place - Try again"
                                                    message:[error localizedDescription]
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles: nil];
    [alert show];
}

I am not going to dive deep into the GooglePlacesConnection class or the GooglePlacesObject. However I will say this, the GooglePlacesObject is just that. An object of the Google Place. It has an init that will take the dictionary object from the JSON results. The GooglePlacesConnection does the work of constructing of the URL and issuing the request. One thing to note, the only error checking on the Google Place request is if there are 0 results or an error with the URL or request. There are other status codes if you want to add them in if needed.

Back into the RootViewController.m, we now should have a locations NSMutableArray so we can now populate our UITableView.

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

- (NSInteger)tableView:(UITableView *)theTableView numberOfRowsInSection:(NSInteger)section
{
    return [locations count];
}

- (UITableViewCell *)tableView:(UITableView *)theTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

	static NSString *CellIdentifier = @"LocationCell";

	// Dequeue or create a cell of the appropriate type.
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell                = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
        cell.accessoryType  = UITableViewCellAccessoryDisclosureIndicator;
    }

    // Get the object to display and set the value in the cell.
    GooglePlacesObject *place     = [[GooglePlacesObject alloc] init];
    place                       = [locations objectAtIndex:[indexPath row]];

    cell.textLabel.text                         = place.name;
    cell.textLabel.adjustsFontSizeToFitWidth    = YES;
	cell.textLabel.font                         = [UIFont systemFontOfSize:12.0];
	cell.textLabel.minimumFontSize              = 10;
	cell.textLabel.numberOfLines                = 4;
	cell.textLabel.lineBreakMode                = UILineBreakModeWordWrap;
    cell.textLabel.textColor                    = [UIColor colorWithRed:0.0 green:128.0/255.0 blue:0.0 alpha:1.0];
    cell.textLabel.textAlignment                = UITextAlignmentLeft;

    cell.detailTextLabel.text                   = place.vicinity;
    cell.detailTextLabel.textColor              = [UIColor darkGrayColor];
    cell.detailTextLabel.font                   = [UIFont systemFontOfSize:10.0];

    return cell;
}

We set the count of the array to the number of rows in section. We then get the locations object at the indexPath and set that to a GooglePlacesObject and populate the cell.

To handle the UISeachBar here is the code:

- (void)updateSearchString:(NSString*)aSearchString
{
    searchString = [[NSString alloc]initWithString:aSearchString];

    //What places to search for
    NSString *searchLocations = [NSString stringWithFormat:@"%@|%@|%@|%@|%@|%@|%@|%@|%@",
                                 kBar,
                                 kRestaurant,
                                 kCafe,
                                 kBakery,
                                 kFood,
                                 kLodging,
                                 kMealDelivery,
                                 kMealTakeaway,
                                 kNightClub
                                 ];

    [googlePlacesConnection getGoogleObjectsWithQuery:searchString andCoordinates:CLLocationCoordinate2DMake(currentLocation.coordinate.latitude, currentLocation.coordinate.longitude) andTypes:searchLocations];

    [tableView reloadData];
}

- (void)searchBarTextDidBeginEditing:(UISearchBar *)theSearchBar
{
    [theSearchBar setShowsCancelButton:YES animated:YES];
    tableView.allowsSelection   = NO;
    tableView.scrollEnabled     = NO;

}

- (void)searchBarCancelButtonClicked:(UISearchBar *)theSearchBar
{
    [theSearchBar setShowsCancelButton:NO animated:YES];
    [theSearchBar resignFirstResponder];
    tableView.allowsSelection   = YES;
    tableView.scrollEnabled     = YES;
    theSearchBar.text           = @"";

    [self updateSearchString:searchBar.text];
}

- (void)searchBarSearchButtonClicked:(UISearchBar *)theSearchBar
{
    tableView.allowsSelection   = YES;
    tableView.scrollEnabled     = YES;

    [self updateSearchString:theSearchBar.text];
}

The second, third and fourth method handle the interaction with the UISearchBar itself. Each calls the updateSearchString method and passes in the text from the search bar. updateSearchString performs a similar search to the location manager search, except it is passing in the search string as well. If you look in the GooglePlacesConnection method I am also expanding the search radius to 1000 from 500.

That is it. You can find the source on github.

My next update will be adding a ‘pull to refresh’.

Nov
28
2011

_Background Image Shrinking on iPad

Recently I was building a site that had full page backgrounds.  One of the pages had a long vertical background to accommodate the content.

Here is an example of what I am talking about:

body {
  background: #fff url(img/my-really-large.jpg) top center no-repeat;
}

Example (view on Desktop)

There really is not an issue (aside from image load) when viewing on Desktop browsers.  However, I would like this site to be compatible with the iPad as well.  Unfortunately this simple technique will not work on the iPad.

The iPad uses Mobile Safari which, by default, scales websites to fit it’s viewport.  It does this to text, images and background images.  It has to do with a resource limit put on the device from displaying large images.  The maximum number is 1024 x 1024 x 3 which is 3,145,728 (result varies a bit depending on iPhone or iPad).  However, Mobile Safari will scale your image accordingly to fit that size and your screen.

You will end up with something like this (view on iPad)

 

There is a simple fix and it has to do with some newer -webkit tags. Add the following to your body declaration in your CSS:

  -webkit-background-size: 1200px 1900px;

(where 1200 1900 is the size of your image)

This will only render on WebKit browsers and fix the issue of scaling.

See here (on iPad please)

This will force the iOS device to load the full background.

This tip will definitely be coming in handy again.

Jul
13
2011

_Incase Product Support

I recently purchased an iPad 2 and started my search for a case.  Apple makes the new case which has ‘magnets’ and puts the iPad to sleep when it is covered and wakes it up when you open the case.  Sounds pretty cool but it does not protect the back of the iPad.  Placing the iPad down on a desk or a park bench will allow it to get scratched easily so I ruled that one out pretty quickly.  I then narrowed in on a ‘notebook’ looking case.  Something that felt nice, protected it in the front and back and functional.  I wanted to the the ‘original’ iPad case from Apple, but the iPad 2 is much thinner and it will not work.

I found the Incase Book Jacket at BestBuy.  It looked great, felt great and had all of these ‘slots’ for different positions of the iPad.  So I picked one up and used it right away.

My overall reaction is it is nice.  Looks and feel is top notch.  Functionality is not.  There are 3 slots within the case to allow different angles of use.  1 slot works all the time.

This postion works well.  It is great for watching Netflix or any videos on it.  You can type on it but this position is ideal for viewing and swiping.

This is the second slot.  This one works pretty well.  Not as much as the first one but it works.  You can type on here but be careful, at random times it slips out of the slot.  A little annoying.

The third slot seems to be ideal for typing.  Just a slight angle and type away.  The problem is I can never get the iPad to stay in the slot.  It keeps slipping out.  And if I do get it to stay as soon as I breath on it let alone type, it slips out.

There’s not a whole lot I can do about it now. I contacted Incase and they responded with the following:

Hello Josh,

Thank you for contacting Incase Designs Corp.

If you have a problem with your warranted Incase product you may file a claim to have it replaced by going to the following URL:

http://www.goincase.com/warranty-claim-form/

Sincerely,

Incase Designs Corp.

I can’t really warranty it since I do not have the receipt and really do not want to deal with the hassle.  So I am informing others about the case.  Reviews posted on GoIncase.com, Amazon and BestBuy will hopefully educate buyers along with informing Incase that they need to fix the issue at some point.

May
07
2011

_jQuery Accordion with 8 Lines of Code

Ok.  Maybe more than 8 lines of code but very little.  I have been working with another agency (that will rename nameless) on a mobile web site for a client of ours.  The agency provided the HTML/CSS/Images and Script for our technology team to implement.  We hook up all the back end and call it a day.

The section that came into question was an ‘accordion’ style UI for a set of FAQ’s and other content.  When I first loaded up their pages, everything worked fine.  Upon further inspection of their code I noticed a lot of JavaScript libraries that were not needed.  They included jQuery AND Prototype along with some effects and accordion scripts.  (Prototype alone is almost 100kb in file size and the other scripts were 42kb.  That still does not include the 70kb for jQuery)  I thought to myself, this is a mobile site…why have extra scripts if not needed.  To make matters even more funny, we are supporting Mobile Safari (iOS), Mobile Chrome (Android) and the BlackBerry Browser (RIM), it does not work in BlackBerry at all.  Forget enabling or disabling JavaScript it just does not work.  Even the elements are hidden.

So it got me thinking about the old saying, “if you want something done right you have to do it yourself”.  So I did.

I removed all the libraries except for jQuery and made all of the elements visible by default.  Basically if a user does not have JavaScript enabled then they can see everything.

Here is my HTML

<div>
     <a href="#">Really Great Information</a>
     <div class="accordion_content">
          Content that will expand
     </div>
     <a href="#">Really Great Information</a>
     <div class="accordion_content">
         Content that will expand again
     </div> 
</div>

Styles and other HTML can be wrapped around it. You can change the paragraph tags to div’s if needed but that is the structure you need more or less.

Now for the JavaScript.

Make sure you embed jQuery (I did from Google)

$('.accordion_content').hide();
//If JavaScript is not enabled, all content containers stay open, which is nice.

//Next, bind the click event to all of the targets, in this case all 'a' elements in the 'accordion_toggle' class
$('.accordion_toggle a').click(function(e){
     //code to come
});
//Within the click function we need to first check if the element being clicked is set as the 'current' which means open.  If it is, we need to remove the class 'current' and close the content container.
if($(this).parent().hasClass('current')) {
     $(this).parent()
          .removeClass('current')
          .next('.accordion_content').slideUp();
} else {
...
}

Then hide all open content containers:

If the element does not have the ‘current’ class then we need to first remove the ‘old’ ‘current’, close the ‘old’ ‘current’ and open and set the new ‘current’.

$(document).find('.current')
     .removeClass('current')
     .next('.accordion_content').slideUp();

$(this).parent()
     .addClass('current')
     .next('.accordion_content').slideDown();

Done.

Here is the final:

$(function() {
     $('.accordion_content').hide();
     $('.accordion_toggle a').click(function(e){
          if($(this).parent().hasClass('current')) {
               $(this).parent()
                   .removeClass('current')
                   .next('.accordion_content').slideUp();
          } else {
               $(document).find('.current')
                    .removeClass('current')
                    .next('.accordion_content').slideUp();
              $(this).parent()
                    .addClass('current')
                    .next('.accordion_content').slideDown();
          }
          e.preventDefault();
      });
});

Depending on how you format the code and what you count as a ‘line’ it can be about 8 lines :)

Demo here – View in Mobile Browser.  Desktop browser will work as well.  Works on iOS, Android, BlackBerry.

Mar
10
2011

_STATUS: 404 NOT FOUND @ STORY WORLDWIDE

When I thought about writing a goodbye letter I figured I would do the usual and say thanks and good luck.  But to be honest it is more than that (thanks for the inspiration Martin).  It is more than thanks and good luck.  It is laughs, tears, wins, losses, thrives and struggles.

Eight years is a long time.  That is like going to high school twice (and times this place is like a high school).  From happy hours where Greg is having a strawberry eating contest, Todd eating full spoonfuls of Wasabi, Travis throwing up in creative (sorry Travis), Rica being carted down the mountain by the ski patrol, Cat, Stacy and Ben hi-jacking a school bus, Dave making all the PM’s cry, Mike and his hillbilly teeth, Chet break dancing in a suit of armor, American Idol style contest (does anyone still have that video of the Carrie rapping?), Softball and the Mojito Crew, summer parties at the beach, car arguments about BMW’s VS. Audi’s VS Fords, a little incident with a scooter and the chains at the Maritime Center, trips to Stratton and more.

Let’s also not forget the ones who have passed through these doors like Liz, Kai, Tai, Llama Lynn, Carol, Parchment, Tracy, Yury, Max and Flannigan, they were truly a pleasure :p

From Rimmel London to all the fun with the lovely ladies of COTY (JLo, SJP, Shania), the bottled water sites (Nestle) and all of their crazy contest that Stacy could dream up (Shrek / choose your own adventure! ARG), to some very serious clients like TradeTrak and Connolly Consulting, even K2.  We were ahead of the time with some of the video contest (glo girls & CK In2U) and fan competitions (ultimate fans) and even our own Stumble Upon (Perrier IER) and never forget the webisodes (obviously Spraychel).  We even did a mobile marketing app that took photos and uploaded them to a site that sent out emails back in 2005.  Here’s to the 4 AM launches of Acquisitions or e-Service, the 3 AM fun with the MLB contests, Sunday or even Saturday Midnight launches of contests, New Years Eve site launches and a few all nighters thrown in (shout out to Mark).   So many to list…

I am very proud to have been part of this company, we have done great work each and every year and I truly wish the best for everyone.

Special thanks to my team – Dave, Anthony, Tom, Travis, Steve and Jacek.  It has been has been a pleasure to work with all of you!  You all made “work” so much fun.

Thanks again.  PS – I won’t remove you any of you as my Facebook friends

joshdrew@gmail.com | facebook.com/jdrew | twitter.com/@jdruid | drew5.net

Sep
23
2010