Android style unlock screen for iPhone apps using Quartz 2D

I’m working on a new application that will be released on Appstore sometime soon and the full source will be posted on this blog, but I want to share a part of my application already.

I want to restrict access to my application using some kind of password and I really like the pattern style unlock screen found on the Android. If you are a total iPhone nerd/geek and don’t know what I am talking about, check out this clip:

The clip showcase an Android style unlock screen on iPhone available for jailbreaked devices through Cydia. I’ve recorded the sample app I am presenting here as well. I think you can appreciate the similarities between the Cydia app and my app:

Do note however that this is not an unlock screen for the phone itself, it’s a password function for your app.

Features

  • Quartz 2D based
  • Delegate driven (easy to use)
  • Customizable background image
  • Supports any complexity pattern

Unlock screen

The unlock screen subclass UIView and utilize Quartz 2D for drawing circles, lines etc. The unlock screen takes a delegate  and let the delegate handle pattern validation and persistence. The idea is that the unlock screen should be easy to use and still flexible.

//
//  BSKeyLock.h
#import 
 
@protocol BSKeyLockDelegate
@required
-(void)validateKeyCombination:(NSArray*)keyCombination sender:(id)sender;
@end
 
@interface BSKeyLock : UIView {
	CGRect keys[9];
	int currentKeyTouch;
	int keyCombination[9];
	int keyComboCount;
	BOOL keyComboIsValid;
 
	id delegate;
 
}
@property (assign) id delegate;
 
-(void)deemKeyCombinationInvalid;
 
@end

The unlock view defines a simple delegate protocol which is used for validating combination input.

//
//  BSKeyLock.m
//
 
#import "BSKeyLock.h"
#define POINT_SIZE			30.0
#define CIRCLE_SIZE			40.0
#define CIRCLE_X_START		35.0
#define CIRCLE_Y_START		55.0
#define CIRCLE_X_PADDING	110.0
#define CIRCLE_Y_PADDING	80.0
 
@implementation BSKeyLock
@synthesize delegate;
 
- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
 
		// Create rectangles to draw buttons in
		for(int i = 0; i < 9; i++)
		{
			// Calculate the row number
			int row = floor(i/3);
 
			// Calculate row position
			int rowPos = i % 3;
 
			// Create the rect calculating it's position on screen.
			keys[i] = CGRectMake(CIRCLE_X_START + (rowPos * CIRCLE_X_PADDING), 
								 CIRCLE_Y_START + (row * CIRCLE_Y_PADDING), 
								 CIRCLE_SIZE, 
								 CIRCLE_SIZE);
 
		}
 
		self.backgroundColor = [UIColor grayColor];
    }
 
    return self;
}
 
- (BOOL) isMultipleTouchEnabled {return NO;}
 
// Checks if the given point is inside a button. If so, the button position
// is returned. If not, -1 is returned.
-(int) isTouchingKey:(CGPoint)point
{
	// Go through our key rect array and see if the users' finger is
	// inside any of the rectangles
	for(int i = 0; i < 9; i++)
	{
		if(CGRectContainsPoint(keys[i], point))
		{
			return i;
		}
	}
 
	return -1;
}
 
