sábado, 23 de febrero de 2013

JAPanoView updated: Hotspots

Just updated my panorama viewer UIView subclass. It now supports hotspots: instantiate any UIView subclass and add it as a hotspot with the new method addHotspot:atHAngle:vAngle: indicating the horizontal angle (azimuth) and vertical angle (elevation). To remove a hotspot form a JAPanoView just call removeFromPanoView on the hotspot instance (like addSubview: and removeFromSuperview).

Since any UIView can be a hotspot, you can add any UIControl or add any UIGestureRecognizer to your hotspot and make it fully user interactive. You can also take advantage of the convertPoint/Rect:from/toView: UIView methods and show popovers from a hotspot as seen in the demo project:


[popover presentPopoverFromRect:hotspot.frame inView:hotspot.superview permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];


There's also a new property for UIView shouldApplyPerspective. It indicates if the hotspot should be rendered always parallel to the screen (shouldApplyPerspective==NO) or perpendicular to the point-of-view/hotspot axis (shouldApplyPerspective==YES; default value).

You can get the code from the repository at bitbucket.

viernes, 22 de febrero de 2013

Integrating KSCrash with JIRA Mobile Connect

KSCrash is a powerful crash reporting library with a lot of extended features such as on-device symbolication. It offers an extensible plugin-like architecture to integrate with your favorite issue tracking platform, based on what KSCrash calls sinks. A sink is kind of a push endpoint for crash reports generated by KSCrash. In addition, KSCrash can be configured with no sink to automatically catch and store crash reports; KSCrash offers a set of APIs to query and retrieve stored crash reports.

JIRA Mobile Connect is a free, open-source library for collecting user feedback directly from your mobile app to JIRA issue tracking platform. It offers automated crash reporting using PLCrashReporter wich is, in my opinion, a bit outdated and falls short in features.

If you are an iOS developer using JIRA and want to ease and improve your automated crash reporting system, integrating JMC with KSCrash will save you lots of headaches and pain.

Push or pull

You can use KSCrash using push or pull crash report extraction. Using push, KSCrash is initialized using a sink that will be called when reports are available. Using pull, KSCrash is initialized with no sink, and some other code should query it to retrieve and process reports.

If you are not interested in any JMC feature apart from crash reporting, you can use push architecture writing a KSCrash sink that uses JMCCrashTransport class to send crashes to JIRA, but it's a bit tricky since JMCCrashTransport is not designed to be used directly and it has dependencies upon top level library classes such as JMC.

JMC uses a pull architecture for crash report extraction from PLCrashReporter. Thankfully, there's only one single point of integration between JMC and PLCrashReporter: the CrashReporter class. CrashReporter is a singleton that offers the following interface:

- (BOOL) hasPendingCrashReport;
- (NSArray*) crashReports;
- (void) cleanCrashReports;

Writing a new implementation of CrashReporter is easy, since KSCrash offers direct implementations of all three services. But KSCrash reports are returned in NSDictionary format and JMC expects you to return an array of NSStrings, so some processing should be taken to write the human readable report.

- (void)cleanCrashReports
{
    [[KSCrash instance] deleteAllReports];
}

-(BOOL)hasPendingCrashReport{
    return [[KSCrash instance] reportCount]>0;
}

In addition to sinks, KSCrash offers filters for report processing. Filters take an array of reports, process them and output them; usually yo configure a filter chain, where a filter takes input from another filter output, with a sink at the endpoint. But filters can be used on their own to do any sort of report processing, and KSCrash comes with a set of predefined filters. One of these filters transforms NSDictionary reports to NSString reports mimicking Apple's crash report format: KSCRashReportFilterAppleFmt.

