Design Tips for iOS Developers – Making UI Linen

In Mac OS and iOS (and even in this blog) we see linen textures used extensively. Why? Because it can add subtle depth and richness to an interface. One app that makes good use of this texture is Path. Check it out Path’s Navigation Bar below:

Path - now with linen-fresh scent.

Path - now with linen-fresh scent.

How’d they do that? If we have Photoshop, it takes about two minutes to make our own.

1. Let’s make a graphic for an iPhone app’s Navigation Bar. To start, create a new Photoshop document with a width of 640 pixels and a height of 88 pixels.

Our Blank Canvas.

Our Blank Canvas

2. Select the gradient tool and draw a vertical gradient, light to dark, using RGB colors: #d34d37 and #b02717.

Our Lovely Gradient.

Our Lovely Gradient

3. Create a new layer and fill it with the darkest color from the gradient we just drew: #b02717.

4. Duplicate the layer by pressing Command + J and hide it, for now.

5. Select your visible solid filled layer, and from the Filter menu choose: Noise > Add Noise…

6. Set the Amount to 120%, the Distribution to Uniform, and make sure monochromatic is selected.

Bring da Noise!

Bring da noise!

7. Next, from the Filter menu choose: Blur > Motion Blur.

8. Set the Angle to 0, and the Distance to 25 pixels.

Fuzzy noise.

Fuzzy noise.

9. Next, from the Filter menu choose Sharpen > Sharpen. Do this twice to get some nicely defined lines.

10. In the layers panel, change the blending mode to Overlay, and the opacity to 25%.

The unbeatable tag team of Overlay and Opacity.

The unbeatable tag team of Overlay and Opacity.

11. Next, select your hidden, solid fill layer and make it visible, and from the Filter menu choose: Noise > Add Noise…

12. Set the Amount to 120%, the Distribution to Uniform, and make sure monochromatic is selected.

Bring da noise again!

Bring da noise again!

13. Next, from the Filter menu choose: Blur > Motion Blur.

14. Set the Angle to 90, and the Distance to 25 pixels.

Some more fuzzy noise.

Some more fuzzy noise.

15. Next, from the Filter menu choose Sharpen > Sharpen. Do this twice to get some nicely defined lines.

16. In the layers panel, change the blending mode to Overlay, and the opacity to 25% for the current layer.

17. Add a title and you’re done!

Ta da!

Ta da!

Feel free to grab my original PSD here.

19
Jan 2012
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Instavert – Detecting and Hiding Faces with iOS 5

Facial obfuscation, once the domain of criminals and confidential informants, is now available for the whole family. Today I’m going to show you how to use the new Core Image framework in iOS 5 to auto-detect and hide faces in your photos.

Awkward family photos?  Not anymore!

Protect you and your family from awkward photos.

Auto-hiding faces can be accomplished in 3 short steps:

  1. Prep a photo for Core Image.
  2. Find faces in a given photo.
  3. Pixelate each detected face.

Prepping a photo for Core Image
The first thing we need to do is prep our photo for Core Image by flipping it on its Y-axis. This will allow us to match coordinate space Core Image uses.

inputImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"my_image_name.jpg"]];
[self.view addSubview:inputImage];
[photo setTransform:CGAffineTransformMakeScale(1, -1)];
[self.view setTransform:CGAffineTransformMakeScale(1, -1)];

Detecting faces in a photo
Next, using our photo, we create a new instance of CIImage to work with.

CIImage *coreImage = [CIImage imageWithCGImage:photo.image.CGImage];

We also create an instance of CIDetector that detects faces with a high level of accuracy.

detector = [CIDetector detectorOfType:CIDetectorTypeFace
                                  context:nil
                                  options:[NSDictionary dictionaryWithObject:CIDetectorAccuracyHigh
                                                                      forKey:CIDetectorAccuracy]];

To find all the face features of our image, we can call the detector’s featuresInImage: method, passing in the core image we just created.

