Translate

Wednesday, December 19, 2012

iOS SDK: Custom Delegates


iOS SDK: Custom Delegates


Delegates are a useful tool in communicating between objects. In this tutorial we will create and implement a custom delegate to allow three UISliders to adjust the background color of a ViewController.

About Delegates

Messages in Objective-C are a one-way street. A parent class can send a message to its child, but the child cannot, on its own, send a message to its parent. However, with the help of a delegate, two-way communication can be achieved. UIScrollView and UITableView use delegates routinely to communicate between the model, view, and controller. There are many reasons why the delegate pattern is useful. A delegate might be used to make an object reusable, to provide a flexible way to send messages, or to implement customization.

Step 1 : Create a New Project

Launch Xcode and click File > New > Project. Select an iOS Single View Application and click "Next." Name your product "Delegates" and enter a name for your Company Identifier, such as "com.companyName.delegates." Choose the iPhone device family and click "Next." Choose a location to store your project and click "Create."
Delegates - project setup


 Subclassing UISlider

Click File > New > File and choose a Cocoa Touch Objective-C class. Name your class "MTSlider" and choose UISlider from the "Subclass of" drop down menu. Click "Next," then click "Create."
Delegates - sublassing UISlider

StepAdding the Delegate Protocol Methods

First we need to declare the methods for the delegate. Click on "MTSlider.h." Type the following code above the interface.
1
2
3
4
5
6
7
@class MTSlider;
@protocol MTSliderDelegate <NSObject>
@optional
- (void)MTSliderDidChange:(MTSlider *)MTSlider withValue:(CGFloat)value;
@required
- (CGFloat)startPositionForMTSlider:(MTSlider *)MTSlider;
@end

Compiler Directive

Notice the line @class MTSlider. By placing this code above everything else, the compiler is informed that at some point down the line, MTSlider will be declared. Without this compiler directive, the compiler will give you a warning because it is expecting to find the interface for MTSlider right away.

Declaring the Delegate Methods

The delegate's methods are declared starting with @protocol. The protocol MTSliderDelegate conforms to the NSObject protocol for a specific reason. The NSObject protocol contains a method,respondsToSelector:, that can be used to ensure the delegate object actually implements an optional method before the method is called. Calling a method that isn't implemented by the delegate object will cause an application to crash.

Optional Method

Plainly speaking, an optional method is a method that does not have to be implemented by the delegate object; in this case the delegate object will be the ViewController, however it could be any object. The optional method MTSliderDidChange:withValue: is messaged in the delegate object when the slider's value changes.

Required Method

On the other hand, a required method is a method that must be implemented by the delegate object or else you will get a compiler warning. The required method startPositionForMTSlider: asks the delegate object where the sliders should start and gets its starting position value in return from the delegate.

Creating an Instance Variable for the Delegate

Still in "MTSlider.h," type the following code directly below @interface to declare an instance variable, or ivar, for the delegate.
1
id <MTSliderDelegate> sliderDelegate;
If you are using ARC, type the following code instead:
1
__weak id <MTSliderDelegate> sliderDelegate;
The ivar is of type id so it is flexible and can accept any object type. The next part, MTSliderDelegatesays that whatever object ends up being assigned to sliderDelegate will contain the protocol methods ofMTSliderDelegate as part of its own implementation.

Creating the Setters and Getters

Finish by synthesizing the setter and getter methods. Type the following code just below the instance variable closing brace.
1
@property (nonatomic, assign) id <MTSliderDelegate> sliderDelegate;
If you are using ARC, type the following code instead:
1
@property (nonatomic, weak) id <MTSliderDelegate> sliderDelegate;
Click on "MTSlider.m" and type the following code just below @implementation to complete the property.
1
@synthesize sliderDelegate;

Step 4: Conforming to the Delegate Protocol Methods

Click on the "ViewController.h" file. Type the following code to conform to the MTSliderDelegate protocol and import "MTSlider.h."
1
2
#import "MTSlider.h"
@interface ViewController : UIViewController <MTSliderDelegate>

Implementing the Delegate Protocol Methods

Click on the "ViewController.m" file and type the following code to implement the MTSliderDelegateprotocol methods.
1
2
3
4
- (CGFloat)startPositionForMTSlider:(MTSlider *)MTSlider{
}
- (void)MTSliderDidChange:(MTSlider *)MTSlider withValue:(CGFloat)value{
}

Step 5: Setting the Delegate

Custom Initializer

Creating a custom initializer is key to getting the starting position for the sliders. In "MTSlider.h," add the following code to declare the new initializer.
1
- (id)initWithFrame:(CGRect)frame andDelegate:(id<MTSliderDelegate>)delegateObject;
Click on "MTSlider.m" and look for the initWithFrame: method. Delete the existing method and replace it with the following code.
1
2
3
4
5
6
7
8
- (id)initWithFrame:(CGRect)frame andDelegate:(id<MTSliderDelegate>)delegateObject{
    self = [super initWithFrame:frame];
    if (self) {
        self.sliderDelegate = delegateObject;
        self.value = [sliderDelegate startPositionForMTSlider:self];
    }
    return self;
}
Setting the delegate during initialization allows the delegate method to be called immediately. The methodstartPositionForMTSlider: gets the starting location for the sliders. Because it is called in the initializer, the sliders' positions are set before they are drawn on the screen.