- (NSArray *)crashReports
{
    KSCrashReportFilterAppleFmt *appleFormatFilter = [KSCrashReportFilterAppleFmt filterWithReportStyle:KSAppleReportStyleSymbolicated];
    __block NSArray *result=nil;
    
    /*
     WARNING: this code works since KSCrashReportFilterAppleFmt works synchronously. Using an asynchronous filter will result in undefined behaviour.
     To use async filters, make sure this method (crashReports) is not called on main thread and use GCD semaphores
     */
    [appleFormatFilter filterReports:[[KSCrash instanceallReportsonCompletion:^(NSArray *filteredReports, BOOL completed, NSError *error) {
        result=filteredReports;
    }];
    
    return result;
}

Note the warning. Filters take a completion block but there is no default behavior on how or when will the block be executed. The Apple format filter executes the block synchronously, but other filters/sinks may execute it asynchronously. To make it work with asynchronous filters you should use GCD semaphores and queues and make sure your code does not execute in the main thread since the semaphore could block it; you should never block the main thread, and even if you are thinking about it just forget: the filter/sink may use the main thread and you would be causing a deadlock.

ARC notes:
- KSCrash source files are ARC agnostic, it can be compiled using ARC or non-ARC
- JMC source files are non-ARC
- My CrashReport class code is ARC

So remember to set the appropriate -fno-objc-arc or -fobjc-arc flags as needed depending on your project global settings.

You can get the code for CrashReport class from a bitbucket repository.

sábado, 28 de abril de 2012

Tweaking UIScrollView (I): restrict touch event handling

Sometimes you want to get the same swiping, panning, paging, bouncing or deceleration behaviors that UIScrollView provides but it's not a direct fit for your needs. I'm starting a series of posts about how to tweak UIScrollView to get some of its standard system-wide behaviors but discard the ones you don't want.

In this first article of the series, we will discuss how to restrict touch event handling in a UIScrollView to one of its subviews. This could be useful in some situations... if you want to put a partially hidden view the user can drag to fully show it, for example a hidden side menu; you can do it "by hand" with gesture recognizers, or get advantage of UIScrollView behaviors.

In practice


Let's assume you want to add a menu to your app, but in order to get the most from your screen real estate you want it to be hidden at the bottom of the screen. To reveal the menu, the user will drag or swipe up from a little menu tab; to hide the menu, drag or swipe down.

The UIScrollView approach to this problem is really simple: create a UIScrollView where the menu will be inserted as subview. Place the menu accordingly into the scroll view and set the scroll view content size. You can download a sample project from https://bitbucket.org/javieralonso/uiscrollview-tweaks/src

Execute the sample project: when you drag up the menu, it goes up, but it also goes up if you pan anywhere in the scroll view. This is a bit annoying, and prevents the user from interacting from content below the scroll view: the menu should move only when the user touches over it.

But, how to?

In Cocoa Touch, every time user touches the screen the Application window tries to know which view should receive the touch event. To do this, UIView implements the following selectors:

pointInside:withEvent:
hitTest:withEvent:

First selector asks a view if a given point falls within the view. The second one, is a recursive selector to ask a view for its farthest descendant in the view hierarchy (including itself) that contains a specified point. Thus, the only thing we have to do is to subclass UIScrollView and override pointInside:withEvent: to return YES if, and only if, any of its subviews returns YES:

@interface JAFirstTweakScrollView : UIScrollView

@end

@implementation JAFirstTweakScrollView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    for (UIView *subview in self.subviews) {
        if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event]) {
            return YES;
        }
    }
    return NO;
}

@end

You can try this in the sample project from the previous link. Simply open ViewController.xib and change the class for the ScrollView from the default UIScrollView to JAFirstTweakScrollView:


That's it. Now, you can benefit from standard UIScrollView behavior: consistent scroll sensitivity, swipe speed, scroll bounce, etc. You can also enable paging in your scroll view; do it on the sample project and see how it works.

martes, 13 de marzo de 2012

Child UITableViewController with cell prototyping on Storyboard

One of the most powerful and code reduction tools in iOS 5 SDK is the Cell Prototyping with Storyboards. You can design directly in Xcode your dynamic cells, and even design the whole table view cells if your table cells are static (as in a grouped style table view to build a menu) and assign outlets to cell subviews to programmatically set its content.

Other new and powerful tool in iOS 5 is View Controller containment, through view controller hierarchy manipulation; the "one screenful of content equals one UIViewController" rule is no longer enforced. Now you can get better code reuse since UIViewController has a list of "child" view controllers, one in charge of controlling one branch of its view hierarchy; UIViewController can transfer the control of one view to another view controller.

But, what if you try to mix both technologies? Storyboarding does not have a direct way to define child view controllers. Some people (as in here) are doing it the wrong way: add an "Object" to the scene, change its class and add some code. In some situations it may work, but it's not the way to go: try to do it with a child UITableViewController with static cells.

How to do it then?

First of all: create a new scene in your storyboard with your UITableViewController subclass and add your static cells as usual. Do not connect this newly created scene with any segue, but don't forget to assign the proper value to the identifier field of your view controller