NSArray *faces = [detector featuresInImage:coreImage];

We can now loop through our faces array to ‘pixelate’ each of them.

Rendering a pixelated face
Before rendering a pixelated face we need to flip the bounds of the face feature we are working with to match Core Image’s coordinate space.

face.bounds.origin.y = photo.bounds.size.height - face.bounds.origin.y - face.bounds.size.height;

We next create a UIView overlay our pixelated image overtop of our photo.

UIView *faceView = [[UIView alloc] initWithFrame:face.bounds];

Since we only want to pixelate a face, and not the entire photo, we create a new CGIMageRef and then use CGImageCreateWithImageInRect() and face.bounds to clip out out the current face.

CGImageRef imageRef = CGImageCreateWithImageInRect([photo.image CGImage], rect);
UIImage *faceImage = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);

To get a ‘pixelated’ effect we first shrink the image (in this example by a factor of 10), and then scale it to it’s original size.

faceImage = [self resizeImage:faceImage newSize:CGSizeMake(rect.size.width * .1, rect.size.height * .1)];
faceImage = [self resizeImage:faceImage newSize:CGSizeMake(rect.size.width, rect.size.height)];
 
....
 
/*
 Resize the image using the given size.
 */
- (UIImage *)resizeImage:(UIImage*)image newSize:(CGSize)newSize {
 
    CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
    CGImageRef imageRef = image.CGImage;
 
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    // Set the quality level
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
    CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height);
 
    CGContextConcatCTM(context, flipVertical); 
 
    // Draw into the context, scaling the image
    CGContextDrawImage(context, newRect, imageRef);
 
    // Get the resized image from the context and make a new UIImage
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
 
    CGImageRelease(newImageRef);
    UIGraphicsEndImageContext();    
 
    return newImage;
}

Finally, we create a texture from our newly pixelated faceImage and draw it onto the faceView UIView we created earlier.

UIColor *faceTexture = [UIColor colorWithPatternImage:faceImage];
faceView.layer.backgroundColor = faceTexture.CGColor;
[self.view addSubview:faceView];

And that’s it! Auto-anonymity! Below is the source in it’s entirety. Feel free to download the project’s source to play around with it.

The final result.

The final result.

//
//  ViewController.m
//  Instavert
//
//  Created by Nathanael De Jager on 11-12-30.
//  Copyright (c) 2011 Nathanael de Jager. All rights reserved.
//
 
#import "ViewController.h"
 
@implementation ViewController
 
#pragma mark - View lifecycle
 
- (void)viewDidLoad
{
    [super viewDidLoad];
	[self setup];
}
 
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
 
#pragma mark - Initialization
/*
 Load and initialize a picture for face detection / pixelation.
 */
- (void)setup
{
    inputImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"my_image_name.jpg"]];
    [self.view addSubview:inputImage];
    [self performSelectorInBackground:@selector(detectFaces:) withObject:inputImage];
    [self prepareImage:inputImage];
}
 
/*
 Prep the image for Core Image by flipping it on the Y-axis.
 */
- (void)prepareImage:(UIImageView *)photo
{
    [photo setTransform:CGAffineTransformMakeScale(1, -1)];
    [self.view setTransform:CGAffineTransformMakeScale(1, -1)];
}
 
#pragma mark - Detection and drawing
 
/*
 Detects and processes faces found in the provided photo
 */
- (void)detectFaces:(UIImageView *)photo
{
 
    CIImage *coreImage = [CIImage imageWithCGImage:photo.image.CGImage];
 
    detector = [CIDetector detectorOfType:CIDetectorTypeFace 
                                  context:nil 
                                  options:[NSDictionary dictionaryWithObject:CIDetectorAccuracyHigh 
                                                                      forKey:CIDetectorAccuracy]];
    NSArray *faces = [detector featuresInImage:coreImage];
 
    for (CIFaceFeature *face in faces)
    {
        [self renderFaceForFaceFeature:face fromImage:photo];
    }
 
}
 