-(void) addKeyToCombo:(int)keyNumber
{
	keyCombination[keyComboCount] = keyNumber;
	keyComboCount++;
}
 
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	NSLog(@"Touch began");
	keyComboCount = 0;
	keyComboIsValid = YES;
 
	CGPoint touchLocation = [[touches anyObject] locationInView:self];
	currentKeyTouch = [self isTouchingKey:touchLocation];
 
	if(currentKeyTouch > -1)
	{
		[self addKeyToCombo:currentKeyTouch];
	}
 
	[self setNeedsDisplay];
}
 
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
	if(keyComboCount > 0)
	{
		// This occurs when the user lifts their finger from the screen
		NSLog(@"Touch ended");
		currentKeyTouch = -1;
		[self setNeedsDisplay];
 
		// If a delegate is set we want to inform which keys has been touched
		if(delegate != nil)
		{
			NSMutableArray *keyCombo = [[[NSMutableArray alloc] init] autorelease];
 
			for(int i = 0; i < keyComboCount; i++)
			{
				[keyCombo addObject:[NSNumber numberWithInt:keyCombination[i]]];
			}
 
			[delegate validateKeyCombination:keyCombo sender:self];
		}
	}
}
 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
	CGPoint touchLocation = [[touches anyObject] locationInView:self];
	int key = [self isTouchingKey:touchLocation];
 
	if(key > -1 && key != currentKeyTouch)
	{
		BOOL addToCombo = YES;
 
		for(int i = 0; i < keyComboCount; i++)
		{
			if(keyCombination[i] == key)
			{
				addToCombo = NO;
				break;
			}
		}
 
		if(addToCombo)
		{
			[self addKeyToCombo:key];
		}
 
	}
 
	currentKeyTouch = key;
 
	[self setNeedsDisplay];
}
 
 
- (void)drawRect:(CGRect)rect {
 
 
	CGContextRef context = UIGraphicsGetCurrentContext();
 
	// Draw the lines between the points. Because we have three layers of controls
	// where all other elements are drawn above the lines we start with the lines. 
	if(keyComboCount > 1)
	{
		CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
		CGContextSetLineWidth(context, 15.0);
 
		// Add points to our stroke path
		for (int i = 0; i < keyComboCount; i++) 
		{
			CGRect r = keys[keyCombination[i]];
 
			if(i == 0)
			{
				CGContextMoveToPoint(context, r.origin.x + (r.size.width/2), r.origin.y + (r.size.height/2));
			}
			else 
			{
				CGContextAddLineToPoint(context, r.origin.x + (r.size.width/2), r.origin.y + (r.size.height/2));
			}
		}
 
		// Draw the line
		CGContextStrokePath(context);				
	}
 
 
 
	CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
	CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 0.5);
	CGContextSetLineWidth(context, 15.0);
 
 
	// Draw semi transparent circles
	for(int i = 0; i < 9; i++)
	{
		CGContextAddEllipseInRect(context, keys[i]);		
	}
	CGContextStrokePath(context);
 
	// Draw white buttons on top of circles
	for(int i = 0; i < 9; i++)
	{
		CGRect r = keys[i];
		CGContextFillEllipseInRect(context, CGRectMake(r.origin.x + ((CIRCLE_SIZE - POINT_SIZE) / 2), 
													   r.origin.y + ((CIRCLE_SIZE - POINT_SIZE) / 2), 
													   POINT_SIZE, 
													   POINT_SIZE));		
	}
 
 
	if(keyComboCount > 0)
	{
		// Draw key touch marker (the green thin marker)
		CGContextSetLineWidth(context, 2.0);
 
		if(keyComboIsValid) {
			CGContextSetRGBStrokeColor(context, 0.0, 0.8, 0.0, 1.0);
		}
		else {
			CGContextSetRGBStrokeColor(context, 0.8, 0.0, 0.0, 1.0);
		}
 
		for (int i = 0; i < keyComboCount; i++) 
		{
			CGRect r = keys[keyCombination[i]];
			CGContextAddArc(context, r.origin.x + (r.size.width/2), r.origin.y + (r.size.height/2), 28.0, 0.0, M_PI*2, false);
			CGContextStrokePath(context);
		}
	}
 
	if(keyComboCount > 1)
	{
		// Draw line direction indicator (the red arrow). The loop starts at position
		// 1 because we can't draw an angle indicator with just one point. 
		for (int i = 1; i < keyComboCount; i++) 
		{
			CGRect r = keys[keyCombination[i]];
			CGRect previousR = keys[keyCombination[i-1]];
 
			// Calculate angle between coordinates in radians
			float angle = atan2(r.origin.y-previousR.origin.y, r.origin.x-previousR.origin.x);
 
			// Save the context because we are rotating things here
			CGContextSaveGState(context);
			CGContextSetRGBFillColor(context, 0.8, 0, 0, 1);
			CGContextBeginPath(context);	
 
			// Translate the context so the center of the context is the center of the circle
			// where we want to draw the arrow
			CGContextTranslateCTM(context, CGRectGetMidX(previousR), CGRectGetMidY(previousR));
 
			// After translating we rotate
			CGContextRotateCTM(context, angle);
 
			// Set points that make up the triangle
			CGContextMoveToPoint(context, 18.0,-6.0);
			CGContextAddLineToPoint(context, 24.0,0.0);
			CGContextAddLineToPoint(context, 18.0,6.0);
 
			// Draw and fill the triangle
			CGContextClosePath(context);
			CGContextFillPath(context);
 
			// Restore our old context.
			CGContextRestoreGState(context);	
 
		}
	}
}
 
-(void)deemKeyCombinationInvalid
{
	keyComboIsValid = NO;
	[self setNeedsDisplay];
}
 
- (void)dealloc {
    [super dealloc];
}
 
 
@end

The drawing is pretty straight forward. Quartz 2D is a very competent drawing engine when working with application style UI. The most complex part of drawing the interface is the directional arrows (red triangles). Hopefully the code comments explain clearly enough what’s going on, if not, drop me a line below the post!