Now, in your parent view controller add some code in its -awakeFromNib and -viewDidLoad method implementation:

-(void)awakeFromNib{
    // instantiate and assign the child view controller to a property to have direct reference to it in
    self.propiedadesVC=[self.storyboard instantiateViewControllerWithIdentifier:@"BGRutaPropiedadesListViewController"]; 
    // configure your child view controller
    self.propiedadesVC.ruta=_ruta
    // add your child view controller to children array
    [self addChildViewController:self.propiedadesVC]; 

    [self.propiedadesVC didMoveToParentViewController:self];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // configure chld view controller view's frame
    self.propiedadesVC.view.frame=CGRectMake(640, 0, self.scrollView.bounds.size.width, self.scrollView.bounds.size.height);
    // add child's view to view hierarchy
    [self.scrollView addSubview:self.propiedadesVC.view];
}

In this sample, the view is added as a subview of a paging scroll view. In case you need to scroll through n views you can instantiate any number of child view controllers.

Simple and straightforward. Storyboarding rocks!

domingo, 4 de marzo de 2012

JAPanoView: open source panorama viewer

I've just published an open source UIView subclass to make awesome interactive 360-180 degrees panorama from 6 images, one for each face of a cube.

The underlying foundation of this component is the CATransform3D matrix from CoreAnimation framework. Applying the correct 3D transform to each of the CALayers you can get a 3D illusion from 2D images. More details on the topic can be found here: Core Animation: 3D perspective

You can get the source code from my bitbucket repository: https://bitbucket.org/javieralonso/japanoview/


Special thanks to Russ Woodman from Remedy Panoramic Design for his permission to use his photos for the sample project.

Update: it now supports hotspots.

miércoles, 15 de febrero de 2012

Mimic iPad system keyboard with Core Graphics

Since iOS 3.2, UIResponder class has a property called inputView. This UIView will be shown like the keyboard when the responder becomes first responder. Using this property we can make our own custom keyboards, allowing for specialized input in our apps like Numbers for iPad:


But, how to mimic system keyboard appearance to avoid crap like the following?


First, draw the keyboard background gradient. Subclass UIView with your own class (JACustomKeyboard) and use this drawRect code:

- (void)drawRect:(CGRect)rect
{
    // Drawing code
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    UIColor *ucolor1=[UIColor colorWithRed:157.0/255.0 green:157.0/255.0 blue:167.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:67.0/255.0 green:67.0/255.0 blue:75.0/255 alpha:1];
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    
    ucolor1=[UIColor colorWithRed:50.0/255.0 green:50.0/255.0 blue:50.0/255 alpha:1];
    ucolor2=[UIColor colorWithRed:191.0/255.0 green:191.0/255.0 blue:191.0/255 alpha:1];
    
    [ucolor1 setFill];
    CGContextFillRect(context, CGRectMake(0, 0, rect.size.width, 1));
    [ucolor2 setFill];
    CGContextFillRect(context, CGRectMake(0, 1, rect.size.width, 1));
    
    CGContextRestoreGState(context);
}

Yo may have noticed there are two calls to CGContextFillRect in addition to CGContextDrawLinearGradient. With those we are drawing the upper border of the keyboard as seen on the system one:


To get our keys, we will subclass UIControl with a class named JACustomKey declared as follows:

@interface ANCustomKey : UIControl

@property (nonatomic, strong) NSString *text;
@property (nonatomic, strong) UIImage *image;

@end

We will use text and image properties to set the the symbol for the key.

First, use CoreAnimation to drop the key shadow:

-(id)initWithFrame:(CGRect)frame{
    self=[super initWithFrame:frame];
    if (self) {
        self.layer.shadowOffset=CGSizeMake(0, 1);
        self.layer.shadowRadius=1;
        self.layer.shadowOpacity=0.7;
        self.layer.shadowColor=[UIColor blackColor].CGColor;
        self.layer.shadowPath=[self newPathForRoundedRect:self.bounds radius:7];
        self.text=@"A";
    }
    return self;
}

Next, we add some easy common stuff:

-(void)setImage:(UIImage *)image{
    _text=nil;
    if (image!=_image) {
        _image=image;
    }
    [self setNeedsDisplay];
}

-(void)setText:(NSString *)text{
    _image=nil;
    if (text!=_text) {
        _text=text;
    }
    [self setNeedsDisplay];
}