/*
 Renders the pixelated face based on the provided feature.
 */
- (void)renderFaceForFaceFeature:(CIFaceFeature *)faceFeature fromImage:(UIImageView *)photo
{
    UIView *faceView = [[UIView alloc] initWithFrame:faceFeature.bounds];
    UIImage *faceImage = [self faceImageFromImage:photo forRect:faceFeature.bounds];
 
    UIColor *faceTexture = [UIColor colorWithPatternImage:faceImage];
    faceView.layer.backgroundColor = faceTexture.CGColor;
 
    [self.view addSubview:faceView];
}
 
 
- (UIImage *)faceImageFromImage:(UIImageView *)photo forRect:(CGRect)rect
{   
    // flip the rect to match the current coordinate space
    rect.origin.y = photo.bounds.size.height - rect.origin.y - rect.size.height;
 
    CGImageRef imageRef = CGImageCreateWithImageInRect([photo.image CGImage], rect);
    UIImage *faceImage = [UIImage imageWithCGImage:imageRef]; 
    CGImageRelease(imageRef);
 
    faceImage = [self resizeImage:faceImage newSize:CGSizeMake(rect.size.width * .1, rect.size.height * .1)];
    faceImage = [self resizeImage:faceImage newSize:CGSizeMake(rect.size.width, rect.size.height)];
 
    return faceImage;
 
}
 
/*
 Resize the image using the given size.
 */
- (UIImage *)resizeImage:(UIImage*)image newSize:(CGSize)newSize {
 
    CGRect newRect = CGRectIntegral(CGRectMake(0, 0, newSize.width, newSize.height));
    CGImageRef imageRef = image.CGImage;
 
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0);
    CGContextRef context = UIGraphicsGetCurrentContext();
 
    // Set the quality level
    CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
    CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, newSize.height);
 
    CGContextConcatCTM(context, flipVertical); 
 
    // Draw into the context, scaling the image
    CGContextDrawImage(context, newRect, imageRef);
 
    // Get the resized image from the context and make a new UIImage
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
 
    CGImageRelease(newImageRef);
    UIGraphicsEndImageContext();    
 
    return newImage;
}
@end
01
Jan 2012
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Chuck Jonesing iOS Maps

Recently I was browsing Dribbble and I came across this really cool Chuck Jones inspired map designed by Assembly Co.

Assembly Co's Chuck Jones inspired map

Assembly Co's Chuck Jones inspired map

Unfortunately the project this map was created for never saw the light of day. But this screenshot got me thinking about the maps we use every day on the iPhone. We see Mapkit, and by extension, Google Map’s imagery, used in a lot of popular apps. Assembly Co’s map is a good reminder that we don’t always need the data density Google Maps gives us. In fact, in some contexts, such as in social gaming, it may detract from the overall experience.

Common iOS Maps

Common iOS Maps

Kids, and even most of us as adults, have favorite board games. Think about the games you love. Do the boards all look the same? No. Each has their own layout, and esthetic, which adds to the experience of playing the game. Now think about social games on the iPhone. While most have beautifully designed UI, almost all of them rely on the stock Google Maps imagery that MapKit provides them. To be fair, replacing this imagery may not always be an option, but in some cases it may be preferred. In this post I’d like to show you how it’s done. If you want to follow along, feel free to download my iOS project code. This project is based on James Howard’s WWDC 2010 MapKit presentation, but updated for Xcode 4 and iOS 5.

Game board design

We see variety in board game design

To add our own custom map imagery to our iOS app we need to create custom map tiles which we ‘stitch’ onto our map view overtop of Google Maps. This is a 4 step process. We need to:

  1. Determine the boundaries of our map.
  2. Design our custom imagery.
  3. Use a mapping utility called GDAL2Tiles to slice up our map into MapKit compatible tiles.
  4. Add a few classes to our iOS project to ‘glue’ our custom map tiles onto the default Google Maps imagery.