Overriding UISlider Method

The UISlider method setValue:animated: is called automatically each time a slider peg is moved. Still in the "MTSlider.m" file, add the following method.
1
2
3
4
5
6
- (void)setValue:(float)value animated:(BOOL)animated{
    [super setValue:value animated:animated];
    if (sliderDelegate != nil && [sliderDelegate respondsToSelector:@selector(MTSliderDidChange:withValue:)]){
        [[self sliderDelegate] MTSliderDidChange:self withValue:value];
    }
}
By overriding setValue:animated: each time a slider peg moves, a message is sent to the delegate object. Notice the call to super, super setValue:animated:. It is important that we don't accidentally mess up something the method is doing behind the scenes when overriding an existing method.
The delegate method MTSliderDidChange:withValue: is the optional protocol method declared earlier. The delegate object is messaged each time the slider changes. Remember, calling a method that hasn't been implemented will cause the application to crash. Calling respondsToSelector: on the delegate object verifies it is okay to go ahead and message the optional delegate method.

Setting the Background Color and Instantiating the Sliders

Click on the "ViewController.m" file and type the following code inside viewDidLoad to set the view's background color with four color components and instantiate the red, green, and blue MTSlider objects. If you are using ARC, be sure to remove the lines [redSlider release];[greenSlider release];, and [blueSlider release]; as these calls are not necessary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CGFloat sliderColorPosition = 0.3f;
self.view.backgroundColor = [UIColor colorWithRed:sliderColorPosition green:sliderColorPosition blue:sliderColorPosition alpha:1.0f];
CGRect redSliderFrame = CGRectMake(20.0f, 20.0f, 280.0f, 28.0f);
MTSlider *redSlider = [[MTSlider alloc] initWithFrame:redSliderFrame andDelegate:self];
redSlider.tag = 1;
[self.view addSubview:redSlider];
[redSlider release];
CGRect greenSliderFrame = CGRectMake(20.0f, 70.0f, 280.0f, 28.0f);
MTSlider *greenSlider = [[MTSlider alloc] initWithFrame:greenSliderFrame andDelegate:self];
greenSlider.tag = 2;
[self.view addSubview:greenSlider];
[greenSlider release];
CGRect blueSliderFrame = CGRectMake(20.0f, 120.0f, 280.0f, 28.0f);
MTSlider *blueSlider = [[MTSlider alloc] initWithFrame:blueSliderFrame andDelegate:self];
blueSlider.tag = 3;
[self.view addSubview:blueSlider];
[blueSlider release];
By using the custom MTSlider initializer, initWithFrame:andDelegate:, the delegate is set and theViewController object becomes the delegate object of each slider. Normally you might expect to see a delegate set using the following code: redSlider.sliderDelegate=self;. However, in this case, the delegate assignment is passed in during the initialization.
Notice each slider's tag property has been set. Initially, the background color of the ViewController is set to dark gray. As each slider is adjusted, the background color will change accordingly because the tag identifies which slider is active.

Changing the Background Color

Find the implementation of startPositionForMTSlider:, and type the following code inside the braces to set the initial value of the sliders to 0.3.
1
2
CGFloat sliderStartPosition = 0.3f;
return sliderStartPosition;

Adjusting the Background Color

Find the implementation of MTSliderDidChange:withValue:, and add the following code to adjust the background color.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (MTSlider.tag == 1) { //Red Slider
    CGColorRef bgColor = self.view.backgroundColor.CGColor;
    const CGFloat *colorsPointer = CGColorGetComponents(bgColor);
    CGFloat currentGreen = colorsPointer[1];
    CGFloat currentBlue = colorsPointer[2];
    self.view.backgroundColor = [UIColor colorWithRed:value green:currentGreen blue:currentBlue alpha:1.0f];
}
if (MTSlider.tag == 2) { //Green Slider
    CGColorRef bgColor = self.view.backgroundColor.CGColor;
    const CGFloat *colorsPointer = CGColorGetComponents(bgColor);
    CGFloat currentRed = colorsPointer[0];
    CGFloat currentBlue = colorsPointer[2];
    self.view.backgroundColor = [UIColor colorWithRed:currentRed green:value blue:currentBlue alpha:1.0f];
}
if (MTSlider.tag == 3) { //Blue Slider
    CGColorRef bgColor = self.view.backgroundColor.CGColor;
    const CGFloat *colorsPointer = CGColorGetComponents(bgColor);
    CGFloat currentRed = colorsPointer[0];
    CGFloat currentGreen = colorsPointer[1];
    self.view.backgroundColor = [UIColor colorWithRed:currentRed green:currentGreen blue:value alpha:1.0f];
}
Each time the slider changes, a message is sent to the delegate method. The message contains the current MTSlider and its value. The current slider's tag property is accessed to determine which slider sent the message, and the background color is updated accordingly.

Testing the Delegate

Click Product > Run, or click the "Run" arrow in the upper left corner, to view the sliders in action. Adjust the sliders to see how the child elements are able to control the parent element's background color.

Conclusion

There are many other ways to control the background color of a UIViewController, including targets or notifications. Apple designed UISlider to use a target pattern to pass data. However, if you want to add more functionality, the best way to communcate from a child to its parent is by creating a custom delegate.