Friday 29 April 2011

Thread Programming

Threading tutorial using NSthread in iPhone SDK (Objective-C)

This tutorial will show you how to use the NSThread in iPhone SDK to avoid loosing of the interface response.
This is good if you need to run some repetitive task on the background of the application or for some task, which may take long time to process.

Full source codes are included in the example aplication. (NSThread, detachNewThreadSelector, performSelectorOnMainThread).

Threading usually works in the same way in most programming languages. First you have the main thread, this one is created when you launch the application. Than you can create a different thread (can be started after some user interaction as a button pressed, etc, or automatically as a part of the program).

Because I am expecting that you already know how to connect elements from the interface builder to IBAction or IBOutlet, I am not going to cover this part. For more information’s please check the previous tutorials on XProgress.com about UIButton, UISwitch, UISlider or How to build the Navigation-based Application tutorial. Once you’ll finish any of these you’ll have pretty good idea "what the hell am I talking about" :)).

Let’s start with the less theoretical, and more interesting part of this tutorial. Yes, we are going to build our first multi-threaded application.

Start a new View-based application; call it "TutorialProject" to maintain the compatibility with the code in this tutorial. Open the TutorialProjectViewController.xib and set the main interface as it is on the next picture (You really need just the Thread part and the Test part). There is in both sections just one active UILabel (the one with the 0 (zero) in it). In the Tread part we have Progress view (UIProgressView) and button (UIButton).

The test part is here just because we want to verify that the interface is responding to touches while the other thread is running. It contains just that label with a value and slider (UISlider).


Now we have the user interface done ad we can move to the actual coding. Open the TutorialProjectViewController.h and create all the necessary IBOutlets for out interface.


@interface TutorialProjectViewController : UIViewController {

// ------ Tutorial code starts here ------

// Thread part
IBOutlet UILabel *threadValueLabel;
IBOutlet UIProgressView *threadProgressView;
IBOutlet UIButton *threadStartButton;

// Test part
IBOutlet UILabel *testValueLabel;

// ------ Tutorial code ends here ------

}

Now we need to create properties for our outlets (variables).

@property (nonatomic, retain) IBOutlet UILabel *threadValueLabel;
@property (nonatomic, retain) IBOutlet UIProgressView *threadProgressView;
@property (nonatomic, retain) IBOutlet UIProgressView *threadStartButton;
@property (nonatomic, retain) IBOutlet UILabel *testValueLabel;

 Next thing we need to do is to create IBAction’s. They are going to handle the events (actions) released when the button is pressed and when you’ll move the slider.

- (IBAction) startThreadButtonPressed:(UIButton *)sender;
- (IBAction) testValueSliderChanged:(UISlider *)sender;
The entire code for TutorialProjectViewController.h is here:


#import "uikit/uikit.h"

@interface TutorialProjectViewController : UIViewController {

// ------ Tutorial code starts here ------

// Thread part
IBOutlet UILabel *threadValueLabel;
IBOutlet UIProgressView *threadProgressView;
IBOutlet UIButton *threadStartButton;

// Test part
IBOutlet UILabel *testValueLabel;

// ------ Tutorial code ends here ------

}

// ------ Tutorial code starts here ------

@property (nonatomic, retain) IBOutlet UILabel *threadValueLabel;
@property (nonatomic, retain) IBOutlet UIProgressView *threadProgressView;
@property (nonatomic, retain) IBOutlet UIButton *threadStartButton;

@property (nonatomic, retain) IBOutlet UILabel *testValueLabel;


- (IBAction) startThreadButtonPressed:(UIButton *)sender;

- (IBAction) testValueSliderChanged:(UISlider *)sender;

// ------ Tutorial code ends here ------

// This function is for button which takes you to the xprogress.com website
- (IBAction) runXprogressComButton: (id) sender;

@end
Now open the TutorialProjectViewController.m file and synthesize the outlets.

@synthesize threadValueLabel, threadProgressView, testValueLabel, threadStartButton;

And after synthesizing don’t forget to release these from the memory in the dealloc function. You can find this one on the end of the file.

- (void)dealloc {

// ------ Tutorial code starts here ------

[threadValueLabel release];
[threadProgressView release];
[threadStartButton release];

[testValueLabel release];

// ------ Tutorial code ends here ------

[super dealloc];
}

Now we are going to do the actual threading. First of all we need to create a definition for our IBAction which will be called after the Start my thread button is pressed.

- (IBAction) startThreadButtonPressed:(UIButton *)sender {

threadStartButton.hidden = YES;
threadValueLabel.text = @"0";
threadProgressView.progress = 0.0;
[NSThread detachNewThreadSelector:@selector(startTheBackgroundJob) toTarget:self withObject:nil];

}

First we are hiding the button, because we don’t want users to press this button twice. On the second line we are setting the text to 0 (zero), resetting the progress bar on the third line and detaching a new thread on the fourth line, which calls the startTheBackgroundJob function.

Now we need to define the startTheBackgroundJob function.

- (void)startTheBackgroundJob {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// wait for 3 seconds before starting the thread, you don't have to do that. This is just an example how to stop the NSThread for some time
[NSThread sleepForTimeInterval:3];
[self performSelectorOnMainThread:@selector(makeMyProgressBarMoving) withObject:nil waitUntilDone:NO];
[pool release];

}
On the first line we are setting a new autorelease pool. Every time autorelease is sent to an object, it is added to the outer-most autorelease pool. When the pool is drained, it simply sends -release to all the objects in the pool.

Autorelease pools are simply a convenience that allows you to defer sending -release until "later". That "later" can happen in several places, but the most common in Cocoa GUI apps is at the end of the current run loop cycle.

The second line is here just because I wanted to show you how to pause the thread for a while, in this case, for 3 seconds. Last line is actually starting a function makeMyProgressBarMoving, and because the waitUntilDone statement is on NO, this function will be actually running on the background.

- (void)makeMyProgressBarMoving {

float actual = [threadProgressView progress];
threadValueLabel.text = [NSString stringWithFormat:@"%.2f", actual];
if (actual < 1) {
threadProgressView.progress = actual + 0.01;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(makeMyProgressBarMoving) userInfo:nil repeats:NO];
}
else
threadStartButton.hidden = NO;
}


Here we are getting the actual value of the progress bar, setting a value for the label and if the value is less than 1 (which is the maxim value for the UIProgressView, equals to 100%) we are adding 0.01 to the progress and setting a new timer (NSTimer), which will call this function again in 0.5 second. If the progress bar is in 100% (1) we are going to show the button again.

To finish our application we need to add the code, which handles UISlider and we are done.

- (IBAction) testValueSliderChanged:(UISlider *)sender {

testValueLabel.text = [NSString stringWithFormat:@"%.2f", sender.value];

}

The entire source code for the TutorialProjectViewController.m is here:

#import "TutorialProjectViewController.h"

@implementation TutorialProjectViewController

@synthesize threadValueLabel, threadProgressView, testValueLabel, threadStartButton;

// ------ Tutorial code starts here ------

- (IBAction) startThreadButtonPressed:(UIButton *)sender {

threadStartButton.hidden = YES;
threadValueLabel.text = @"0";
threadProgressView.progress = 0.0;
[NSThread detachNewThreadSelector:@selector(startTheBackgroundJob) toTarget:self withObject:nil];

}

- (void)startTheBackgroundJob {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// wait for 3 seconds before starting the thread, you don't have to do that. This is just an example how to stop the NSThread for some time
[NSThread sleepForTimeInterval:3];
[self performSelectorOnMainThread:@selector(makeMyProgressBarMoving) withObject:nil waitUntilDone:NO];
[pool release];

}

- (void)makeMyProgressBarMoving {

float actual = [threadProgressView progress];
threadValueLabel.text = [NSString stringWithFormat:@"%.2f", actual];

if (actual < 1) {
threadProgressView.progress = actual + 0.01;
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(makeMyProgressBarMoving) userInfo:nil repeats:NO];
}
else
threadStartButton.hidden = NO;
}

- (IBAction) testValueSliderChanged:(UISlider *)sender
{
testValueLabel.text = [NSString stringWithFormat:@"%.2f", sender.value];
}

// ------ Tutorial code ends here ------ /*
// The designated initializer. Override to perform setup that is required before the view is loaded.

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])
{
// Custom initialization
}
return self;
}