-(void)setHighlighted:(BOOL)highlighted{
    [super setHighlighted:highlighted];
    [self setNeedsDisplay];
}

-(void)setSelected:(BOOL)selected{
    [super setSelected:selected];
    [self setNeedsDisplay];
}

-(void)setEnabled:(BOOL)enabled{
    [super setEnabled:enabled];
    [self setNeedsDisplay];
}

-(BOOL)isOpaque{
    return NO;
}

-(UIColor *)backgroundColor{
    return [UIColor clearColor];
}

Now, we are ready to get into Core Graphics to draw our beautiful system-like key. In a first approach, we should draw a background gradient, a top inner light shadow, a bottom inner dark shadow and a border stroke:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    UIColor *ucolor1=[UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:241.0/255 alpha:1];
    
    UIColor *ucolor2=[UIColor colorWithRed:211.0/255.0 green:211.0/255.0 blue:217.0/255 alpha:1];
    
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);

    CGPathRelease(keyBorderPath);

    CGContextRestoreGState(context);
}

Note that if the key is selected or highlighted, the colors for the gradient change to darker variants. Also, our key border CGPath is created using an external function to create a rounded rect. And this is what we get with this piece of code:


Pretty, but a problem arises: keys at the top look darker that ones at the bottom due to keyboard background gradient... the system keyboard does not look like this. See:



To solve this issue, we should add an additional global gradient to make bottom keys darker than ones at the top. Remember to change the existing gradient colors to a lighter ones as the new adding gradient will darken them:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    UIColor *ucolor1=[UIColor colorWithRed:241.0/255.0 green:241.0/255.0 blue:243.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:223.0/255.0 green:223.0/255.0 blue:231.0/255 alpha:1];
    
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);

    //overall gradient
    
    CGContextSaveGState(context);
    ucolor1=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0];
    ucolor2=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0.4];
    
    color1=ucolor1.CGColor;
    color2=ucolor2.CGColor;
    
    colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    topCenter = CGPointMake(CGRectGetMidX(currentBounds), -(self.frame.origin.y));
    midCenter = CGPointMake(CGRectGetMidX(currentBounds), self.superview.bounds.size.height-(self.frame.origin.y));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);

    CGPathRelease(keyBorderPath);

    CGContextRestoreGState(context);
}

Ok. This is how it looks like:


Way better. Let's draw the key symbol.

Our key will be capable of draw a text string or an image. One or the other, not both. And to mimic real general use keyboards we will use the supplied image as a draw mask: no color, only black (gray if key is disabled). To draw the text, we will use Core Text. And we will not forget to add the final touch of a white shadow to our symbol.

Let's see the code:

