Ad

Our DNA is written in Swift
Jump

Forward Geocoding

There are two kinds of Geocoding which you might encounter: forward and reverse. The class doing that for you in iOS is CLGeocoder. It is both capable of doing it forward (from address to lat/long) and reverse (from geo coordinates to placemarks). For this tutorial we will build an app that lets you search for addresses and display the results individually on a map view.

The finished sample app looks like this:

Entering Search Text Search Result on Map

As an added bonus the search results will be displayed in cells which have variable height and show the exact search term matches as bold text. The full source code of the sample is available on my Examples repo on GitHub.

I’ll refrain from reproducing all the individual steps involved in setting up the app interface. Look at the storyboard to see this. But to summarize: there is a search display controller which owns the search bar. Due to a limitation of this in conjunction with storyboards you cannot reuse the same cell prototype for both table views and therefore I opted to register the cell class in code.

Performing the Search

The CLGeocoder instance used throughout the SearchViewController is created in -viewDidLoad. Whenever the user types a character – if at least 3 characters in the text field – the geocoding/search is being performed. Once it returns the search result table view is reloaded causing the new results to show up.

- (void)_performGeocodingForString:(NSString *)searchString
{
   [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
 
   [_geoCoder geocodeAddressString:searchString completionHandler:^(NSArray *placemarks, 
                                                                    NSError *error) {
      if (!error)
      {
         _searchResults = placemarks;
         [self.searchDisplayController.searchResultsTableView reloadData];
         [self.tableView reloadData];
      }
      else
      {
         NSLog(@"error: %@", error);
      }
 
      [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
   }];
}

The shouldReloadTableForSearchString method gets a YES return value if it is supposed to reload the results table immediately. But since the search results might take a while we show the network activity indicator in the status bar and trigger the reload as soon as we have received results.

We don’t bother in this example with different rows for the regular and the search results table view. So if you tap on the Cancel button next to the search bar you get the same rows with one difference: only on the search results the search terms are highlighted.

The geocoding process returns an array of CLPlacemark objects which have an address dictionary which we can transform into a string by an obscure function contained in the AddressBookUI.framework. This is a more Apple-like method than manually concatenating the address parts. The following convenience method returns such an address string for a row:

- (NSString *)_addressStringAtIndexPath:(NSIndexPath *)indexPath
{
   CLPlacemark *placemark = _searchResults[indexPath.row];
   return ABCreateStringWithAddressDictionary(placemark.addressDictionary, NO);
}

Sizing strings is a pain in the backside for NSString objects. Since we want to highlight search terms anyway, we have another convenience method to create the attributed string for a placemark:

- (NSAttributedString *)_attributedAddressStringAtIndexPath:(NSIndexPath *)indexPath
{
   NSString *string = [self _addressStringAtIndexPath:indexPath];
 
   // get standard body font and bold variant
   UIFont *bodyFont = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
   UIFontDescriptor *descriptor = [bodyFont fontDescriptor];
   UIFontDescriptor *boldDescriptor =
     [descriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold];
   UIFont *highlightFont = [UIFont fontWithDescriptor:boldDescriptor
                                                 size:bodyFont.pointSize];
 
   NSDictionary *attributes = @{NSFontAttributeName: bodyFont};
   NSMutableAttributedString *attribString =
      [[NSMutableAttributedString alloc] initWithString:string attributes:attributes];
 
   // show search terms in bold
   if ([self.searchDisplayController isActive])
   {
      NSString *searchText = self.searchDisplayController.searchBar.text;
      NSArray *searchTerms = [searchText componentsSeparatedByCharactersInSet:
                              [[NSCharacterSet alphanumericCharacterSet] invertedSet]];
 
      for (NSString *term in searchTerms)
      {
         if (![term length])
         {
            continue;
         }
 
         NSRange matchRange = [string rangeOfString:term options:NSDiacriticInsensitiveSearch
                               | NSCaseInsensitiveSearch];
 
         if (matchRange.location != NSNotFound)
         {
            [attribString addAttribute:NSFontAttributeName value:highlightFont
                                 range:matchRange];
         }
      }
   }
 
   return attribString;
}

This code snippet has two interesting bits: see how it creates the bold variant of the standard font for body text as provided by TextKit. Also it sets the bold font for the ranges in the address string that match to alphanumeric parts of the search terms.

To determine the correct height for the table view cell we only need minimal code.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSAttributedString *attributedString = [self _attributedAddressStringAtIndexPath:indexPath];
   CGSize neededSize = [attributedString size];
 
   return ceilf(neededSize.height) + 20;
}

Note that in this setup we are creating the attributed string twice for each row. If you wanted better performance you might want to cache this. The PlacemarkCell class is a regular table view cell with a UILabel inset 10 points from top and bottom, hence the addition of 20.

Showing the Placemark on the Map

The MapViewController is a normal UIViewController with the main view being filled by an MKMapView. A placemark property lets us set the placemark object that the user tapped on. Our goal is to show a map annotation for it as well as zoom in on it. Placemarks can be for a whole country as well as an individual address, so a fixed zoom scale does not cut it.

Here is all the magic:

- (void)viewWillAppear:(BOOL)animated
{
   [super viewWillAppear:animated];
 
   if (!_placemark)
   {
      return;
   }
 
   // set title from first address line
   self.navigationItem.title = ABCreateStringWithAddressDictionary(_placemark.addressDictionary, NO);
 
   // set zoom to fit placemark
   CLCircularRegion *circularRegion = (CLCircularRegion *)_placemark.region;
   CLLocationDistance distance = circularRegion.radius*2.0;
   MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(_placemark.location.coordinate, 
                                                                  distance, distance);
   [self.mapView setRegion:region animated:NO];
 
   // create annoation
   MKPlacemark *mkPlacemark = [[MKPlacemark alloc] initWithPlacemark:_placemark];
   [self.mapView addAnnotation:mkPlacemark];
}

Again we use the address book function to get a string from the address dictionary. We don’t worry about lines after the first one because the navigation bar title field only shows the first line anyway.

CLGeocoder specifies circular regions in the returned placemarks, map views need an MKCoordinateRegion to set the appropriate position and zoom scale. MKCoordinateRegionMakeWithDistance helps us make the transition.

Finally we create an MKPlacemark straight from the CLPlacemark. It really is just a subclass of it adding support for the MKAnnotation protocol methods for talking with an MKMapView.

Conclusion

With CLGeocoder it is easy to search for addresses. Not only do you get best guesses as to what kind of places you are looking for. You also can show these places on a map as shown in this example.

If you like this sample then also buy my book which is chock full of more great sample apps.


Categories: Recipes

1 Comment »

Trackbacks

  1. Tutorial on Forward Geocoding [iOS developer:tips];

Leave a Comment