Code, Code, Revolution!
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.
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!
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.
-(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]; } }
-(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]; }
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!
With this blog I try to provide useful tips and solutions for programming .NET, Objective-C and more. My name is Björn Sållarp, and I love writing code.
It's now available on AppStore. It's free and open source. Read more about the app here: Swedish / English
ashish kumar pandita
December 29th, 2010 at 10:55 am
nice one thanks! waiting for some new ones
miguel
February 27th, 2011 at 11:53 pm
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?