-(void)drawRect:(CGRect)rect{
    CGContextRef context=UIGraphicsGetCurrentContext();
    
    CGContextSaveGState(context);
    
    CGPathRef keyBorderPath=[self newPathForRoundedRect:CGRectInset(rect, 0.5, 0.5) radius:7];
    
    
    //gradient
    CGContextSaveGState(context);
    //UIColor *ucolor1=[UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:241.0/255 alpha:1];
    UIColor *ucolor1=[UIColor colorWithRed:241.0/255.0 green:241.0/255.0 blue:243.0/255 alpha:1];
    //UIColor *ucolor2=[UIColor colorWithRed:211.0/255.0 green:211.0/255.0 blue:217.0/255 alpha:1];
    UIColor *ucolor2=[UIColor colorWithRed:223.0/255.0 green:223.0/255.0 blue:231.0/255 alpha:1];
    
    if (self.state & (UIControlStateHighlighted | UIControlStateSelected)) {
        ucolor1=[UIColor colorWithRed:179.0/255.0 green:179.0/255.0 blue:187.0/255 alpha:1];
        ucolor2=[UIColor colorWithRed:130.0/255.0 green:130.0/255.0 blue:140.0/255 alpha:1];
    }
    
    CGColorRef color1=ucolor1.CGColor;
    CGColorRef color2=ucolor2.CGColor;
    
    CGGradientRef gradient;
    CGFloat locations[2] = { 0.0, 1.0 };
    NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    CGRect currentBounds = self.bounds;
    CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
    CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //overall gradient
    
    CGContextSaveGState(context);
    ucolor1=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0];
    ucolor2=[UIColor colorWithRed:0.1 green:0.1 blue:0.11 alpha:0.4];
    
    color1=ucolor1.CGColor;
    color2=ucolor2.CGColor;
    
    colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];
    
    gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);
    
    topCenter = CGPointMake(CGRectGetMidX(currentBounds), -(self.frame.origin.y));
    midCenter = CGPointMake(CGRectGetMidX(currentBounds), self.superview.bounds.size.height-(self.frame.origin.y));
    
    CGContextAddPath(context, keyBorderPath);
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    
    //bottom inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, -1), 1, [UIColor colorWithWhite:0 alpha:0.5].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //top inner shadow
    CGContextSaveGState(context);
    
    CGContextAddPath(context, keyBorderPath);
    CGContextEOClip(context);
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 0, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextAddRect(context, CGRectInset(rect, -5, -5));
    CGContextAddPath(context, keyBorderPath);
    CGContextEOFillPath(context);
    
    CGContextRestoreGState(context);
    
    //border
    CGContextSetLineWidth(context, 1);
    [[UIColor colorWithWhite:0.0 alpha:0.4] setStroke];
    CGContextAddPath(context, keyBorderPath);
    CGContextStrokePath(context);
    

    CGPathRelease(keyBorderPath);
    
    CGContextSetShadowWithColor(context, CGSizeMake(0, 1), 1, [UIColor colorWithWhite:1 alpha:1].CGColor);
    CGContextTranslateCTM(context, 0, rect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    if (_text) {
        //draw text
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        
        //    create font
        CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 26, NULL);
        
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGRect boundingBox = CTFontGetBoundingBox(font);
        
        //Get the position on the y axis
        float midHeight = rect.size.height / 2;
        midHeight -= boundingBox.size.height / 2;
        
        CGPathAddRect(path, NULL, CGRectMake(0, midHeight, rect.size.width, boundingBox.size.height));
        //CGPathAddRect(path, NULL, rect);
        
        // Initialize an attributed string.
        CFStringRef string = (__bridge_retained CFStringRef)self.text;//CFSTR("A");
        CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
        CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), string);
        
        //    create paragraph style and assign text alignment to it
        CTTextAlignment alignment = kCTCenterTextAlignment;
        CTParagraphStyleSetting _settings[] = {    {kCTParagraphStyleSpecifierAlignment, sizeof(alignment), &alignment} };
        CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(_settings, sizeof(_settings) / sizeof(_settings[0]));
        
        //    set paragraph style attribute
        CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTParagraphStyleAttributeName, paragraphStyle);
        
        //    set font attribute
        CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, font);
        if (self.state & UIControlStateDisabled) {
            CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTForegroundColorAttributeName, [UIColor colorWithWhite:0.5 alpha:1].CGColor);
        }
        
        //    release paragraph style and font
        CFRelease(paragraphStyle);
        CFRelease(font);
        
        // Create the framesetter with the attributed string.
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
        CFRelease(attrString);
        
        // Create the frame and draw it into the graphics context
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
                                                    CFRangeMake(0, 0), path, NULL);
        CFRelease(framesetter);
        CTFrameDraw(frame, context);
        CFRelease(frame);
    }else if(_image){
        // use image as mask to draw the key symbol
        
        CGContextSaveGState(context);
        
        CGImageRef maskRef = _image.CGImage; 
        
        CGImageRef mask = CGImageMaskCreate(CGImageGetWidth(maskRef),
                                            CGImageGetHeight(maskRef),
                                            CGImageGetBitsPerComponent(maskRef),
                                            CGImageGetBitsPerPixel(maskRef),
                                            CGImageGetBytesPerRow(maskRef),
                                            CGImageGetDataProvider(maskRef), NULL, false);
        
        CGRect maskRect=CGRectMake(0, 0, _image.size.width, _image.size.height);
        maskRect=CGRectOffset(maskRect, (rect.size.width-maskRect.size.width)/2.0, (rect.size.height-maskRect.size.height)/2.0);
        maskRect=CGRectIntegral(maskRect);
        CGRect shadowMaskRect=CGRectOffset(maskRect, 0, -0.5);
        
        CGContextClipToMask(context, shadowMaskRect, mask);
        
        [[UIColor colorWithWhite:1 alpha:0.8] setFill];
        
        CGContextFillRect(context, maskRect);
        
        CGContextRestoreGState(context);
        
        CGContextClipToMask(context, maskRect, mask);
        
        if (self.state & UIControlStateDisabled) {
            [[UIColor colorWithWhite:0.5 alpha:1] setFill];
        }else{
            [[UIColor blackColor] setFill];
        }
        
        
        CGContextFillRect(context, maskRect);
        
        CGImageRelease(mask);
    }
    
    
    
    CGContextRestoreGState(context);
}