Using the unlock screen

My sample application demonstrate how to use the unlock screen to both record a pattern and validate a pattern (see the YouTube video above). The “password” or pattern used to unlock is easily stored into the application settings as well as the number of attempts to authenticate. By utilizing a  delegate the same unlock screen code can be used to both record a pattern and to validate it. The “deemKeyCombinationInvalid” function turns the circles red, informing the user that the pattern is invalid.

Record pattern

-(void)validateKeyCombination:(NSArray*)keyCombination sender:(id)sender
{
	// We require at least four points to be connected in the combination
	if([keyCombination count] > 3)
	{
		// Store the combo and remove the keypad.
		NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];
		[settings setObject:keyCombination forKey:@"KeyLockCombo"];
		[settings synchronize];
 
		[keyLock.view removeFromSuperview];
 
		lockSwitch.on = YES;
	}
	else 
	{
		[(BSKeyLock*)sender deemKeyCombinationInvalid];		
	}
}

Validate pattern

-(void)validateKeyCombination:(NSArray*)keyCombination sender:(id)sender
{
 
	NSUserDefaults *settings = [NSUserDefaults standardUserDefaults];
	NSArray *storedKeyCombo = [settings objectForKey:@"KeyLockCombo"];
	int storedKeyComboCount = [storedKeyCombo count];
 
	BOOL comboIsValid = storedKeyComboCount == [keyCombination count];
 
	if (comboIsValid)
	{
		for (int i = 0; i < storedKeyComboCount; i++) {
			if([storedKeyCombo objectAtIndex:i] != [keyCombination objectAtIndex:i])
			{
				comboIsValid = NO;
				break;
			}
		}
	}
 
	if(comboIsValid)
	{
		// The combination is valid, let the user into the app.
		[[keyLockController view] removeFromSuperview];
		[window addSubview:loggedInController.view];
 
		// Reset the attempts to log in
		[settings setObject:[NSNumber numberWithInt:0] forKey:@"KeyLockFailedTries"];
 
		[keyLockController release];
	}
	else {
 
		int attempt = 0;
		if([settings objectForKey:@"KeyLockFailedTries"] != nil) {
			attempt = [[settings objectForKey:@"KeyLockFailedTries"] intValue];
		}
 
		// Increase the failed attempts to log in		 
		attempt++;
		[settings setObject:[NSNumber numberWithInt:attempt] forKey:@"KeyLockFailedTries"];
 
		if(attempt < 3)
		{
			[(BSKeyLock*)sender deemKeyCombinationInvalid];
			keyLockController.titleText = [NSString stringWithFormat:@"Sorry, try again! Attempts left: %d", 3 - attempt];
		}
		else 
		{
			// The user failed three times. Remove all settings and let them into the app
			[settings setObject:[NSNumber numberWithInt:0] forKey:@"KeyLockFailedTries"];
			[settings setObject:nil forKey:@"KeyLockCombo"];
 
 
			// Show an alert informing the user that he/she screwd up!
			UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Too many failed attempts!" 
															message:@"Due to too many failed login attempts the application has been wiped of information." 
														   delegate:nil 
												  cancelButtonTitle:@"OK" 
												  otherButtonTitles:nil];
			[alert show];
			[alert release];
 
			[[keyLockController view] removeFromSuperview];
			[window addSubview:loggedInController.view];
 
			[keyLockController release];
		}
	}
 
	// Persist changes
	[settings synchronize];
 
}

Download sample project

Enjoy the code. As usual there is no copyright etc, use it anyway you like. If you do like it, please drop an encouraging comment below.
DOWNLOAD – Android style unlock screen for iPhone apps

*NOTE* The included padlock icon image comes from http://www.glyphish.com. Check them out, their icons are awesome!

4 thoughts on “Android style unlock screen for iPhone apps using Quartz 2D

  1. Hi merrimack, i was searching for this kind of app, i tried to install mac os 10.5.7 iatkos on my dell inspiron 1525, to install the SDK and start coding, but when I realized that I have on my iphone 4.2.1 IOS I need the SDK 4.2 Mac OS 10.6 install discontinuance to do so there will be a way to install your app on my iphone?

  2. Hi merrimack,

    Every thing was perfect and the sample application was great too.

    The only thing was the colors were not configurable. The view doesn’t have properties like lineColor,inCircleColor,EnCircleColor.numberColor. Which apparently making it harder to implement in own apps as the theme would differ in different apps..

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>