Aug 22, 2013

Custom iOS Components: Scaling Background Images

Overview

When developing custom components like buttons or tabs, there's often a need to have those components in variety of sizes for different views/purposes.

For example, one view might have two tabs and another one might have three tabs. Even though basic tab background is the same, it will often have borders or shadows at the sides which can't be stretched. That's why many developers come up with most common solutions:

a) Saving the same background in all required sizes
b) Saving background picture by parts (top/bottom/middle or left/right/middle) with further combining

Apple has actually very elegant single line solution for this problem. Let's say we are developing custom alert view and our background looks like this :


Red lines mark the area which can't be stretched due to color variations and rounded corners (40 pixels at the top and 25 pixels at left, right and bottom), but everything else inside is basically single solid color which can be resized as wanted.

Basically you just need to "tell" iOS which parts of picture should not be stretched. Let's assume image name is "alert_bg.png".

For iOS 5 and up the following method does the trick:

AlertBg = [[UIImage imageNamed@"alert_bg.png"resizableImageWithCapInsets:UIEdgeInsetsMake(40.025.025.025.0)];


For earlier iOS versions you should use slightly different version:

AlertBg = [[UIImage imageNamed@"alert_bg.png"stretchableImageWithLeftCapWidth25 topCapHeight40];



And combined universal solution:


if ([AlertBg respondsToSelector:@selector(resizableImageWithCapInsets:)])
    AlertBg = [[UIImage imageNamed@"alert_bg.png"resizableImageWithCapInsets:UIEdgeInsetsMake(40.025.025.025.0)];
else
    AlertBg = [[UIImage imageNamed@"alert_bg.png"stretchableImageWithLeftCapWidth25 topCapHeight40];

Summary

This approach is also great for saving space - you can store minimal size backgrounds for buttons, tabs, etc with further image expansion.

Jun 20, 2013

Custom Circular Progress View for iOS

Overview

This tutorial will show how to create simple circular progress view. Although iOS offers UIActivityIndicator, the problem is that it is indefinite, so you are not able to display percentage using it.

Here's how implemented circular progress view looks:



Of course, this is a very basic look, you can achieve more sophisticated result by adding custom graphics.

1. Setting up the project 

Open XCode and create new project using Single View Application template: 


Name the project, select "Universal" option from "Devices" menu, make sure that "Use Automatic Reference Counting" is checked, press "Next" one more time and save your project.

Now create new file (shortcut CMD+N) which inherits from UIView and name it CustomProgressView. 

Also include QuartzCore.framework to the project, it will be used for drawing purposes. 

2. Adding Functionality

Before staring to write actual code, let's think how the control works:

1. After progress view has been created, new progress value will be passed using setProgress: function (values from 0.0 to 1.0). 
2. If passed value is less than current progress value, no animation is performed
3. If passed value is greater than 1.0, program sets final value to 1.0 and performs the animation
4. If new value is passed while animation is still in progress, this value is stored and used to perform new animation after current animation is finished
5. After value reaches 1.0 and appropriate animation is finished, delegate is informed using didFinishAnimation: method.

Setting up CustomProgressView.h

  • <QuartzCore/QuartzCore.h> should be included for drawing purposes
  • CustomProgressViewDelegate should be created so that delegate could be informed when progress view had reached 100% 
  • current_value is going to store current progress value (from 0.0 to 1.0)
  • new_to_value variable will be used if currently there's animation in progress (in this case new animation will be performed after current animation is finished using new_to_value as final progress animation value)
  • ProgressLbl is used to display numeric value of progress (from 0% to 100%)
  • IsAnimationInProgress is boolean value indicating if there's animation running at the moment
CustomProgressView.h file should look like this:


#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface CustomProgressView : UIView
{
    float current_value;
    float new_to_value;
    
    UILabel *ProgressLbl;

    id delegate;
    
    BOOL IsAnimationInProgress;
}

@property id delegate;
@property float current_value;

- (id)init;
- (void)setProgress:(NSNumber*)value;

@end

@protocol CustomProgressViewDelegate
- (void)didFinishAnimation:(CustomProgressView*)progressView;
@end

Setting up CustomProgressView.m

1. Initialization

init method performs a few tasks:

  • Setting the frame - since CAShapeLayer will be used to display progress, autorotation mask can't be used to center objects. That's why view is created using the larger side of a screen for both width and height and centered manually (view is bigger than actual screen). When rotating, it doesn't change its size thus all objects remain centered.
  • Initializing global variables
  • Setting transparent background for the view
  • Creating label used for displaying progress view
Here's the final code:

- (id)init
{
    CGRect frame;
    if ([[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeLeft || [[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeRight)
        frame = CGRectMake(0.0, ([[UIScreen mainScreen] bounds].size.width-[[UIScreen mainScreen] bounds].size.height)/2, [[UIScreen mainScreen] bounds].size.height, [[UIScreen mainScreen] bounds].size.height);
    else
        frame = CGRectMake(([[UIScreen mainScreen] bounds].size.width-[[UIScreen mainScreen] bounds].size.height)/2, 0.0, [[UIScreen mainScreen] bounds].size.height, [[UIScreen mainScreen] bounds].size.height);

    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
        current_value = 0.0;
        new_to_value = 0.0;
        IsAnimationInProgress = NO;
        
        self.alpha = 0.95;
        self.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.6];
        self.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
        
        ProgressLbl = [[UILabel alloc] initWithFrame:CGRectMake((self.frame.size.width-200)/2, self.frame.size.height/2-150, 200, 40.0)];
        ProgressLbl.font = [UIFont boldSystemFontOfSize:24.0];
        ProgressLbl.text = @"0%";
        ProgressLbl.backgroundColor = [UIColor clearColor];
        ProgressLbl.textColor = [UIColor whiteColor];
        ProgressLbl.textAlignment = NSTextAlignmentCenter ;
        ProgressLbl.alpha = self.alpha;
        [self addSubview:ProgressLbl];
    }
    return self;
}

2. Setting new label 's value:

-(void)UpdateLabelsWithValue:(NSString*)value
{
    ProgressLbl.text = value;
}

3. Updating the label 's value

We don't want the control to jump from 0 to 50%, instead update function should be called to gradually change label's value during the animation time, passed as the parameter:


-(void)setProgressValue:(float)to_value withAnimationTime:(float)animation_time
{
    float timer = 0;
    
    float step = 0.1;
    
    float value_step = (to_value-self.current_value)*step/animation_time;
    int final_value = self.current_value*100;
    
    while (timer<animation_time-step) {
        final_value += floor(value_step*100);
        [self performSelector:@selector(UpdateLabelsWithValue:) withObject:[NSString stringWithFormat:@"%i%%", final_value] afterDelay:timer];
        timer += step;
    }
    
    [self performSelector:@selector(UpdateLabelsWithValue:) withObject:[NSString stringWithFormat:@"%.0f%%", to_value*100] afterDelay:animation_time];
}

4. Running new progress animation in case there was progress value change during current animation:

-(void)SetAnimationDone
{
    IsAnimationInProgress = NO;
    if (new_to_value>self.current_value)
        [self setProgress:[NSNumber numberWithFloat:new_to_value]];
}


5. Performing animation itself:

Note, that animation time is proportional to value change (for example, time for 2% change would be 10 times less than for 20% change). This function also calls setProgressValue: function to gradually change progress label value.

After animation is finished, SetAnimationDone function checks if there was value change during animation.

When final progress value is 1.0 and animation is finished, delegate is informed using didFinishAnimation: function. 

Actual circle drawing code is used from here with slight modifications. 

- (void)setProgress:(NSNumber*)value{
    
    float to_value = [value floatValue];
    
    if (to_value<=self.current_value)
        return;
    else if (to_value>1.0)
        to_value = 1.0;
    
    if (IsAnimationInProgress)
    {
        new_to_value = to_value;
        return;
    }
    
    IsAnimationInProgress = YES;
    
    float animation_time = to_value-self.current_value;
    
    [self performSelector:@selector(SetAnimationDone) withObject:Nil afterDelay:animation_time];
    
    if (to_value == 1.0 && delegate && [delegate respondsToSelector:@selector(didFinishAnimation:)])
        [delegate performSelector:@selector(didFinishAnimation:) withObject:self afterDelay:animation_time];
    
    [self setProgressValue:to_value withAnimationTime:animation_time];
    
    float start_angle = 2*M_PI*self.current_value-M_PI_2;
    float end_angle = 2*M_PI*to_value-M_PI_2;
    
    float radius = 75.0;
    
    CAShapeLayer *circle = [CAShapeLayer layer];

    // Make a circular shape
    
    circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2,self.frame.size.height/2)
                                                 radius:radius startAngle:start_angle endAngle:end_angle clockwise:YES].CGPath;
    
    // Configure the apperence of the circle
    circle.fillColor = [UIColor clearColor].CGColor;
    circle.strokeColor = [UIColor whiteColor].CGColor;
    circle.lineWidth = 3;
    
    // Add to parent layer
    [self.layer addSublayer:circle];
    
    // Configure animation
    CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    
    drawAnimation.duration            = animation_time;
    drawAnimation.repeatCount         = 0.0// Animate only once..
    drawAnimation.removedOnCompletion = NO;   // Remain stroked after the animation..
    
    // Animate from no part of the stroke being drawn to the entire stroke being drawn
    drawAnimation.fromValue = [NSNumber numberWithFloat:0.0];
    drawAnimation.toValue   = [NSNumber numberWithFloat:1.0];
    
    // Experiment with timing to get the appearence to look the way you want
    drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    
    // Add the animation to the circle
    [circle addAnimation:drawAnimation forKey:@"drawCircleAnimation"];
    self.current_value = to_value;
}

Setting up ViewController.h

We just need to include CustomProgressView.h and set view controller to be its delegate:

#import <UIKit/UIKit.h>
#import "CustomProgressView.h"
@interface mkViewController : UIViewController <CustomProgressViewDelegate>
{
    CustomProgressView *customProgressView;
}
@end

Setting up ViewController.m

Let's create a button that should be clicked to show progress view:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIButton *ProgressBtn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    ProgressBtn.frame = CGRectMake((int)((self.view.frame.size.width-200.0)/2.0), 120.0, 200.0, 50.0);
    [ProgressBtn setTitle:@"Show Progress View" forState:UIControlStateNormal];
    [ProgressBtn addTarget:self action:@selector(onProgressBtnPressed) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:ProgressBtn];
    ProgressBtn.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
}

When button is clicked, onProgressBtnPressed will change progress value a few times with delay for demonstrating purposes:


- (void)onProgressBtnPressed
{
    customProgressView = [[CustomProgressView alloc] init];
    customProgressView.delegate = self;
    [self.view addSubview:customProgressView];
    
    [self performSelector:@selector(setProgress:) withObject:[NSNumber numberWithFloat:0.3] afterDelay:0.0];
    [self performSelector:@selector(setProgress:) withObject:[NSNumber numberWithFloat:0.75] afterDelay:2.0];
    [self performSelector:@selector(setProgress:) withObject:[NSNumber numberWithFloat:1.0] afterDelay:4.0];
}

Please note that redrawing should be performed in the main thread:


-(void)setProgress:(NSNumber*)value
{
    [customProgressView performSelectorOnMainThread:@selector(setProgress:) withObject:value waitUntilDone:NO];
}

That's it, you can download source code here.

May 2, 2013

UIScrollView With Page Numbers (UIPageControl-like)


Overview

When using UIScrollView in paging mode, it is quite common to incorporate it with UIPageControl for simpler navigation. This tutorial shows how to create completely custom page control with page numbers which works very similar to UIPageControl

Here's how final result looks like:

1. Setting up the project 

Open XCode and create new project using Single View Application template: 


Name the project, select "iPhone" option from "Devices" menu, make sure that "Use Automatic Reference Counting" is checked, press "Next" one more time and save your project.

2. Adding graphics

In order to create custom page control, you will need two pictures which would be used for disabled/enabled page background similar to these:


(note that @2x version of the files should be added for Retina display support)


3. Adding functionality 

Setting up ViewController.h

We will need to create scroll view and an array to store page control elements. Also we will need to access UIScrollView's delegate method, so here's how file should look like:


#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UIScrollViewDelegate>
{
    UIScrollView* scrollView;

    NSMutableArray *PageControlBtns;
}
@end



Setting up ViewController.m

First, let's define pages number at the top of the file:


#define PAGES_NUMBER 6


Next, let's modify viewDidLoad function by adding scroll view and calling GeneratePages function which is going to generate page control:


- (void)viewDidLoad
{
    [super viewDidLoad];
    
    scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(50.0, 50.0, self.view.frame.size.width-100.0, self.view.frame.size.width-100.0)];
    scrollView.pagingEnabled = YES;
    scrollView.showsHorizontalScrollIndicator = NO;
    scrollView.delegate = self;
    scrollView.backgroundColor = [UIColor clearColor];
    [self.view addSubview:scrollView];
    
    [self GeneratePages];
}


Now we are going to actually generate pages in loop, which includes following:

  • Creating UIView (Page) with label which displays current page number, and adding this page to scroll view
  • Creating UIButton with page number label which represents page control element, adding this button to view, and storing it in the array. Each button has appropriate tag, so that when onPageControlBtnPressed: is called, we will know exactly which button was pressed.
Here's function implementation:

-(void)GeneratePages
{
    UIImage *PageControlImg = [UIImage imageNamed:@"page_deselected.png"];
    int x_coord = (self.view.frame.size.width-PageControlImg.size.width*PAGES_NUMBER)/2.0;
    
    PageControlBtns = [[NSMutableArray alloc] init];
    
    for (int i=0; i<PAGES_NUMBER; i++)
    {
        
        UIView *Page = [[UIView alloc] initWithFrame:CGRectMake(scrollView.frame.size.width*i, 0.0, scrollView.frame.size.width, scrollView.frame.size.height)];
        Page.backgroundColor = [UIColor colorWithRed:0.32 green:0.55 blue:0.88 alpha:1.0];
        
        UILabel *titleLbl = [[UILabel alloc] initWithFrame:CGRectMake(10.0, 10.0, Page.frame.size.width-20.0, 40.0)];
        titleLbl.backgroundColor = [UIColor clearColor];
        titleLbl.textColor = [UIColor whiteColor];
        titleLbl.font = [UIFont boldSystemFontOfSize:16.0];
        titleLbl.text = [NSString stringWithFormat:@"This is page number %d", i+1];
        titleLbl.textAlignment = UITextAlignmentCenter;
        [Page addSubview:titleLbl];
        
        [scrollView addSubview:Page];
        
        UIButton *PageControlBtn = [[UIButton alloc] initWithFrame:CGRectMake(x_coord+i*PageControlImg.size.width, scrollView.frame.origin.y+scrollView.frame.size.height+20.0, PageControlImg.size.width, PageControlImg.size.height)];
        [PageControlBtn setBackgroundImage:[UIImage imageNamed: (i==0)?@"page_selected.png":@"page_deselected.png"] forState:UIControlStateNormal];
        [PageControlBtn setBackgroundImage:[UIImage imageNamed: @"page_selected.png"] forState:UIControlStateHighlighted];
        [PageControlBtn setBackgroundImage:[UIImage imageNamed: @"page_deselected.png"] forState:UIControlStateDisabled];
        [PageControlBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
        [PageControlBtn setTitleColor:(i==0)?[UIColor whiteColor]:[UIColor grayColor] forState:UIControlStateNormal];
        PageControlBtn.titleLabel.font = [UIFont boldSystemFontOfSize:16.0];
        PageControlBtn.titleLabel.textAlignment = UITextAlignmentCenter;
        [PageControlBtn setTitle:[NSString stringWithFormat:@"%d", i+1] forState:UIControlStateNormal];
        [PageControlBtn setTitle:[NSString stringWithFormat:@"%d", i+1] forState:UIControlStateSelected];
        [PageControlBtn setTag:i+2000];
        [PageControlBtn addTarget:self action:@selector(onPageControlBtnPressed:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:PageControlBtn];
        
        [PageControlBtns addObject:PageControlBtn];
    }
    scrollView.contentSize = CGSizeMake(scrollView.frame.size.width*PAGES_NUMBER, scrollView.frame.size.height);
}

Now, when any button is pressed, we need to do two things - manually scroll UIScrollView to appropriate page and change button look (both background image and text color), which is going to be implemented in a separate function:


-(void)onPageControlBtnPressed:(id)sender
{
    UIButton *button = (UIButton *)sender;
    
    int tag=button.tag-2000;
    
    [scrollView setContentOffset:CGPointMake(scrollView.frame.size.width*tag, 0) animated:YES];
    
    [self SetActivePageControlWithIndex:tag];
}

-(void)SetActivePageControlWithIndex:(int)index
{
    for (int i=0; i<[PageControlBtns count]; i++)
    {
        UIImage *backgroundImg = [[PageControlBtns objectAtIndex:i] backgroundImageForState:(i==index)?UIControlStateHighlighted:UIControlStateDisabled];
        [[PageControlBtns objectAtIndex:i] setBackgroundImage:backgroundImg forState:UIControlStateNormal];
        [[PageControlBtns objectAtIndex:i] setTitleColor:(i==index)?[UIColor whiteColor]:[UIColor grayColor] forState:UIControlStateNormal];
    }
}


By now pressing a button would change scroll view's position, but it doesn't work vise-versa. So, we should change active page control when scroll view is scrolled. To do this, we will use UIScrollView's delegate method:

-(void)scrollViewDidEndDecelerating:(UIScrollView *)sender
{
    CGFloat pageWidth = scrollView.frame.size.width;
    int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
    [self SetActivePageControlWithIndex:page];
}

That's it - now you get response both ways - when scroll view is scrolled and when page control button is pressed. You can download source codes here.

May 1, 2013

Customizing UIToolbar

1. Changing background of UIToolbar

a) In case you just need to change background color, use tintColor property, for example:

toolbar.tintColor = [UIColor redColor];

b) In case your app runs iOS 5 and up only, use setBackgroundImage:forToolbarPosition:barMetrics: method, for example:

[toolbar setBackgroundImage:[UIImage imageNamed:@"toolbar.png"] forToolbarPosition:UIToolbarPositionAny barMetrics:UIBarMetricsDefault];

c) In case you do need older iOS versions compatibility, you need to subclass UIToolbar and combine it with solution (b) using respondsToSelector: method. Basically you need to use setBackgroundImage:forState:barMetrics: method for iOS 5 and up, and override drawRect: method for older iOS versions:

CustomToolbar.h:

#import <UIKit/UIKit.h>

@interface CustomToolbar : UIToolbar

@end

CustomToolbar.m:

#import "CustomToolbar.h"

@implementation CustomToolbar

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}


// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    UIImage *image = [UIImage imageNamed: @"toolbar.png"];
    [image drawInRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
}


@end

Depending on your background picture selection, toolbar buttons might not look good. Here's and example:


The easiest fix would be to combine custom background with tintColor property (in this case it would be dark blue color):

toolbar.tintColor = [UIColor colorWithRed:0.0 green:0.11 blue:0.15 alpha:1.0];

Looks much better now:


If this is not enough, you might consider adding custom buttons to UIToolbar:

2. Adding custom buttons to UIToolbar

Create UIButton using any image that you want, and then create UIBarButtonItem using initWithCustomView: method. In case you want to have white glow when user touches the button similar to system bar button items, set showsTouchWhenHighlighted property to YES.

Here's some sample code:



UIButton *EmailBtn = [[UIButton alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[EmailBtn setBackgroundImage:[UIImage imageNamed:@"email.png"] forState:UIControlStateNormal];
EmailBtn.showsTouchWhenHighlighted = YES;

UIBarButtonItem *EmailBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:EmailBtn];
NSArray *buttons = [NSArray arrayWithObjects: EmailBarButtonItem, nil];
[toolbar setItems: buttons animated:NO];




And here's the final look:

Conclusion

Using the combination of custom UIToolbar background and custom bar button items gives you great possibilities for creating your own unique user experience. Enjoy!