Et voilà, this is what we get:


Maybe gray tones need a bit of adjustment, but really cool. There is only one thing left: play the system click sound every time a key is pressed.

Easy; from 'Text, Web, and Editing Programming Guide for iOS': "Starting in iOS 4.2, you can play standard system keyboard clicks when a user taps in your custom input views and keyboard accessory views. First, adopt the UIInputViewAudioFeedback protocol in your input view. Then, call the playInputClick method when responding to a key tap in the view."

In our example, we will place input clicks in our JACustomKeyboard class:

@interface JACustomKeyboard : UIView 

@end

@implementation JACustomKeyboard

-(void)processKeyStroke:(id)sender{
    [[UIDevice currentDevice] playInputClick];
}

#pragma mark UIInputViewAudioFeedback

- (BOOL) enableInputClicksWhenVisible {
    return YES;
}

@end

For every custom key in your keyboard add the custom keyboard as target with processKeyStroke: selector for UIControlEventTouchUpInside.

Update

As KKL noted, the code for -(CGRect) newPathForRoundedRect:(CGRect) rect radius:(CGFloat)radius selector is missing. This is it:

-(CGRect) newPathForRoundedRect:(CGRect) rect radius:(CGFloat)radius{
 CGMutablePathRef retPath = CGPathCreateMutable();
    
 CGRect innerRect = CGRectInset(rect, radius, radius);
    
 CGFloat inside_right = innerRect.origin.x + innerRect.size.width;
 CGFloat outside_right = rect.origin.x + rect.size.width;
 CGFloat inside_bottom = innerRect.origin.y + innerRect.size.height;
 CGFloat outside_bottom = rect.origin.y + rect.size.height;
    
 CGFloat inside_top = innerRect.origin.y;
 CGFloat outside_top = rect.origin.y;
 CGFloat outside_left = rect.origin.x;
    
 CGPathMoveToPoint(retPath, NULL, innerRect.origin.x, outside_top);
    
 CGPathAddLineToPoint(retPath, NULL, inside_right, outside_top);
 CGPathAddArcToPoint(retPath, NULL, outside_right, outside_top, outside_right, inside_top, radius);
 CGPathAddLineToPoint(retPath, NULL, outside_right, inside_bottom);
 CGPathAddArcToPoint(retPath, NULL,  outside_right, outside_bottom, inside_right, outside_bottom, radius);
    
 CGPathAddLineToPoint(retPath, NULL, innerRect.origin.x, outside_bottom);
 CGPathAddArcToPoint(retPath, NULL,  outside_left, outside_bottom, outside_left, inside_bottom, radius);
 CGPathAddLineToPoint(retPath, NULL, outside_left, inside_top);
 CGPathAddArcToPoint(retPath, NULL,  outside_left, outside_top, innerRect.origin.x, outside_top, radius);
    
 CGPathCloseSubpath(retPath);
    
 return retPath;
}

martes, 31 de enero de 2012

Nice emboss effect using Core Graphics

I'm working on an enterprise data visualization app. Most of the data is shown using circles whose attributes change according to the data they represent, with varying color and size.
The graphic designer made a great work giving those circles nice adornments that make them look nicer than a simple plain color circle. One of that adornments is the Photoshop "bevel and emboss" layer style as shown here
The easy way in the short term, but painful in the long run, is to use a transparent PNG with the adornments and resize accordingly. But ask yourself: what if Apple releases a Retina Display iPad? Not sure when it will happen, but they will do it.
One way to go may be CoreImage, buy it seems like using a sledgehammer to crack a nut. Core Graphics looks like a better tool for this.
Basically, we have four different adornments: a color gradient, the black shadow, the shine and a small border. Gradient and border are trivial, but shadow and shine may not.

Black shadow
Shadows are not difficult to get using CoreGraphics. Simply configure your CGContext using CGContextSetShadow() and fill a path to obtain an outer shadow for your filled path. But here we need an inner shadow, and there is no CoreGraphics to get it. How to draw an inner shadow with CoreGraphics? Simply make an "inverse" of your path using Even-Odd path compositing APIs with the help of an outer CGRect. Once you have your "inverse" path, configure shadow and fill your path; to avoid the outer rect being filled, simply clip the context to the original circle.

