I was recently working on an app that required a uniform presentation of objects on the screen as a result of tap gestures. In other words I needed to make sure that approximate, tapped locations became exact points on the screen that the app could keep track of. I thought this might be worth sharing. This tutorial will demonstrate how to create a snap-to grid in an iOS app using a custom UIView and tap gestures.
Here are the app requirements for this tutorial:
When the user opens the app, the user will see a yellow screen with red gridlines. These gridlines will show the intersections or resolution points for the snap-to behavior. When the user taps the screen. A light gray dot will be added to the screen at the nearest intersection to which the tap was made. The user can delete all of the dots with a right swipe gesture.
When learning, it’s best to use as little template help as possible so that you understand the building blocks of an iOS application. For that reason, I will choose to start with an empty application. If your current version of Xcode is missing the ‘Empty Application’ template, please see my earlier post, ‘Nothing From Something’. Let’s get started.
Select File > New > Project > Empty Application
Fill in the project options. If you are using Xcode > 5, select Objective-C as the language. I’ll post a Swift version later.
Make sure that Foundation, UIKit and CoreGraphics frameworks have been added to the project under Build Phases > Link Binary With Libraries.
Highlight the AppDelegate.m file in the project navigator. We will create a new Objective-C class file right below this.
Select File > New > File
In the template drop-down, under the iOS section, highlight Cocoa Touch and click on Objective-C class. Click Next.
If you are using Xcode > 5, under the iOS section, highlight Source and click on Cocoa Touch Class. Click Next.
Let’s name the class ‘Dot’ and make it a subclass of NSObject. This should leave the ‘create XIB file’ checkbox and platform selection unavailable, which is good, because we won’t be using XIB’s. Again, if you’re using Xcode > 5, select Objective-C for the language. Click ‘Create’ on the next screen to save the file.
For now, the ‘Dot’ class will have one property of type CGPoint which we will name ‘location’. CGPoint is a struct declared in the CoreGraphics framework that contains two members: x and y, which are of type CGFloat and define a point in a two-dimensional coordinate system. When the user taps the UI screen, the coordinates of it’s ‘locationInView’ will be adjusted to the closest grid resolution points, an instance of Dot will be created and it’s location will be set to the adjusted values. That dot will then be added to an NSMutableArray and drawn to the screen. Sounds simple enough
Your Dot.h file should look like this:
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
@interface Dot : NSObject
@property (nonatomic) CGPoint location;
@end
Now, we need to create our custom UIView class. This class will be responsible for receiving touch events, adding the dots to a collection and drawing those dots to the screen. Use the same steps we used above in creating the ‘Dot’ class however, let’s name this class ’SnapGridView’. Make sure it only inherits from NSObject so there is no starting code in the implementation file. After it is created, change the superclass to UIView. Replace the code in SnapGridView.h with the following:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface SnapGridView : UIView {
int horizontalSpacing;
int verticalSpacing;
NSMutableArray *allDots;
}
@end
We have a couple instance variables that we’ll use to store the spacing for the snap-to grid which is going to be dependent on the UIScreen size and an NSMutableArray for storing the Dot objects.
Now, replace the code in SnapGridView.m with the following:
#import "SnapGridView.h"
#import "Dot.h"
@implementation SnapGridView
static const int kNUM_X_POINTS_PLUS_1 = 5;
static const int kNUM_Y_POINTS_PLUS_1 = 7;
- (id)initWithFrame:(CGRect)r
{
self = [super initWithFrame:r];
if (self) {
[self setBackgroundColor:[UIColor yellowColor]];
allDots = [[NSMutableArray alloc] init];
// This is the recognizer to add a dot with a single tap
UITapGestureRecognizer *tapRecognizer =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(tap:)];
[tapRecognizer setNumberOfTapsRequired:1];
[self addGestureRecognizer:tapRecognizer];
// This is the recognizer to delete all of the notes with
// a swipe to the right
UISwipeGestureRecognizer *swipeRecognizer =
[[UISwipeGestureRecognizer alloc] initWithTarget:self
action:@selector(swipe:)];
[self addGestureRecognizer:swipeRecognizer];
}
return self;
}
- (void)drawRect:(CGRect)rect {
// Calculate horizontal and vertical spacing which is UI screen size dependent
horizontalSpacing = self.window.bounds.size.width / kNUM_X_POINTS_PLUS_1;
verticalSpacing = self.window.bounds.size.height / kNUM_Y_POINTS_PLUS_1;
// Draw grid
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.0);
CGContextSetLineCap(context, kCGLineCapSquare);
[[UIColor redColor] set];
// Draw horizontal grid lines
for (int y = 0; y < self.window.bounds.size.height; y += verticalSpacing) {
CGContextMoveToPoint(context, 0, y);
CGContextAddLineToPoint(context, self.window.bounds.size.width, y);
CGContextStrokePath(context);
}
// Draw vertical grid lines
for (int x = 0; x < self.window.bounds.size.width; x += horizontalSpacing) {
CGContextMoveToPoint(context, x, 0);
CGContextAddLineToPoint(context, x, self.window.bounds.size.height);
CGContextStrokePath(context);
}
// Draw Dots
CGContextRef drawDot = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(drawDot, 25.0);
CGContextSetLineCap(drawDot, kCGLineCapRound);
[[UIColor lightGrayColor] set];
for (Dot *dot in allDots) {
CGContextMoveToPoint(drawDot, dot.location.x, dot.location.y);
CGContextAddLineToPoint(drawDot, dot.location.x, dot.location.y);
CGContextStrokePath(drawDot);
}
}
- (void)tap:(UIGestureRecognizer *)gr
{
// Get the initial tap location
CGPoint loc = [gr locationInView:self];
// Adjust the x value
if (loc.x < horizontalSpacing) {
// Adjusted x value should be a minimum of the horizontalSpacing,
// or the leftmost usuable x position.
loc.x = horizontalSpacing;
} else {
if (loc.x > self.window.bounds.size.width - horizontalSpacing) {
// Adjusted x value should be (self.window.bounds.size.width - horizontalSpacing),
// or rightmost usuable x position.
loc.x = self.window.bounds.size.width - horizontalSpacing;
} else {
if (((int)loc.x % (int)horizontalSpacing) < (horizontalSpacing / 2)) {
// need to subtract the (loc.x % horizontalSpacing)
loc.x = loc.x - ((int)loc.x % (int)horizontalSpacing);
} else {
if (((int)loc.x % (int)horizontalSpacing) >= (horizontalSpacing / 2)) {
loc.x = loc.x + (horizontalSpacing - ((int)loc.x % (int)horizontalSpacing));
}
}
}
}
// Adjust the y value
if (loc.y < verticalSpacing) {
// Adjusted y value should be a minimum of verticalSpacing,
// or the topmost usuable y position.
loc.y = verticalSpacing;
} else {
if (loc.y > (self.window.bounds.size.height - verticalSpacing)) {
// Adjusted y value should be (self.window.bounds.size.height - verticalSpacing),
// or the bottommost usuable y position.
loc.y = (self.window.bounds.size.height - verticalSpacing);
} else {
if (((int)loc.y % (int)verticalSpacing) < (verticalSpacing / 2)) {
// need to subtract the (loc.y % verticalSpacing)
loc.y = loc.y - ((int)loc.y % (int)verticalSpacing);
} else {
if (((int)loc.y % (int)verticalSpacing) >= (verticalSpacing / 2)) {
loc.y = loc.y + (verticalSpacing - ((int)loc.y % (int)verticalSpacing));
}
}
}
}
// Write the final adjusted location value to allDots array
Dot *dot = [[Dot alloc] init];
dot.location = loc;
[allDots addObject:dot];
[self setNeedsDisplay];
}
- (void)swipe:(UIGestureRecognizer *)gr
{
[allDots removeAllObjects];
[self setNeedsDisplay];
}
@end
In the ‘initWithFrame’ method, the ‘allDots’ array is initialized, the background color is set and the UITapGestureRecognizer as well as the UISwipeGestureRecognizer are initialized. The swipe recognizer will be for convenience to clear the dots off of the screen without having to close the app. We are going to override the drawRect method of UIView, which in it’s default implementation does nothing. In drawRect, we will set the values of our horizontal and vertical spacing variables by dividing the UIWindow. We can change the number of snap-to grid points by changing the value of constants ‘kNUM_X_POINTS_PLUS_1’ and ‘kNUM_Y_POINTS_PLUS_1’. The ‘PLUS_1’ is a reference to the fact that we need to divide the UIWindow by one more than the number of available grid points in the window. So, if you want four points across, kNUM_X_POINTS_PLUS_1 should equal 5.
Before drawRect is called in a view, a ‘current context’ is created. We will grab a pointer to that context, set the line width and color and use a couple ‘for’ loops to draw the gridlines. We then draw the array of dots. When the tap method is called by the UITapGestureRecognizer, we first get the locationInView then proceed to adjust that position to resolve to one of our snap-to points. Basically, for each x and y coordinate, we first check to see if it’s outside of the outermost points. If it is, we resolve that coordinate to the nearest outermost coordinate. If the coordinate is already within those boundaries, we use the modulus (%) operator to return the difference of the coordinate value divided by the spacing value we obtained using the kNUM_X_POINTS_PLUS_1 and kNUM_Y_POINTS_PLUS_1 constants. If that difference is less than half of the spacing’s value, it will be adjusted to the lesser grid value by subtracting the difference from the current coordinate value. If the difference is more than half the spacing’s value we adjust it in the other direction. For convenience, a swipe right gesture will erase all of the dots.
In the AppDelegate.m file, add the code, in the ‘applicationDidFinishLaunchingWithOptions’ method, to create an instance of our custom ’SnapGridView’. Don’t forget to import the SnapGridView.h file at the top of the AppDelegate.m file. Also, ignore the warning that application windows are expected to have a root view controller. In a real app this custom view would be set as the view of a view controller. The AppDelegate file should now look like this:
#import "AppDelegate.h"
#import "SnapGridView.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
CGRect viewFrame = CGRectMake(0, 0, self.window.bounds.size.width, self.window.bounds.size.height);
SnapGridView *snapGridView = [[SnapGridView alloc] initWithFrame:viewFrame];
[[self window] addSubview:snapGridView];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
For brevity, I’ve only shown the top of the AppDelegate.m file down through the ‘applicationDidFinishLaunchingWithOptions’ method body.
Challenge:
There is a slight bug in this implementation that will cause some improperly placed points on the outermost gridlines when certain screen division values are used. (kNUM_X_POINTS_PLUS_1 and kNUM_Y_POINTS_PLUS_1). See if you can change/add the code to fix this problem.
I hope this example is helpful.