Determining the boundaries of our map
In our example, I’ve decided to create a fixed location game based on the geography and neighborhoods of San Francisco.

Before we begin drawing our map, we need to define its boundaries. In our example I’ve decided to use San Francisco’s city boundaries. To find the boundary coordinates, we can use a service like GetLatLon to determine the north and south latitudes, along with the west and east longitudes. In our case we end up with:

North Latitude: 37.81175043319711
South Latitude: 37.70663997801684
West Longitude: -122.51687049865723
East Longitude: -122.35465049743652

We’ll be using a tile slicing utility that will need the coordinates of our map’s four corners starting with the longitude, so let’s combine the coordinates like so:

North West: -122.51687049865723 37.81175043319711
North East: -122.35465049743652 37.81175043319711
South West: -122.51687049865723 37.70663997801684
South East: -122.35465049743652 37.70663997801684

Design our custom imagery
Designing a custom map is actually quite easy. In order to maintain a level of accuracy, we can trace over a Google Maps screenshot of our chosen location (in this case San Francisco). Once we define a basic map graphic, we can add in neighborhood boundaries and other game elements.

Once we’re happy with the look of our map we can delete the Google Maps screenshot we traced, crop the edges of canvas, and export it as a flattened PNG file.

Our Custom Map Imagery

Our Custom Map Imagery

Use GDAL2Tiles to make map tiles
To support panning and zooming, Google Maps uses a grid of image tiles. To get our custom map to work with Google Maps in MapKit we need to convert our .png to a grid of tiles too. To do this we use a Python utility called GDAL2Tiles.

You can download and install the complete GDAL framework (for Snow Leopard, and Lion) from http://www.kyngchaos.com/software:frameworks

Once installed, we can use the latitudes and longitudes we made note of earlier to define a geographic area for our map. Open up a new terminal and type:

gdalinfo your_filename.png

. Your output will look similar to this:

Driver: PNG/Portable Network Graphics
Files: SF.png
Size is 473, 396
Coordinate System is `'
Metadata:
Software=Adobe Fireworks CS5.1
Creation_Time=12/25/11
Image Structure Metadata:
INTERLEAVE=PIXEL
Corner Coordinates:
Upper Left ( 0.0, 0.0)
Lower Left ( 0.0, 396.0)
Upper Right ( 473.0, 0.0)
Lower Right ( 473.0, 396.0)
Center ( 236.5, 198.0)
Band 1 Block=473x1 Type=Byte, ColorInterp=Red
Mask Flags: PER_DATASET ALPHA
Band 2 Block=473x1 Type=Byte, ColorInterp=Green
Mask Flags: PER_DATASET ALPHA
Band 3 Block=473x1 Type=Byte, ColorInterp=Blue
Mask Flags: PER_DATASET ALPHA
Band 4 Block=473x1 Type=Byte, ColorInterp=Alpha

We’re most interested in the information the Upper Left, Lower Left, Upper Right, and Lower Right lines gives us. We will use this info to create metadata that describes the positions of our map’s four corners.

Next, using the gdal_translate utility, we can create a VRT file to store this metadata. VRT files contain XML which describe the coordinates we’ll use to slice our map up into tiles.

Using terminal, run the following command:

gdal_translate -of VRT -a_srs EPSG:4326 -gcp 0 0 -122.51687049865723 37.81175043319711 -gcp 473 0 -122.35465049743652 37.81175043319711 -gcp 473 396 -122.35465049743652 37.70663997801684 -gcp 0 396 your_filename.png your_vrt_name.vrt

This command is a bit daunting to read because of the number of arguments it contains, so let’s break it down:

-of defines the output format. In our case we want to create a VRT (Virtual) file.

-a_srs defines the coordinate system we’ll be using. Since we’re creating MapKit compatible tiles we’ll be using ESPSG:4326 (which is the same as WGS84).

-gcp We define for Ground Point Controls, one for each corder of our image. To do this we first define the x, y pixel coordinates of the corner we’re referencing. In our example 0 0 references the top left corner of our map graphic. We then define the corresponding longitude, and latitude coordinates of our map’s boundary. In our example -122.51687049865723 37.81175043319711 references the NorthWest corner of our map.

The final two parameters define the origin file (in our case a PNG image) and the target VRT file we will be creating.

Once we have our geo-reference data, we can slice up our map into tiles using the following command:

gdal2tiles.py -p mercator -z 10-15 your_vrt_name.vrt

Let’s take a look at the arguments we provide:

-p defines the profile we will use to cut the tiles. We define the mercator projection as that’s what MapKit and Google Maps uses.

 -z defines the zoom levels we want our tiles to support. In our case levels 10 to 15.

The final parameter defines the VRT file we’ll use to dice up our imagery.

Once the slicing is completed (it may take longer on large maps) you are left with a directory structure containing all the tiles that make up the grid of our custom map.

Adding our custom map tiles to an iOS App
The next, and final step, is to add our map imagery to an iOS app. We start by open up an iOS project in Xcode and add our root tiles directory to the Supporting Files group. When adding this directory make sure to select the option “Create folder references for any added folders”. This will allow us to easily update the tile images if we need to make adjustments.

Xcode Add Files

Xcode - Add Existing Files

Next we add three classes to our Xcode project:

  • MapTile
  • MapOverlay
  • MapOverlayView

MapTile is a value object used to store the file path and frame for each of our tiles.

MapTile.h:

#import 
#import 
 
@interface MapTile : NSObject
{
    NSString *path;
    MKMapRect frame;
}
 
@property (nonatomic, readonly) NSString *path;
@property (nonatomic, readonly) MKMapRect frame;
 
- (id) initWithFrame:(MKMapRect)tileFrame path:(NSString *)tilePath;
 
@end

MapTile.m:

#import "MapTile.h"
 
@implementation MapTile
 
@synthesize path;
@synthesize frame;
 
- (id)initWithFrame:(MKMapRect)tileFrame path:(NSString *)tilePath
{
    if( self = [super init])
    {
        path = tilePath;
        frame = tileFrame;
    }
    return self;
}
 
@end

MapOverlay builds a grid of our custom tiles based on the map’s rectangle and zoom scale. We initialize MapOverlay with our map tile directory. MapOverlay will go through all of our tiles and match each to a cell in our grid. The tilesInMapRect:rect zoomScale:scale method will determine which tiles our map will used based on grid size, and zoom scale.

MapOverlay.h:

#import 
#import 
 
@interface MapOverlay : NSObject 
{
    NSString *baseDirectory;
    MKMapRect boundingRect;
    NSSet *paths;
}
 
// Initializes the overlay with a directory containing map tile images.
- (id)initWithDirectory:(NSString *)directory;
 
// Returns an array of image tiles for the current map rectangle and zoom scale.
- (NSArray *)tilesInMapRect:(MKMapRect)rect zoomScale:(MKZoomScale)scale;
 
@end

MapOverlay.m:

#import "MapOverlay.h"
#import "MapTile.h"
 
#define OVERLAY_SIZE 256.0
 
static NSInteger zoomScaleToZoomLevel(MKZoomScale scale)
{
    // Conver an MKZoomScale to a zoom level where level 0 contains
    // four square tiles.
    double numberOfTilesAt1_0 = MKMapSizeWorld.width / OVERLAY_SIZE;
 
    //Add 1 to account for the virtual tile
    NSInteger zoomLevelAt1_0 = log2(numberOfTilesAt1_0);
    NSInteger zoomLevel = MAX(0, zoomLevelAt1_0 + floor(log2f(scale) + 0.5));
    return zoomLevel;
}
 
@implementation MapOverlay
 
- (id)initWithDirectory:(NSString *)directory
{
 
    if (self = [super init])
    {
        NSString *filePath = nil;
        NSMutableSet *pathSet = [NSMutableSet set];
        NSInteger minZ = INT_MAX;
 
        baseDirectory = directory;
 
        // Find available tiles.
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSDirectoryEnumerator *directoryEnum = [fileManager enumeratorAtPath:directory];
 
        while (filePath = [directoryEnum nextObject])
        {
            if (NSOrderedSame == [[filePath pathExtension] caseInsensitiveCompare:@"png"])
            {
                NSArray *components = [[filePath stringByDeletingPathExtension] pathComponents];
 
                if ([components count] == 3) 
                {
                    NSInteger z = [[components objectAtIndex:0] integerValue];
                    NSInteger x = [[components objectAtIndex:1] integerValue];
                    NSInteger y = [[components objectAtIndex:2] integerValue];
 
                    NSString *tileKey = [[ NSString alloc] initWithFormat:@"%d/%d/%d", z, x, y];
 
                    [pathSet addObject:tileKey];
 
                    if (z < minZ)
                    {
                        minZ = z;
                    }
                }
            }
        }
 
        if ([pathSet count] == 0)
        {
            NSLog(@"Could not find any tiles at %@", directory);
            return nil;
        }
 
        // Define the bounding map rect.
        NSInteger minX = INT_MAX;
        NSInteger minY = INT_MAX;
        NSInteger maxX = 0;
        NSInteger maxY = 0;
 
        for (NSString *tileKey in pathSet)
        {
            NSArray *components = [tileKey pathComponents];
 
            NSInteger z = [[components objectAtIndex:0] integerValue];
            NSInteger x = [[components objectAtIndex:1] integerValue];
            NSInteger y = [[components objectAtIndex:2] integerValue];
 
            if (z == minZ)
            {
                minX = MIN(minX, x);
                minY = MIN(minX, y);
                maxX = MAX(maxX, x);
                maxY = MAX(maxY, y);
            }
        }
 
        // The first tile in the y direction is at the bottom, while
        // the first point is in the upper left.  Flip the y direction
        // to get the tiles in order.
 
        NSInteger zTiles = pow(2, minZ);
        double zSize = zTiles * OVERLAY_SIZE;
        double minZZoomScale = zSize / MKMapSizeWorld.width;
 
        NSInteger flippedMinY = abs(minY + 1 - zTiles);
        NSInteger flippedMaxY = abs(maxY + 1 - zTiles);
 
        double x0 = (minX * OVERLAY_SIZE) / minZZoomScale;
        double x1 = ((maxX + 1) * OVERLAY_SIZE) / minZZoomScale;
        double y0 = (flippedMaxY * OVERLAY_SIZE) / minZZoomScale;
        double y1 = ((flippedMinY + 1) * OVERLAY_SIZE) / minZZoomScale;
 
        boundingRect = MKMapRectMake(x0, y0, x1 - x0, y1 - y0);
 
        paths = pathSet;
    }
    return self;
}
 
- (NSArray *)tilesInMapRect:(MKMapRect)rect zoomScale:(MKZoomScale)scale
{
    NSInteger z = zoomScaleToZoomLevel(scale);
 
    NSMutableArray *tiles = nil;
 
    // The number of tiles either wide or high.
 
    NSInteger zTiles = pow(2, z);
 
    NSInteger minX = floor((MKMapRectGetMinX(rect) * scale) / OVERLAY_SIZE);
    NSInteger maxX = floor((MKMapRectGetMaxX(rect) * scale) / OVERLAY_SIZE);
    NSInteger minY = floor((MKMapRectGetMinY(rect) * scale) / OVERLAY_SIZE);
    NSInteger maxY = floor((MKMapRectGetMaxY(rect) * scale) / OVERLAY_SIZE);
 
    for(NSInteger x = minX; x <= maxX; x++)
    {
        for(NSInteger y = minY; y <=maxY; y++)
        {
            // Flip the y index to properly reference overlay files.
            NSInteger flippedY = abs(y + 1 - zTiles);
            NSString *tileKey = [[NSString alloc] initWithFormat:@"%d/%d/%d", z, x, flippedY];
 
            if ([paths containsObject:tileKey])
            {
                if (!tiles)
                {
                    tiles = [NSMutableArray array];
                }
                MKMapRect frame = MKMapRectMake((double)(x * OVERLAY_SIZE) / scale, (double)(y * OVERLAY_SIZE) / scale, OVERLAY_SIZE / scale, OVERLAY_SIZE / scale);
 
                NSString *path = [[NSString alloc] initWithFormat:@"%@/%@.png", baseDirectory, tileKey];
                MapTile *tile = [[MapTile alloc] initWithFrame:frame path:path];
                [tiles addObject:tile];
            }
        }
    }
    return tiles;
}
 
- (CLLocationCoordinate2D)coordinate
{
    return MKCoordinateForMapPoint(MKMapPointMake(MKMapRectGetMidX(boundingRect), MKMapRectGetMidY(boundingRect)));
}
 
- (MKMapRect)boundingMapRect
{
    return boundingRect;
}
@end

MapOverlayView is a subclass of MKOverlayView we use to construct and draw our tile grid overlay.

MapOverlayView.h

#import 
#import 
 
@interface MapOverlayView : MKOverlayView
{
    CGFloat tileAlpha;
}
 
@property (nonatomic, assign)CGFloat overlayAlpha;
 
@end

MapOverlayView.m

#import "MapOverlayView.h"
#import "MapOverlay.h"
#import "MapTile.h"
 
@implementation MapOverlayView
 
@synthesize overlayAlpha;
 
- (id)initWithOverlay:(id)overlay
{
    if (self = [super initWithOverlay:overlay])
    {
        overlayAlpha = 0.75;
    }
    return self;
}
 
- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale
{
    // Return YES if there are some tiles in this rect at this zoom scale
    MapOverlay *mapOverlay = (MapOverlay *)self.overlay;
 
    NSArray *tilesInRect = [mapOverlay tilesInMapRect:mapRect zoomScale:zoomScale];
    return [tilesInRect count] &gt; 0;
}
 
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
    MapOverlay *mapOverlay = (MapOverlay *)self.overlay;
 
    // Get a list of one or more tile images for this map's rect.
 
    NSArray *rectTiles = [mapOverlay tilesInMapRect:mapRect zoomScale:zoomScale];
 
    CGContextSetAlpha(context, overlayAlpha);
 
    for (MapTile *tile in rectTiles)
    {
        // draw each tile in its frame
        CGRect rect = [self rectForMapRect:tile.frame];
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:tile.path];
 
        CGContextSaveGState(context);
        CGContextTranslateCTM(context, CGRectGetMinX(rect), CGRectGetMinY(rect));
        CGContextScaleCTM(context, 1 / zoomScale, 1 / zoomScale);
        CGContextTranslateCTM(context, 0, image.size.height);
        CGContextScaleCTM(context, 1, -1);
        CGContextDrawImage(context, CGRectMake(0, 0, image.size.width, image.size.height), [image CGImage]);
        CGContextRestoreGState(context);
    }
}
 
@end

To put all these classes to work, we need to make sure our app has a map to work with. To do this we choose a view controller to add a map view to (in our example MapViewController). We also make sure the view controller implements MKMapViewDelegate:

@interface MapViewController : UIViewController <MKMapViewDelegate>

Also add an IBOutlet for a MKMapView in the interface body like so:

IBOutlet MKMapView *map;

Opening our app’s StoryBoard file, we pick the view controller we just updated, add drag a map view control onto it. Opening the Connections Inspector, we can connect the map view’s delegate to our view controller, and our view controller’s map property to the map view control.

Add a Map View to your UIVIewController

Open your StoryBoard file to add a map view to your UIVIewController

Once the map view is set up we can open the map view main class file and add the following to the viewDidLoad method:

 // Initialize the map overlay with tiles in the app's bundle.
 NSString *tileDirectory = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Tiles"];
 
    MapOverlay *overlay = [[MapOverlay alloc] initWithDirectory:tileDirectory];
    [map addOverlay:overlay];
 
    // Set the starting game location.
    CLLocationCoordinate2D startingLocation;
    startingLocation.latitude = 37.76745803822967;
    startingLocation.longitude =-122.44159698486328;
 
    map.region = MKCoordinateRegionMakeWithDistance(startingLocation, 10000, 10000);
    [map setCenterCoordinate:startingLocation];

This will initialize our map overlay with the path to our tile directory. It will also set our map’s starting location.

We also need to add the following method:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
    MapOverlayView *view = [[MapOverlayView alloc] initWithOverlay:overlay];
    view.overlayAlpha = 1.0;
    return view;
}

This method is what provides the map with a overlay grid of our custom map tiles.

Now that our map view is wired up to our custom overlay classes, and our map tiles, let’s run the app.

Our custom map overlay

Our custom map overlay in action.

Tada! We see our map overlay has been added. We also see it supports panning and zooming. With a relatively small number of steps we’ve added some fun to MapKit, and made a map that stands out from the crowd.

28
Dec 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS 4 Comments

Removing Non-Alphanumeric Characters from NSString

Removing non-alphanumeric characters from a string, while maintaining spaces is actually quite easy in Objective-C. Check it out:

// starting string
NSString *myString = [NSString stringWithString:@"#99 bottles of beer on the wall."

// create a new mutable character set
NSMutableCharacterSet *nonAlphaNumericCharacters = [[NSMutableCharacterSet alloc] init];

// add an inverted alphanumeric set to your characters
[nonAlphaNumericCharacters formUnionWithCharacterSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]];

// remove spaces from the set
[nonAlphaNumericCharacters removeCharactersInString:@" "];

// parse out the non-alphanumerics
NSString *myAlphaNumericString = [[myString componentsSeparatedByCharactersInSet:nonAlphaNumericCharacters] componentsJoinedByString:@""];

// results in: 99 bottles of beer on the wall
NSLog(@"results in: %@", myAlphaNumericString);
15
Sep 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Throttling Network Access on the iOS Simulator

Speedlimit is a Leopard preference pane for limiting your network bandwidth to one of a couple different speeds—768k DSL, Edge, 3G, and Dialup. This is really handy for testing iOS apps under normal Edge network conditions in the iPhone Simulator. The new version allows you to restrict the slowdown to only a specific set of hosts.

07
Sep 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Automatic Reference Counting in iOS5

Automatic Reference Counting (ARC) for Objective-C makes memory management the compiler’s job. By enabling ARC with the new LLVM compiler, you’ll never need to type retain or release again. The compiler has a complete understanding of your objects, and releases each object the instant it’s no longer used, so apps run fast, while performing smoothly.

If you’re interested in learning more about ARC take a look at the public spec.

06
Sep 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Getting an array of unique attributes from a NSArray of objects

Need a quick way to grab an array of unique object attributes from an NSArray of objects? Here’s how you do it:

NSArray *uniqueAttributes = [[NSSet setWithArray:[myArrayOfObjects valueForKey:@"attributeName"]] allObjects];
02
Sep 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments

Intro

Hey there, my name is Nate de Jager. I’m an iOS and Android developer (it’s tearing me apart). This is my blog. It’s more of a learning journal for me than you, but feel free to browse as I use it catalog interesting & useful pieces of code.

18
Aug 2011
AUTHOR nate
CATEGORY

Archives

COMMENTS No Comments