Shine
Shine is easier. Simply realize that it's a white outer shadow of a smaller circle. By default, CoreGraphics shadows are black, but you can change shadow color with CGContextSetShadowWithColor(). Don't forget to make Even-Odd clipping to avoid the circle dropping the shadow from being draw.

Code
And finally: this is our code and an example output:

- (void)drawRect:(CGRect)rect
{
// Drawing code
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(currentContext);

[[UIColor clearColor] setFill];
CGContextFillRect(currentContext, rect);

CGFloat diameter=MIN(rect.size.height, rect.size.width);
CGFloat borderWidth=1;
CGMutablePathRef circle=CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(rect), CGRectGetMidY(rect), (diameter/2.0)-borderWidth, M_PI, -M_PI, NO);
//percentValue goes from 0 to 1 and defines the circle main color from red (0) to green (1)
CGColorRef color1=[UIColor colorWithHue:_percentValue*(1.0/3.0) saturation:0.9 brightness:0.8 alpha:1].CGColor;
CGColorRef color2=[UIColor colorWithHue:_percentValue*(1.0/3.0) saturation:0.7 brightness:0.6 alpha:1].CGColor;
CGGradientRef gradient;
CGFloat locations[2] = { 0.0, 1.0 };
NSArray *colors = [NSArray arrayWithObjects:(__bridge id)color1, (__bridge id)color2, nil];

gradient = CGGradientCreateWithColors(NULL, (__bridge CFArrayRef)colors, locations);

CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMaxY(currentBounds));
//fill the circle with gradient
CGContextAddPath(currentContext, circle);
CGContextSaveGState(currentContext);
CGContextClip(currentContext);
CGContextDrawLinearGradient(currentContext, gradient, topCenter, midCenter, 0);

//inner shadow to simulate emboss
CGMutablePathRef innerShadowPath=CGPathCreateMutable();
CGPathAddRect(innerShadowPath, NULL, CGRectInset(rect, -100, -100));
CGPathAddEllipseInRect(innerShadowPath, NULL, CGRectInset(rect, borderWidth-1, borderWidth-1));
CGContextSetShadow(currentContext, CGSizeMake(-4, -4), 3);
[[UIColor whiteColor] setFill];
CGContextAddPath(currentContext, innerShadowPath);
CGContextEOFillPath(currentContext);
CGPathRelease(innerShadowPath);

// white shine
CGMutablePathRef whiteShinePath=CGPathCreateMutable();
CGPathAddEllipseInRect(whiteShinePath, NULL, CGRectInset(rect, borderWidth+5, borderWidth+5));
CGContextSetShadowWithColor(currentContext, CGSizeMake(-3, -3), 2, [UIColor colorWithWhite:1 alpha:0.4].CGColor);

CGMutablePathRef innerClippingPath=CGPathCreateMutable();
CGPathAddRect(innerClippingPath, NULL, CGRectInset(rect, -100, -100));
CGPathAddEllipseInRect(innerClippingPath, NULL, CGRectInset(rect, borderWidth+4, borderWidth+4));
CGContextAddPath(currentContext, innerClippingPath);
CGContextEOClip(currentContext);

CGContextAddPath(currentContext, whiteShinePath);
CGContextFillPath(currentContext);
CGPathRelease(innerClippingPath);
CGPathRelease(whiteShinePath);
CGMutablePathRef circleBorder=CGPathCreateMutable();
CGPathAddArc(circleBorder, NULL, CGRectGetMidX(rect), CGRectGetMidY(rect), (diameter-(borderWidth*2))/2.0, M_PI, -M_PI, NO);
[[UIColor colorWithWhite:0.2 alpha:1] setStroke];
CGContextSetLineWidth(currentContext, borderWidth);
CGContextAddPath(currentContext, circleBorder);
CGContextStrokePath(currentContext);
CGPathRelease(circleBorder);
CGContextRestoreGState(currentContext);

CGGradientRelease(gradient);
CFRelease(circle);
}

And the result, circles with different sizes and colors and a Core Graphics generated emboss effect:
Note that the main outer shadow is not generated with the published Core Graphics code. It's a CoreAnimation shadow.