*/ /* // Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView { }
*/ /* // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.

- (void)viewDidLoad {
[super viewDidLoad];
}
*/ /* // Override to allow orientations other than the default portrait orientation.

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.

[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload
{
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

// This function is for button which takes you to the xprogress.com website

- (IBAction) runXprogressComButton: (id) sender
{ NSURL *url = [ [ NSURL alloc ] initWithString: @"http://www.xprogress.com/" ]; [[UIApplication sharedApplication] openURL:url];
}

- (void)dealloc 
{ // ------ Tutorial code starts here ------
[threadValueLabel release];
[threadProgressView release];
[threadStartButton release];
[testValueLabel release];
// ------ Tutorial code ends here ------
[super dealloc];
} @end

Now you need to open Interface builder and set all the IBOutlets and IBAction’s to the elements on your view. Connect threadStartButton with the Start my thread button, threadValueLabel with the 0 label above the progress bar, threadProgressView with progress bar and testValueLabel with the 0 (zero) UILabel above your testing UISlider.

Now connect Start my thread button’s Touch Up Inside action with startThreadButtonPressed., and testValueSliderChanged with the Value Changed event on your UISlider.
WooohoooOOO, the work is done. Launch the application in the simulator on the actual device, press the Start thread button and start using the UISlider to test that even if the application is running a thread on the background, you can still work with the application without loosing the respond ability in the UI.
Thanks for your time and I hope that I’ll see your next visit in my Google Analytics stats soon :))

Friday 22 April 2011

BlueTooth Programming


iPhone BlueTooth Programming

One of the neat features available in iPhone OS 3.0 is the GameKit framework. The GameKit framework contains APIs to allow communications over a Bluetooth network. Using these APIs, you can create peer-to-peer games and applications with ease. Unlike other mobile platforms, using Bluetooth as a communication channel in iPhone is way easier than expected. Hence, in this article, I will show you how to build a simple application that allows two iPhone or iPod Touch devices to communicate with each other.

All the various APIs for accessing the Bluetooth is located in the GameKit framework. Hence, you need to add this framework to your project.

In the BluetoothVC.h file, declare the following object, outlets, and actions:

#import "UIKit/UIKit.h"

#import "GameKit/GKSession.h"
#import "GameKit/GKPeerPickerController.h"

@interface BluetoothVC : UIViewController {

GKSession *currentSession;
UITextField *textField;


}

@property (nonatomic, retain) GKSession *currentSession;

-(void)setBlueToothConnection;

@end

The GKSession object is used to represent a session between two connected Bluetooth devices. You will make use of it to send and receive data between the two devices.

In the BluetoothVC.m file, add in the following statements in bold: 

#import "BluetoothVC.h"

@implementation BluetoothVC
@synthesize currentSession;

GKPeerPickerController *picker;

- (void)loadView {

UIView *mainView=[[UIView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];

mainView.backgroundColor=[UIColor grayColor];

self.view= mainView;

[mainView release];


textField= [[UITextField alloc] initWithFrame:CGRectMake(10, 8, 120, 30)];
textField.backgroundColor = [UIColor clearColor];
[textField setFont:[UIFont boldSystemFontOfSize:12]];
textField.placeholder = @"Enter First Name";
//textField.tag = kFieldTag;
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
textField.text = kEmptyText;

textField.autocorrectionType= UITextAutocorrectionTypeNo;
textField.autocapitalizationType = UITextAutocapitalizationTypeWords;
textField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;

[self.view addSubview:textField];


UIButton *mButton= [[UIButton alloc]initWithFrame:CGRectMake(5, 45, 120 , 35)];
[mButton addTarget:self action:@selector(ButtonAction:) forControlEvents:UIControlEventTouchUpInside];
//[mButton setTag:kButtonTag];
[mButton setTitle:@"send" forState:UIControlStateNormal];

[self.view addSubview:mButton];
[mButton release];

UIButton *mButton1= [[UIButton alloc]initWithFrame:CGRectMake(5, 45, 120 , 35)];
[mButton1 addTarget:self action:@selector(ButtonAction1:) forControlEvents:UIControlEventTouchUpInside];
[mButton1 setTag:1];
[mButton1 setTitle:@"Connect" forState:UIControlStateNormal];

[self.view addSubview:mButton1];
[mButton1 release];

UIButton *mButton2= [[UIButton alloc]initWithFrame:CGRectMake(5, 85, 120 , 35)];
[mButton2 addTarget:self action:@selector(ButtonAction2:) forControlEvents:UIControlEventTouchUpInside];
[mButton2 setTag:2];
[mButton2 setTitle:@"Connect" forState:UIControlStateNormal];
[mButton2 setHidden:YES];     
[self.view addSubview:mButton2];
[mButton2 release];

}

- (void)dealloc {
[super dealloc];
[textField release];
}
Here it look like this:


Button Actions perfromed Here:
Connect Funcationality
-(void)ButtonAction1:(id)sender {

picker = [[GKPeerPickerController alloc] init];
picker.delegate = self;
picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
uiButton *button1 = (uibutton *) [self.view viewWithTag:1];
uiButton *button2 = (uibutton *) [self.view viewWithTag:2];
[button1 setHidden:YES];
[button2 setHidden:NO];  
[picker show];
}

   
Send Functionality:


-(void)ButtonAction:(id) sender
{
//---convert an NSString object to NSData---
NSData* data;
NSString *str = [NSString stringWithString:textField.text];
data = [str dataUsingEncoding: NSASCIIStringEncoding];

if (currentSession)
[self.currentSession sendDataToAllPeers:data withDataMode:GKSendDataReliable
error:nil];
}


Disconnect Functionality:
-(void) ButtonAction2:(id) sender {
[self.currentSession disconnectFromAllPeers];
[self.currentSession release];
currentSession = nil;

uiButton *button1 = (uibutton *) [self.view viewWithTag:1];
uiButton *button2 = (uibutton *) [self.view viewWithTag:2];
[button1 setHidden:NO];
[button2 setHidden:YES];
}


General Functionality:

When data is received from the other device, the receiveData:fromPeer:inSession:context: method will be called. Implement this method as follows:

Note: maintain SessionID unique between devices. Otherwise device doesn't identify the other device.
- (GKSession *)peerPickerController: (GKPeerPickerController *) picker
sessionForConnectionType: (GKPeerPickerConnectionType) type {

GKSession* session = [[GKSession alloc] initWithSessionID: @"HelloWorldID"
displayName: [mAppDelegate uID]
sessionMode: GKSessionModePeer];
[session autorelease];
return session;
}

- (void)peerPickerController:(GKPeerPickerController *)picker didConnectPeer:(NSString *)peerID toSession:(GKSession *)session {
self.currentSession = session;
session.delegate = self;
[session setDataReceiveHandler:self withContext:nil];
picker.delegate = nil;

[picker dismiss];
[picker autorelease];
}

- (void)peerPickerControllerDidCancel:(GKPeerPickerController *)picker
{
picker.delegate = nil;
[picker autorelease];
}


- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state {

switch (state)
{
case GKPeerStateConnected:
NSLog(@"connected");
break;
case GKPeerStateDisconnected:
NSLog(@"disconnected");
[self.currentSession release];
currentSession = nil;
break;

default:
break;
}
}

- (void) receiveData:(NSData *)data
fromPeer:(NSString *)peer
inSession:(GKSession *)session
context:(void *)context {

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
NSDictionary *myDictionary = [[unarchiver decodeObjectForKey:@"points"] retain];
[unarchiver finishDecoding];
[unarchiver release];

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Data received"
message:[myDictionary valueForKey:@"eventid"]
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}

    

Here, the received data is in the NSData format. To display it using the UIAlertView class, you need to convert it to an NSString object.

 

Thursday 14 April 2011

Plist

Create Plist Programmatically 
Create Plist:
            Here I am creating plist which contains array objects.
-(void)customInitialization
{
NSFileManager *fileManager = [NSFileManager defaultManager];

BOOL success;

NSArray *paths= NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentDirectory = [paths objectAtIndex:0];

NSString *plistDirectory = [NSString stringWithFormat:@"%@/Enterprise",documentDirectory];

if ([[NSFileManager defaultManager] createDirectoryAtPath:plistDirectory withIntermediateDirectories:YES attributes:nil error:nil]) {
mWritablePath = [plistDirectory stringByAppendingPathComponent:@"Downloads.plist"];
}

success = [fileManager fileExistsAtPath:mWritablePath];
//check if plist already exist
if(success)
{

mDownloadsArray = [[NSMutableArray alloc] initWithContentsOfFile:mWritablePath];

return;
}


NSString *defaultPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Downloads.plist"];

success = [fileManager copyItemAtPath:defaultPath toPath:mWritablePath error:nil];

if(!success)
NSLog(@"Failed to create DB");
else
NSLog(@"Created editable copy of DB");

mDownloadsArray = [[NSMutableArray alloc] initWithContentsOfFile:mWritablePath];
}

Add/Update the values in Plist:

-(void)setDownloadCount:(NSString *)inCount
{
//store count in temp array
if([inCount intValue] > 0)
{
//copy the contains main array into temp aray

NSMutableArray *mTempArray = [[NSMutableArray alloc] initWithArray:mDownloadsArray];

//get the updated value into dict

NSMutableDictionary *mtempDict = [[NSMutableDictionary alloc]initWithDictionary:[mTempArray objectAtIndex:mRow]];

[mtempDict setObject:inCount forKey:kDownloadCount];

[mTempArray removeObjectAtIndex:mRow];

[mTempArray insertObject:mtempDict atIndex:mRow];


mDownloadsArray = [mTempArray retain];

[mtempDict release];
[mTempArray release];

//[DownloadClubTableView reloadData];

}
//write the data into plist
if([inCount intValue] > 0)
{
NSArray *paths= NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentDirectory = [paths objectAtIndex:0];

NSString *plistDirectory = [NSString stringWithFormat:@"%@/Enterprise",documentDirectory];

NSString *mPath = [plistDirectory stringByAppendingPathComponent:@"Downloads.plist"];

[mDownloadsArray writeToFile:mPath atomically:YES];
}
}

Read the Values from Plist:

NSString *DownloadsPlistPath =[[NSBundle mainBundle] pathForResource:@"Downloads" ofType:@"plist"];

mDownloadsArray =[[NSMutableArray alloc] initWithContentsOfFile:DownloadsPlistPath];

Remove the values from Plist:

-(void)removeDownloadCount:(int)iRow
{

if([inCount intValue] > 0)
{
NSMutableArray *mTempArray = [[NSMutableArray alloc] initWithArray:mDownloadsArray];
[mTempArray removeObjectAtIndex:mRow];
mMillionDownloadsArray = [mTempArray retain];
[mTempArray release];

//[DownloadClubTableView reloadData];

}

if([inCount intValue] > 0)
{
NSArray *paths= NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentDirectory = [paths objectAtIndex:0];

NSString *plistDirectory = [NSString stringWithFormat:@"%@/SBEnterprise",documentDirectory];

NSString *mPath = [plistDirectory stringByAppendingPathComponent:@"MillionDownloads.plist"];

[mMillionDownloadsArray writeToFile:mPath atomically:YES];
}

}

Remove Plist from Directory:

NSString *DownloadsPlistPath =[[NSBundle mainBundle] pathForResource:@"Downloads" ofType:@"plist"];
[[NSFileManager defaultManager] removeItemAtPath:DownloadsPlistPath error:NULL];

Wednesday 13 April 2011

DataPicker

Display Datepicker SingleComponentpicker and Doublecomponentpicker with each of the tabbar in iPhone

App will be this way:


We will see how to display Datepicker, SingleComponent picker and Doublecomponent picker using the Tab Bar controller.
Step 1: Create aTab bar Application using template . Name the project  ”TabBarWithPicker”.

Step 2: Xcode automatically creates the directory structure and adds essential frameworks to it. You can explore the directory structure to check out the content of the directory.

Step 3: Expand classes and notice Interface Builder created the FirstViewController class for you. Expand Resources and notice the template generated a separate nib, SecondView.xib, for the TabBarWithPicker.

Step 4: Now we’ll add UIViewController class to the project. Choose New file -> Select cocoa touch classes group and then select UIViewController . Give the name of the class SingleComponentPickerViewController. Create one more UIViewController class file and corresponding .xib file ,give the name DoubleComponentPickerViewController.

Step 5: In the FirstViewController.h file we added UIDatePicker class that implements an object for multiple rotating wheels to allow users to select date and a method an IBAction. make the following changes:


@interface FirstViewController : UIViewController <UIPickerViewDataSource , UIPickerViewDelegate>{
  IBOutlet UIDatePicker *datePicker;
}
  @property (nonatomic , retain) UIDatePicker *datePicker;
  -(IBAction)buttonPressed;

Step 6: Now double click your MainWindow.xib file and open it to the Interface Builder. Open the TabBar Controller and drag the Tab Bar Item from the library and place it below of the Tab Bar(See the figure below). Now dragDate Picker from the library and place it to the view window, and also drag Round Rect from the library and place it to the below of the Date Picker. Select DatePicker from the tab bar and bring up Connection Inspector and now drag from the datePicker to the picker. Select Round Rect button and bring up Connection Inspector then drag from TouchUpInside to the File’s Owner icon, select buttonPressed: action. Now save the MainWindow.xib file and go back to the Xcode.

 
Step 7: In the FirstViewController.m make the following changes:

-(IBAction)buttonPressed{
  NSDate *selected = [datePicker date];
  NSString *message = [[NSString alloc] initWithFormat:@"The date and time is %@",selected];
  UIAlertView *alert = [[UIAlertView alloc]
  initWithTitle:@"Date and Time selected" message:message delegate:nil   cancelButtonTitle:@"YES" otherButtonTitles:nil];   [alert show];
  [alert release];
  [message release];
}
  - (void)viewDidLoad {
  [super viewDidLoad];
  NSDate *now = [[NSDate alloc] init];
  [datePicker setDate:now animated:YES];
  [now release];
}

Step 8: Single click the SingleComponentPickerViewController.h file in the classes folder and open it to the Xcode.This controller class will act as a both DataSource and Delegate for its Picker. So we need to implement datasource and delegate protocol. Also need to declare an IBOutlet and an Action .Add the following code in the file:

@interface SingleComponentPickerViewController : UIViewController
  <UIPickerViewDataSource , UIPickerViewDelegate>
 {
   IBOutlet UIPickerView *singlePicker;
   NSArray *pickerData;
}
  @property(nonatomic , retain) UIPickerView *singlePicker;
  @property(nonatomic , retain) NSArray *pickerData;   -(IBAction)buttonPressed1;

Step 9: Open the SecondView.xib file in the Interface Builder. Double click your view icon open the view window, first drag the Picker view from the library and place it to the view window (See the figure below). Select the picker and bring up connection inspector, you will see the first two items are Datasource and Delegate. Drag from the circle next to DataSource to File’s Owner.Then drag again from the circle next to Delegate to the File’s Owner. Next drag Round Rect button from the library and place it to the view window and give the title Select. Now select the Select button and bring up connection inspector and drag from the Touch Up Inside to the File’s Owner and select buttonPressed1: action. Save the nib file,close it and go back to the Xcode.
 
Step 10: Now open the MainWindow.xib file in the Interface Builder. Load the secondView.xib file in the MainWindow.xib . Select the SingleCompoPicker from the tab bar and bring up Attribute inspector change the NIB name into SecondView and bring up Identity Inspector change the class name into SingleComponentPickerViewController. Save the nib file , close it and go back to the Xcode.

Step 11: Open the SingleComponentPickerViewController.m file and make the following changes:

-(IBAction)buttonPressed1
  {
    NSInteger row = [singlePicker selectedRowInComponent:0];
    NSString *selected = [pickerData objectAtIndex:row];
    NSString *title = [[NSString alloc] initWithFormat:
    @"you selected %@!", selected];
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
    message : @"Thank you for choosing."
    delegate:nil
    cancelButtonTitle :@"Welcome"
    otherButtonTitles :nil];
    [alert show];
    [alert release];
    [title release];
}   - (void)viewDidLoad {
    NSArray *array = [[NSArray alloc]     initWithObjects:@"iPhone",@"iPad",@"iPod",@"iMac",@"Mac",
   @"iBook",@"Safari",nil];
    self.pickerData = array;
    [array release];
    [super viewDidLoad];
}

Step 12: Open the DoubleComponentPickerViewController.h, we’ll define here a picker with two components or wheels,each wheel will be independent of the other wheel. Define two constants that will represent the two components. Components are assigned numbers and declare DataSource and Delegate for its Picker. Also need to declare an IBOutlet and an Action .Now make the following changes in the file:

#define kFillingComponent  0
   #define kBreadComponent  1   @interface DoubleComponentPickerViewController : UIViewController
  {
   IBOutlet UIPickerView *doublePicker;
   NSArray *fillingTypes;
   NSArray *breadTypes;
}
  @property (nonatomic,retain)  UIPickerView *doublePicker;
  @property (nonatomic,retain)  NSArray *fillingTypes;
  @property (nonatomic,retain)  NSArray *breadTypes;
  -(IBAction)buttonPressed2;

Step 13: Open the DoubleComponentPickerViewController.xib file in the Interface Builder. Double click your view icon open the view window, first drag the Picker view from the library and place it to the view window. Select the picker and bring up connection inspector, you will see the first two items are Datasource and Delegate. Drag from the circle next to DataSource to File’s Owner.Then drag again from the circle next to Delegate to the File’s Owner. Next drag Round Rect button from the library and place it to the view window and give the title Select. Now select the Select button and bring up connection inspector and drag from the Touch Up Inside to the File’s Owner and select buttonPressed2: action. Save the nib file,close it and go back to the Xcode.

Step 14: Now open the MainWindow.xib file in the Interface Builder. Load the DoubleComponentPickerViewController file in the MainWindow.xib . Select the DoubleCompoPicker from the tab bar and bring up Attribute inspector change the NIB name into DoubleComponentPickerViewController and bring up Identity Inspector change the class name into DoubleComponentPickerViewController. Save the nib file , close it and go back to the Xcode
.
Step 15: Now make the following changes in the DoubleComponentPickerViewController.m file:

-(IBAction)buttonPressed2
  {
   NSInteger breadRow = [doublePicker selectedRowInComponent:
   kBreadComponent];
   NSInteger fillingRow = [doublePicker selectedRowInComponent:
   kFillingComponent];
   NSString *bread = [breadTypes objectAtIndex:breadRow];
   NSString *filling = [fillingTypes objectAtIndex:fillingRow];    NSString *message = [[NSString alloc] initWithFormat:
   @"Your %@ on %@ bread will be right up.",filling, bread];
   UIAlertView *alert =[[UIAlertView alloc] initWithTitle:@"Thank you for   your order" message:message delegate:nil cancelButtonTitle:@"Great!"   otherButtonTitles:nil];
   [alert show];
   [alert release];
   [message release];
}
 - (void)viewDidLoad {
  NSArray *breadArray = [[NSArray alloc] initWithObjects:
  @"White",@"Whole Wheat",@"Rye",@"Sourdough",@"Seven Grain", nil];
  self.breadTypes = breadArray;
  [breadArray release];
  NSArray *fillingArray = [[NSArray alloc] initWithObjects:
  @"Chicken",@" Butter",@"Mutton",@"Salad",@"Beef",
  @"Vegetable",nil];
  self.fillingTypes = fillingArray;
  [fillingArray release];
  [super viewDidLoad];
}
 -(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
 {
   return 2;
}
  -(NSInteger)pickerView:(UIPickerView *)pickerView
  numberOfRowsInComponent:(NSInteger)component
 {
   if (component == kBreadComponent)
   return[self.breadTypes count];
   return[self.fillingTypes count];
}
  -(NSString *)pickerView:(UIPickerView *)pickerView
  titleForRow:(NSInteger)row
  forComponent:(NSInteger)component
 {
   if (component == kBreadComponent)
   return [self. breadTypes objectAtIndex:row];
   return [self.fillingTypes objectAtIndex:row];
 }

Step 16: Now compile and run the application.
 

Tuesday 12 April 2011

Badge

Using Application Badges

Several native applications on the iPhone use application badges as an indicator of new messages, think email and SMS. Creating badges is quite straightforward and is nothing more than a method call, passing in the desired number to display.

The image below shows how a badge may look when applied to your application. The code to create the badge is below the image.

 

[[UIApplication sharedApplication] setApplicationIconBadgeNumber:99];
 
As one would expect, the iPhone does limit the number of digits it will display – see the code and image that follow:
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:123456];
 
 


Create an Custom Badge

Here the code is:


CustomBadge.h
#import
@interface CustomBadge : UIView {

NSString *badgeText;
UIColor *badgeTextColor;
UIColor *badgeInsetColor;
UIColor *badgeFrameColor;
BOOL badgeFrame;
CGFloat badgeCornerRoundness;

}

@property(nonatomic,retain) NSString *badgeText;
@property(nonatomic,retain) UIColor *badgeTextColor;
@property(nonatomic,retain) UIColor *badgeInsetColor;
@property(nonatomic,retain) UIColor *badgeFrameColor;
@property(nonatomic,readwrite) BOOL badgeFrame;
@property(nonatomic,readwrite) CGFloat badgeCornerRoundness;

+ (CustomBadge*) customBadgeWithString:(NSString *)badgeString;
+ (CustomBadge*) customBadgeWithString:(NSString *)badgeString withStringColor:(UIColor*)stringColor withInsetColor:(UIColor*)insetColor withBadgeFrame:(BOOL)badgeFrameYesNo withBadgeFrameColor:(UIColor*)frameColor;
- (void) autoBadgeSizeWithString:(NSString *)badgeString;
@end


CustomBadge.m


#import "CustomBadge.h"

@interface CustomBadge()
- (id) initWithString:(NSString *)badgeString;
- (id) initWithString:(NSString *)badgeString withStringColor:(UIColor*)stringColor withInsetColor:(UIColor*)insetColor withBadgeFrame:(BOOL)badgeFrameYesNo withBadgeFrameColor:(UIColor*)frameColor;
- (void) drawRoundedRectWithContext:(CGContextRef)context withRect:(CGRect)rect;
- (void) drawFrameWithContext:(CGContextRef)context withRect:(CGRect)rect;
@end

@implementation CustomBadge

@synthesize badgeText;
@synthesize badgeTextColor;
@synthesize badgeInsetColor;
@synthesize badgeFrameColor;
@synthesize badgeFrame;
@synthesize badgeCornerRoundness;

// Use this method if you want to change the badge text after the first rendering
- (void) autoBadgeSizeWithString:(NSString *)badgeString
{
CGSize retValue = CGSizeMake(25, 25);
CGFloat rectWidth, rectHeight;
CGSize stringSize = [badgeString sizeWithFont:[UIFont boldSystemFontOfSize:12]];
CGFloat flexSpace;
if ([badgeString length]>=2)
{
flexSpace = [badgeString length]*1;
rectWidth = 25 + (stringSize.width + flexSpace); rectHeight = 25;
retValue = CGSizeMake(rectWidth, rectHeight);
}
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, retValue.width, retValue.height);
self.badgeText = badgeString;
}

// I recommend to use the allocator customBadgeWithString
- (id) initWithString:(NSString *)badgeString
{
self = [super initWithFrame:CGRectMake(0, 0, 25, 25)];
[self autoBadgeSizeWithString:badgeString];
if(self!=nil)
{
self.backgroundColor = [UIColor clearColor];
self.badgeText = badgeString;
self.badgeTextColor = [UIColor whiteColor];
self.badgeFrame = YES;
self.badgeFrameColor = [UIColor whiteColor];
self.badgeInsetColor = [UIColor redColor];
self.badgeCornerRoundness = 0.40;
}
return self;
}

// I recommend to use the allocator customBadgeWithString
- (id) initWithString:(NSString *)badgeString withStringColor:(UIColor*)stringColor withInsetColor:(UIColor*)insetColor withBadgeFrame:(BOOL)badgeFrameYesNo withBadgeFrameColor:(UIColor*)frameColor
{
self = [super initWithFrame:CGRectMake(0, 0, 25, 25)];
[self autoBadgeSizeWithString:badgeString];
if(self!=nil)
{
self.backgroundColor = [UIColor clearColor];
self.badgeText = badgeString;
self.badgeTextColor = stringColor;
self.badgeFrame = badgeFrameYesNo;
self.badgeFrameColor = frameColor;
self.badgeInsetColor = insetColor;
self.badgeCornerRoundness = 0.40;
}
return self;
}

// Creates a Badge with a given Text
+ (CustomBadge*) customBadgeWithString:(NSString *)badgeString
{
return [[[self alloc] initWithString:badgeString] autorelease];
}

// Creates a Badge with a given Text, Text Color, Inset Color, Frame (YES/NO) and Frame Color
+ (CustomBadge*) customBadgeWithString:(NSString *)badgeString withStringColor:(UIColor*)stringColor withInsetColor:(UIColor*)insetColor withBadgeFrame:(BOOL)badgeFrameYesNo withBadgeFrameColor:(UIColor*)frameColor
{
return [[[self alloc] initWithString:badgeString withStringColor:stringColor withInsetColor:insetColor withBadgeFrame:badgeFrameYesNo withBadgeFrameColor:frameColor] autorelease];
}


// Draws the Badge with Quartz
-(void) drawRoundedRectWithContext:(CGContextRef)context withRect:(CGRect)rect
{
CGFloat radius = CGRectGetMaxY(rect)*self.badgeCornerRoundness;
CGFloat puffer = CGRectGetMaxY(rect)*0.10;

CGFloat maxX = CGRectGetMaxX(rect) - puffer;
CGFloat maxY = CGRectGetMaxY(rect) - puffer;
CGFloat minX = CGRectGetMinX(rect) + puffer;
CGFloat minY = CGRectGetMinY(rect) + puffer;

CGContextBeginPath(context);
CGContextSetFillColorWithColor(context, [self.badgeInsetColor CGColor]);
CGContextAddArc(context, maxX-radius, minY+radius, radius, M_PI+(M_PI/2), 0, 0);
CGContextAddArc(context, maxX-radius, maxY-radius, radius, 0, M_PI/2, 0);
CGContextAddArc(context, minX+radius, maxY-radius, radius, M_PI/2, M_PI, 0);
CGContextAddArc(context, minX+radius, minY+radius, radius, M_PI, M_PI+M_PI/2, 0);
CGContextSetShadowWithColor(context, CGSizeMake(2,2), 3, [[UIColor blackColor] CGColor]);
CGContextClosePath(context);
CGContextFillPath(context);

}

// Draws the Badge Frame with Quartz
-(void) drawFrameWithContext:(CGContextRef)context withRect:(CGRect)rect
{
CGFloat radius = CGRectGetMaxY(rect)*self.badgeCornerRoundness;
CGFloat puffer = CGRectGetMaxY(rect)*0.10;

CGFloat maxX = CGRectGetMaxX(rect) - puffer;
CGFloat maxY = CGRectGetMaxY(rect) - puffer;
CGFloat minX = CGRectGetMinX(rect) + puffer;
CGFloat minY = CGRectGetMinY(rect) + puffer;

CGContextBeginPath(context);
CGContextSetLineWidth(context, 2);
CGContextSetStrokeColorWithColor(context, [self.badgeFrameColor CGColor]);
CGContextAddArc(context, maxX-radius, minY+radius, radius, M_PI+(M_PI/2), 0, 0);
CGContextAddArc(context, maxX-radius, maxY-radius, radius, 0, M_PI/2, 0);
CGContextAddArc(context, minX+radius, maxY-radius, radius, M_PI/2, M_PI, 0);
CGContextAddArc(context, minX+radius, minY+radius, radius, M_PI, M_PI+M_PI/2, 0);
CGContextClosePath(context);
CGContextStrokePath(context);
}

// Draws Method
- (void)drawRect:(CGRect)rect {

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetShouldAntialias(context, YES);

CGLayerRef buttonLayer = CGLayerCreateWithContext(context, rect.size, NULL);
CGContextRef buttonLayer_Context = CGLayerGetContext(buttonLayer);
[self drawRoundedRectWithContext:buttonLayer_Context withRect:rect];
CGContextDrawLayerInRect(context, rect, buttonLayer);
CGLayerRelease(buttonLayer);

if (self.badgeFrame)
{
CGLayerRef frameLayer = CGLayerCreateWithContext(context, rect.size, NULL);
CGContextRef frameLayer_Context = CGLayerGetContext(frameLayer);
[self drawFrameWithContext:frameLayer_Context withRect:rect];
CGContextDrawLayerInRect(context, rect, frameLayer);
CGLayerRelease(frameLayer);
}

if ([self.badgeText length]>0)
{
[badgeTextColor set];
UIFont *textFont = [UIFont boldSystemFontOfSize:13];
CGSize textSize = [self.badgeText sizeWithFont:textFont];
[self.badgeText drawAtPoint:CGPointMake((rect.size.width/2-textSize.width/2), (rect.size.height/2-textSize.height/2)) withFont:textFont];

}

}

- (void)dealloc {

[badgeText release];
[badgeTextColor release];
[badgeInsetColor release];
[badgeFrameColor release];

[super dealloc];
}

@end

CustomBadgeViewController.h
#import CustomBadgeViewController.h
@interface CustomBadgeViewController : UIViewController {

}
@end

CustomBadgeViewController.m

#import "CustomBadgeViewController.h"
#import "CustomBadge.h"

@implementation CustomBadgeViewController

- (void) viewDidLoad
{

// Create simple Badge
CustomBadge *customBadge1 = [CustomBadge customBadgeWithString:@"Badge 1"];

// Create advanced Badge
CustomBadge *customBadge2 = [CustomBadge customBadgeWithString:@"Badge 2"
withStringColor:[UIColor whiteColor]
withInsetColor:[UIColor blueColor]
withBadgeFrame:YES
withBadgeFrameColor:[UIColor whiteColor]];

// Set Position of Badge 1
[customBadge1 setFrame:CGRectMake(0, 30, customBadge1.frame.size.width, customBadge1.frame.size.height)];

// Add Badges to View
[self.view addSubview:customBadge1];
[self.view addSubview:customBadge2];

// Change text afterwards
[customBadge1 autoBadgeSizeWithString:@"Neuer Text"];

}

- (void)dealloc {
[super dealloc];
}
@end

 



Custom Scrollview


Multiple virtual pages in a UIScrollView with just 2 views

I'm presenting the following application which looks like this:

 

  The black region is a UIScrollView with pagingEnabled set to YES. The dots at the bottom are a UIPageControl indicating the number of pages in the UIScrollView.
Apple's sample application "PageControl" presents a similar user interface that loads a different UIViewController and UIView for each of the pages in the UIScrollView. I'll show you how you can create the same effect using exactly two child UIViewControllers and UIViews — a "current" and the "next" view which leap around out-of-view to fill in each new page as the scrolling reaches it.

Moving child views to maintain the illusion

The trick in this post is handling an arbitrary array of child pages using just two views. It will work as follows:
  1. Initially, the displayed view will be the currentPage view. The next view will be configured to display the cached data for Page 1
  2. As the user begins scrolling to the right, the nextPage is quickly moved into the location for the next page in the scrolling direction. At the same time as it is positioned, nextPage is configured with the data for Page 2. Neither of these configuration changes will be visible to the user.
  3. As the scroll operation ends, the pointers for currentPage and nextPage are swapped so that currentPage now points to the view displaying Page 2 and nextPage now points to Page 1
  4. If the next scroll is to the left, nextPage is already configured and in position. If the next scroll is to the right, nextPage will be moved and configured as it was during the first scroll.
The code that chooses how to move the views as the scroll view scrolls (step 2 in the above description) looks like this:



- (void)scrollViewDidScroll:(UIScrollView *)sender
{
    CGFloat pageWidth = scrollView.frame.size.width;
    float fractionalPage = scrollView.contentOffset.x / pageWidth;
    
    NSInteger lowerNumber = floor(fractionalPage);
    NSInteger upperNumber = lowerNumber + 1;
    
    if (lowerNumber == currentPage.pageIndex)
    {
        if (upperNumber != nextPage.pageIndex)
        {
            [self applyNewIndex:upperNumber pageController:nextPage];
        }
    }
    else if (upperNumber == currentPage.pageIndex)
    {
        if (lowerNumber != nextPage.pageIndex)
        {
            [self applyNewIndex:lowerNumber pageController:nextPage];
        }
    }
 

I've left off the final condition for size but it is a rarely invoked path for when very fast scrolling leaves the currentPage out of position and it needs to configure both pages.
The exchange of pointers at the end of scrolling (step 3 in the above description) is handled in the scrollViewDidEndScrollingAnimation: method:



- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)newScrollView
{
    CGFloat pageWidth = scrollView.frame.size.width;
    float fractionalPage = scrollView.contentOffset.x / pageWidth;
    NSInteger nearestNumber = lround(fractionalPage);

    if (currentPage.pageIndex != nearestNumber)
    {
        PageViewController *swapController = currentPage;
        currentPage = nextPage;
        nextPage = swapController;
    }

    pageControl.currentPage = currentPage.pageIndex;
}
 


The only remaining component to reveal is exactly how the pages are positioned and configured in applyNewIndex:pageController:


- (void)applyNewIndex:(NSInteger)newIndex pageController:
(PageViewController *)pageController
{
    NSInteger pageCount = 
[[DataSource sharedDataSource] numDataPages];
    BOOL outOfBounds = newIndex >= pageCount || newIndex < 0;

    if (!outOfBounds)
    {
        CGRect pageFrame = pageController.view.frame;
        pageFrame.origin.y = 0;
        pageFrame.origin.x = scrollView.frame.size.width * newIndex;
        pageController.view.frame = pageFrame;
    }
    else
    {
        CGRect pageFrame = pageController.view.frame;
        pageFrame.origin.y = scrollView.frame.size.height;
        pageController.view.frame = pageFrame;
    }

    pageController.pageIndex = newIndex;
}

You can see here that if a page is given an out-of-bounds index, it is placed below the bottom of the scroll view (invisible). I chose not to use setHidden: on the view because this resulted in "pop-in" when making the view visible again.
The actual configuration of the view for the new page index happens in the setter method for pageController.pageIndex. This method fetches the data for the page out of the DataSource and configures the view for displaying that page.



- (void)setPageIndex:(NSInteger)newPageIndex
{
    pageIndex = newPageIndex;
    
    if (pageIndex >= 0 &&
        pageIndex < [[DataSource sharedDataSource] numDataPages])
    {
        NSDictionary *pageData =
            [[DataSource sharedDataSource] dataForPage:pageIndex];
        label.text = [pageData objectForKey:@"pageName"];
        textView.text = [pageData objectForKey:@"pageText"];
    }
}


Notice that I've made this method tolerant of out-of-bounds indices. This is because while out-of-bounds indices are invalid for data from the DataSource, they can be valid locations for views in the scroll view (representing an offscreen position) so these positions must be permitted.




Building The Custom UIScrollView Menu 

Building The Main Slider


The requirements were simple: can be swiped left or right and each item in the menu can be selected. This led me to make the main component a UIScrollView subclass. I subclassed it because I needed to do my own custom drawing in its drawRect method to execute the design. Let's take a look at the drawing code, it's very simple:
- (void)drawRect:(CGRect)rect {
    UIImage *bg = [[UIImage imageNamed:@"slider.png"]
      stretchableImageWithLeftCapWidth:15 topCapHeight:0];
    [bg drawInRect:self.bounds];
}
 
Here we're taking a PNG, stretching it horizontally, and drawing it in
the precise location that this scrollview is located. The left cap of
15px means that the first 15px of the image are kept pixel-precise, the
16th pixel is used to stretch across the wide area, then the final
right pixels are kept pixel precise also. This is a common technique to
execute custom designs, I wish I could do this in CSS! 
  
Adding The Tappable Topics 

To make a UIScrollView actually scroll you need to know the total width (or height) of the content it contains. For my scrollview, I programmatically add the tappable topics and then calculate the total width of them once I'm finished. I first thought to make each topic a custom UIButton but for some reason, if the buttons are one-after-another with no pixels in between, the touch events they intercepted stopped the slider from scrolling. I couldn't quite figure out the issue but fortunately there are numerous ways to accomplish the same design. Instead of UIButtons I decided to use UILabel subclasses and add the tap events myself using UIGestureRecognizers, one of the new APIs available on the iPad. Here's the code that calculates the total width of this scrollview's content:

CGFloat contentWidth = 20;
for( NSString *string in topics ) {
    contentWidth += [string sizeWithFont:[UIFont boldSystemFontOfSize:18]].width + 30;
}
self.contentSize = CGSizeMake(contentWidth, self.bounds.size.height);

 
Here I'm using NSString's method sizeWithFont to calculate the exact size of a string rendered using a given font. The 30px extra is to account for 15px padding on the left and right of each one. Once I've iterated across my entire array of topics, I now have an exact pixel amount to assign to the contentSize property.
After calculating the total width, I add each topic one at a time to the scrollview. (DPTopicLabel is my UILabel subclass.)


CGPoint startingPoint = CGPointMake(10, 0);
for( NSString *string in topics ) {
    CGRect labelRect = CGRectMake(startingPoint.x, startingPoint.y,
      [string sizeWithFont:[UIFont boldSystemFontOfSize:18]].width + 30,
      self.bounds.size.height);
            
    DPTopicLabel *label = [[DPTopicLabel alloc] initWithFrame:labelRect andText:string];
    if( [string isEqualToString:@"Top Stories"] ) {
        [label setSelected:YES];
    }
            
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
      initWithTarget:self action:@selector(handleTap:)];
    [label addGestureRecognizer:tap];
    [tap release];
            
    startingPoint.x += label.bounds.size.width;
    [self addSubview:label];
    [label release];
}

 
First, I create the CGRect that will be the exact position of my tappable topic. The CGPoint startingPoint is updated at the end of each iteration to push it ahead to where the next topic will go. Next, I create my new DPTopicLabel and use my custom initWithFrame:andText: method to pass in what the text should be. If the string is "Top Stories" then I call my setSelected method which draws the custom background for a selected topic.
After I create the topic I need to make it respond to touch events. There are a few ways to do this but I like how the new gesture recognizers work so I used that. Once you add a gesture recognizer to a view, you tell it what type of gesture you want it to recognize (complicated, eh?) and then what method you want it to call when it happens. In this case I'm catching tap events and passing in my handleTap method which will toggle the selected state of my label.
All that's left to do at the end is change my startingPoint variable and add the label to the overall scrollview. Done!


UIScrollView


UIScrollView – A really simple tutorial


The UIScrollView it’s a very versatile class, you can handle zooming, panning, scrolling, etc, and I have no intention of explaining all the properties and delegates (well, if you want to know something about the UIScrollView, fell free to request it in the comments, just let me know =D ), the documentation itself is pretty good, so you should give it a look.

Basic:

       There’s 2 very important properties in UIScrollView, the contentSize and the contentOffset.
The contentsize is the width and height of your content, it’s a CGSize and a property of UIScrollView, let’s say that you have an image that’s 500×500, it would not fit on the iPhone screen, right? So, set your contentsize to 500,500. If you want to add more scrollable space at the bottom or at the top, you can use the property contentInset.top and contentInset.bottom, so you can add some extra space without changing the contentsize. And why would you need these insets? Well, if you have a UINavigationBar or a UIToolBar, like the photos app, you will use this.

Every UIScrollView has a scroll indicator (it’s visible by default, but if you want, you can hide it with showsHorizontalScrollIndicator and showsVerticalScrollIndicator), to give an indication of how far in the content you are, and you can change where the indicator starts using the scrollIndicatorInsets.top just like the contentInsets.The contentOffset is the point that is currently visible, this point represents the top left of your screen. The contentOffset discards the contentInsets, so it can happen that the contentInset is negative, that’s not a problem.

The only thing that you need to have a functional UIScrollView is the UIView that you want to display and the contentSize of this UIView, so let’s start coding…

Create a View-based Application and name it SimpleScroll. Again, I always use the Window-based Application for my projects, but let’s pick the View-based just to speed things up.
Go to your SimpleScrollViewController class, in the loadView method and create your UIScrollView, we will create it with the same width and heigh as the view from SimpleScrollViewController

UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
Enable the pagination
scroll.pagingEnabled = YES;
If the value of this property is YES, the scroll view stops on multiples of the view bounds when the user scrolls. The default value is NO.

Create all three UIViews

NSInteger numberOfViews = 3;
for (int i = 0; i < numberOfViews; i++) {
CGFloat yOrigin = i * self.view.frame.size.width;
UIView *awesomeView = [[UIView alloc] initWithFrame:CGRectMake(yOrigin, 0, self.view.frame.size.width, self.view.frame.size.height)];
awesomeView.backgroundColor = [UIColor colorWithRed:0.5/i green:0.5 blue:0.5 alpha:1];
[scroll addSubview:awesomeView];
[awesomeView release];
}


Set the UIScrollView contentSize

scroll.contentSize = CGSizeMake(self.view.frame.size.width * numberOfViews, self.view.frame.size.height);

The contentSize is just the sum of the widths of the three UIViews, if the width of each UIView is 320, and we have three UIViews, your contentSize width will be 920.



Add the UIScrollView to the SimpleScrollViewController UIView

[self.view addSubview:scroll];
[scroll release];
And you’re done :)



At the end, you should have something like this :


- (void)loadView {
[super loadView];
self.view.backgroundColor = [UIColor redColor];
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];

scroll.pagingEnabled = YES;

NSInteger numberOfViews = 3;
for (int i = 0; i < numberOfViews; i++) {
CGFloat yOrigin = i * self.view.frame.size.width;

UIView *awesomeView = [[UIView alloc] initWithFrame:CGRectMake(yOrigin, 0, self.view.frame.size.width, self.view.frame.size.height)];

awesomeView.backgroundColor = [UIColor colorWithRed:0.5/i green:0.5 blue:0.5 alpha:1];

[scroll addSubview:awesomeView];

[awesomeView release];

}
scroll.contentSize = CGSizeMake(self.view.frame.size.width * numberOfViews, self.view.frame.size.height);

[self.view addSubview:scroll];
[scroll release];

}

As you can see, I changed the background color of the SimpleScrollViewController UIView to red, just to make sure that you can distinguish the content of the UIScrollView.

 



UIScrollView and Zoom

Create a ScrollView as:

- (void)loadView {
UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
scroll.backgroundColor = [UIColor blackColor];
scroll.delegate = self;
image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"image.JPG"]];
scroll.contentSize = image.frame.size;
[scroll addSubview:image];
scroll.minimumZoomScale = scroll.frame.size.width / image.frame.size.width;
scroll.maximumZoomScale = 2.0;
[scroll setZoomScale:scroll.minimumZoomScale];
self.view = scroll;
[scroll release];
}
we have to implement a UIScrollView delegate method called
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return image;
}

Place the image is on the center of your screen


- (CGRect)centeredFrameForScrollView:(UIScrollView *)scroll andUIView:(UIView *)rView {

CGSize boundsSize = scroll.bounds.size;
CGRect frameToCenter = rView.frame;
// center horizontally
if (frameToCenter.size.width < boundsSize.width)
{
frameToCenter.origin.x = (boundsSize.width – frameToCenter.size.width) / 2; } else { frameToCenter.origin.x = 0;
} // center vertically
if (frameToCenter.size.height < boundsSize.height)
{
frameToCenter.origin.y = (boundsSize.height – frameToCenter.size.height) / 2; }
else {
frameToCenter.origin.y = 0;
}
return frameToCenter;
}

This method will receive your UIScrollView and the UIView that you are zooming and calculate the correct frame for your UIView and return a CGRect. All you have to do is call this method on your scrollViewDidZoom delegate, that’s get called everytime that there’s some zooming, and you are ready.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
image.frame = [self centeredFrameForScrollView:scrollView andUIView:image];;
}


Monday 11 April 2011

SpringBoard



SpringBoard Implmentation


              I've implemented a little iPhone app that reproduces the behavior of the iPhone home screen's icon reorganization interface. (You know, dragging the wiggly icons around.).

The primary classes to look at are TilesViewController and Tile. The view controller implements all of the "logic" of the application, while the Tile class has the animations.
An instance of Tile represents one of the icons, and is derived from CAGradientLayer. The gradient layer properties get set to provide a gloss effect for the tiles. Tile also provides a few animations, initiated by calling these methods:
  • appearDraggable: Changes the tile to be partially transparent, and makes it slightly bigger. This is invoked when the user touches a tile.
  • appearNormal: Reverses the effects of appearDraggable. This is invoked when the tile is released.
  • startWiggling: Starts a tile "wiggling", as in the iPhone home screen while in reorganization mode.
  • stopWiggling: Stops the wiggling effect
The TilesViewController class is pretty straightforward. When the user touches a the screen, the touchesBegan method determines which tile was touched, calls its appearDraggable method, and calls other tiles' startWiggling methods.
As the user drags the tile around the screen, the touchesMoved method moves the dragged tile, and moves the other tiles as needed to provide an open space for it. Core Animation takes care of all the zooming around of the icons.
When the user lets go of the tile, the touchesEnded method drops it in place and removes all the animations.
Things I learned from this project:
  • Turning on the masksToBounds property for layers slows things down quite noticeably.
  • When hit-testing layers, you have to use a layer's presentation layer, not a model layer itself.
  • CAGradientLayer is easy to use.
Here are some things I don't understand. (Maybe some smart person can explain.)
  • When hit-testing to see which layer was touched, I had to do both [touch locationInView:view] and [view convertPoint:location toView:nil]. However, when handling touch-moves, I only have to use [touch locationInView:view]. I don't understand why the coordinate systems are (apparently) different.
 


    Here the code is:

     TilesViewController.h

    #import "uikit/uikit.h"
    #define TILE_ROWS 6
    #define TILE_COLUMNS 4
    #define TILE_COUNT (TILE_ROWS * TILE_COLUMNS)

    @class Tile;

    @interface TilesViewController : UIViewController {
    @private

    CGRect tileFrame[TILE_COUNT];
    Tile *tileForFrame[TILE_COUNT];
    Tile *heldTile;
    int heldFrameIndex;
    CGPoint heldStartPosition;
    CGPoint touchStartLocation;

    }
    @end

    TilesViewController.m
    #import "TilesViewController.h"
    #import "Tile.h"
    #import "QuartzCore/QuartzCore.h"


    #define TILE_WIDTH 57
    #define TILE_HEIGHT 57
    #define TILE_MARGIN 18


    @interface TilesViewController ()
    - (void)createTiles;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    - (CALayer *)layerForTouch:(UITouch *)touch;
    - (int)frameIndexForTileIndex:(int)tileIndex;
    - (int)indexOfClosestFrameToPoint:(CGPoint)point;
    - (void)moveHeldTileToPoint:(CGPoint)location;
    - (void)moveUnheldTilesAwayFromPoint:(CGPoint)location;
    - (void)startTilesWiggling;
    - (void)stopTilesWiggling;
    @end


    @implementation TilesViewController


    - (void)viewDidLoad {
    [super viewDidLoad];
    [self createTiles];
    }


    - (void)createTiles {
    UIColor *tileColors[] = {
    [UIColor blueColor],
    [UIColor brownColor],
    [UIColor grayColor],
    [UIColor greenColor],
    [UIColor orangeColor],
    [UIColor purpleColor],
    [UIColor redColor],
    };
    int tileColorCount = sizeof(tileColors) / sizeof(tileColors[0]);

    for (int row = 0; row < TILE_ROWS; ++row) {
    for (int col = 0; col < TILE_COLUMNS; ++col) {
    int index = (row * TILE_COLUMNS) + col;
    CGRect frame = CGRectMake(TILE_MARGIN + col * (TILE_MARGIN + TILE_WIDTH),
    TILE_MARGIN + row * (TILE_MARGIN + TILE_HEIGHT),
    TILE_WIDTH, TILE_HEIGHT);

    tileFrame[index] = frame;
    Tile *tile = [[Tile alloc] init];
    tile.tileIndex = index;
    tileForFrame[index] = tile;
    tile.frame = frame;
    tile.backgroundColor = tileColors[index % tileColorCount].CGColor; tile.cornerRadius = 8;
    tile.delegate = self;

    [self.view.layer addSublayer:tile];
    [tile setNeedsDisplay];
    [tile release];
    }
    }
    }

    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    UIGraphicsPushContext(ctx);
    Tile *tile = (Tile *)layer;
    [tile draw];
    UIGraphicsPopContext(); }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    CALayer *hitLayer = [self layerForTouch:[touches anyObject]];
    if ([hitLayer isKindOfClass:[Tile class]]) {
    Tile *tile = (Tile*)hitLayer;
    heldTile = tile;
    touchStartLocation = [[touches anyObject] locationInView:self.view];
    heldStartPosition = tile.position;
    heldFrameIndex = [self frameIndexForTileIndex:tile.tileIndex];
    [tile moveToFront];
    [tile appearDraggable];
    [self startTilesWiggling];
    } }

    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

    if (heldTile) {
    UITouch *touch = [touches anyObject];
    UIView *view = self.view;
    CGPoint location = [touch locationInView:view];
    [self moveHeldTileToPoint:location];
    [self moveUnheldTilesAwayFromPoint:location];
    } }

    - (void)moveHeldTileToPoint:(CGPoint)location {
    float dx = location.x - touchStartLocation.x;
    float dy = location.y - touchStartLocation.y;
    CGPoint newPosition = CGPointMake(heldStartPosition.x + dx, heldStartPosition.y + dy);
    [CATransaction begin];
    [CATransaction setDisableActions:TRUE];
    heldTile.position = newPosition;
    [CATransaction commit];
    }

    - (void)moveUnheldTilesAwayFromPoint:(CGPoint)location {
    int frameIndex = [self indexOfClosestFrameToPoint:location];
    if (frameIndex != heldFrameIndex) {

    [CATransaction begin];
    if (frameIndex < heldFrameIndex) {
    for (int i = heldFrameIndex; i > frameIndex; --i) {

    Tile *movingTile = tileForFrame[i-1];
    movingTile.frame = tileFrame[i];
    tileForFrame[i] = movingTile;
    }
    }

    else if (heldFrameIndex < frameIndex) {
    for (int i = heldFrameIndex; i < frameIndex; ++i) {
    Tile *movingTile = tileForFrame[i+1];
    movingTile.frame = tileFrame[i];
    tileForFrame[i] = movingTile;
    } }
    heldFrameIndex = frameIndex;
    tileForFrame[heldFrameIndex] = heldTile;
    [CATransaction commit]; } }

    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    if (heldTile) {
    [heldTile appearNormal];
    heldTile.frame = tileFrame[heldFrameIndex];
    heldTile = nil; }
    [self stopTilesWiggling]; }

    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {

    [self touchesEnded:touches withEvent:event];
    }

    - (CALayer *)layerForTouch:(UITouch *)touch {

    UIView *view = self.view;
    CGPoint location = [touch locationInView:view];
    location = [view convertPoint:location toView:nil];

    CALayer *hitPresentationLayer = [view.layer.presentationLayer hitTest:location];
    if (hitPresentationLayer) {
    return hitPresentationLayer.modelLayer; }
    return nil; }

    - (int)frameIndexForTileIndex:(int)tileIndex {

    for (int i = 0; i < TILE_COUNT; ++i) {

    if (tileForFrame[i].tileIndex == tileIndex) {
    return i; } }
    return 0; }

    - (int)indexOfClosestFrameToPoint:(CGPoint)point {

    int index = 0;
    float minDist = FLT_MAX;
    for (int i = 0; i < TILE_COUNT; ++i) {
    CGRect frame = tileFrame[i];
    float dx = point.x - CGRectGetMidX(frame);
    float dy = point.y - CGRectGetMidY(frame);
    float dist = (dx * dx) + (dy * dy);
    if (dist < minDist) {
    index = i;
    minDist = dist; } }
    return index; }

    - (void)startTilesWiggling {
    for (int i = 0; i < TILE_COUNT; ++i) {
    Tile *tile = tileForFrame[i];
    if (tile != heldTile) {
    [tile startWiggling]; }
    } }

    - (void)stopTilesWiggling {
    for (int i = 0; i < TILE_COUNT; ++i) {
    Tile *tile = tileForFrame[i];
    [tile stopWiggling]; } }
    @end


    Tile.h
    #import <Foundation/Foundation.h>
    #import <QuartzCore/CAGradientLayer.h>
    #import "CALayer+Additions.h"
    @interface Tile : CAGradientLayer {
        int tileIndex;   
    }
    @property (nonatomic) int tileIndex;

    - (void)draw;
    - (void)appearDraggable;
    - (void)appearNormal;
    - (void)startWiggling;
    - (void)stopWiggling;
    @end

    Tile.m

    #import "Tile.h"
    #import "NSString+Additions.h"
    #import <QuartzCore/QuartzCore.h>

    @interface Tile ()
    - (void)setGlossGradientProperties;
    @end

    @implementation Tile
    @synthesize tileIndex;

    - (id)init {
        self = [super init];
        if (self) {
            [self setGlossGradientProperties];
        }
        return self;
    }

    - (void)setGlossGradientProperties {
        static CGFloat colorComponents0[] = { 1.0, 1.0, 1.0, 0.35 };
        static CGFloat colorComponents1[] = { 1.0, 1.0, 1.0, 0.06 };
         
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGColorRef color0 = CGColorCreate(colorSpace, colorComponents0);
        CGColorRef color1 = CGColorCreate(colorSpace, colorComponents1);
        NSArray *colors = [NSArray arrayWithObjects:(id)color0,
                                                    (id)color1,
                                                    nil];
        CGColorRelease(color0);
        CGColorRelease(color1);
        CGColorSpaceRelease(colorSpace);
       
        self.colors = colors;
        self.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
                                                   [NSNumber numberWithFloat:1.0],
                                                   nil];
        self.startPoint = CGPointMake(0.5f, 0.0f);
        self.endPoint = CGPointMake(0.5f, 0.5f);
    }


    - (void)draw {
        NSString *labelText = [NSString stringWithFormat:@"%d", (int)tileIndex];

        UIFont *font = [UIFont boldSystemFontOfSize:36];
        [[UIColor whiteColor] set];   
        [labelText drawCenteredInRect:self.bounds withFont:font];
    }


    - (void)appearDraggable {
        self.opacity = 0.6;
        [self setValue:[NSNumber numberWithFloat:1.25] forKeyPath:@"transform.scale"];
    }


    - (void)appearNormal {
        self.opacity = 1.0;
        [self setValue:[NSNumber numberWithFloat:1.0] forKeyPath:@"transform.scale"];
    }


    - (void)startWiggling {
        CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-0.05],
                                                [NSNumber numberWithFloat:0.05],
                                                nil];
        anim.duration = 0.09f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        [self addAnimation:anim forKey:@"wiggleRotation"];
       
        anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-1],
                       [NSNumber numberWithFloat:1],
                       nil];
        anim.duration = 0.07f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        anim.additive = YES;
        [self addAnimation:anim forKey:@"wiggleTranslationY"];
    }

    - (void)stopWiggling {
        [self removeAnimationForKey:@"wiggleRotation"];
        [self removeAnimationForKey:@"wiggleTranslationY"];
    }

    @end


    Tile.m // additional features

    #import "Tile.h"
    #import "NSString+Additions.h"
    #import <QuartzCore/QuartzCore.h>


    @interface Tile ()
    - (void)setGlossGradientProperties;
    @end


    @implementation Tile

    @synthesize tileIndex;


    - (id)init {
        self = [super init];
        if (self) {
            [self setGlossGradientProperties];
        }
        return self;
    }


    - (void)setGlossGradientProperties {
        static CGFloat colorComponents0[] = { 1.0, 1.0, 1.0, 0.35 };
        static CGFloat colorComponents1[] = { 1.0, 1.0, 1.0, 0.06 };
         
        CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
        CGColorRef color0 = CGColorCreate(colorSpace, colorComponents0);
        CGColorRef color1 = CGColorCreate(colorSpace, colorComponents1);
        NSArray *colors = [NSArray arrayWithObjects:(id)color0,
                                                    (id)color1,
                                                    nil];
        CGColorRelease(color0);
        CGColorRelease(color1);
        CGColorSpaceRelease(colorSpace);
       
        self.colors = colors;
        self.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
                                                   [NSNumber numberWithFloat:1.0],
                                                   nil];
        self.startPoint = CGPointMake(0.5f, 0.0f);
        self.endPoint = CGPointMake(0.5f, 0.5f);
    }

    - (void)draw {
        NSString *labelText = [NSString stringWithFormat:@"%d", (int)tileIndex];

        UIFont *font = [UIFont boldSystemFontOfSize:36];
        [[UIColor whiteColor] set];   
        [labelText drawCenteredInRect:self.bounds withFont:font];
    }


    - (void)appearDraggable {
        self.opacity = 0.6;
        [self setValue:[NSNumber numberWithFloat:1.25] forKeyPath:@"transform.scale"];
    }

    - (void)appearNormal {
        self.opacity = 1.0;
        [self setValue:[NSNumber numberWithFloat:1.0] forKeyPath:@"transform.scale"];
    }
    - (void)startWiggling {
        CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-0.05],
                                                [NSNumber numberWithFloat:0.05],
                                                nil];
        anim.duration = 0.09f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        [self addAnimation:anim forKey:@"wiggleRotation"];
       
        anim = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.y"];
        anim.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:-1],
                       [NSNumber numberWithFloat:1],
                       nil];
        anim.duration = 0.07f + ((tileIndex % 10) * 0.01f);
        anim.autoreverses = YES;
        anim.repeatCount = HUGE_VALF;
        anim.additive = YES;
        [self addAnimation:anim forKey:@"wiggleTranslationY"];
    }
    - (void)stopWiggling {
        [self removeAnimationForKey:@"wiggleRotation"];
        [self removeAnimationForKey:@"wiggleTranslationY"];
    }
    @end


     NSString+Additions.h

    #import <Foundation/Foundation.h>

    @interface NSString (Additions)
    - (void)drawCenteredInRect:(CGRect)rect withFont:(UIFont *)font;
    @end

     NSString+Additions.m

    #import "NSString+Additions.h"

    @implementation NSString (Additions)

    - (void)drawCenteredInRect:(CGRect)rect withFont:(UIFont *)font {
        CGSize size = [self sizeWithFont:font];
       
        CGRect textBounds = CGRectMake(rect.origin.x + (rect.size.width - size.width) / 2,
                                       rect.origin.y + (rect.size.height - size.height) / 2,
                                       size.width, size.height);
        [self drawInRect:textBounds withFont:font];   
    }
    @end


    CALayer+Additions.h

    #import <Foundation/Foundation.h>
    #import <QuartzCore/CALayer.h>
    @interface CALayer (Additions)

    - (void)moveToFront;

    @end

    CALayer+Additions.m

    #import "CALayer+Additions.h"
    @implementation CALayer (Additions)

    - (void)moveToFront {
        CALayer *superlayer = self.superlayer;
        [self removeFromSuperlayer];
        [superlayer addSublayer:self];
    }
    @end