CocoaTouch
Creating singletons using dispatch_once
1Love them or loathe them, sometimes you need to have a singleton. In fact every iOS and Mac OS application has at least one, UIApplication or NSApplication.
So what is a singleton? Wikipedia defines it as:
In software engineering, the singleton pattern is a design pattern used to implement the mathematical concept of a singleton, by restricting the instantiation of a class to one object.
Or as I would put it:
A singleton is a class, where only one instance of it can instantiated.
Although this is the actual definition of a singleton, this isn’t always the case in the world of Foundation. NSFileManger and NSNotificationCenter for example, are usually accessed through their class methods defaultManager and defaultCenter respectively. Although not strictly a singleton, these class methods return a shared instance of that class that developers can then access throughout their code. It is this approach that we will be looking at in this post.
There has always been a debate on the best way to implement the singleton pattern using Objective-C, and developers (including Apple) seem to have been changing their minds every couple of years. When Apple introduced Grand Central Dispatch (GCD) (in Mac OS 10.6 and iOS 4.0) they introduced a function that is perfect for implementing the singleton pattern.
This function is dispatch_once:
void dispatch_once( dispatch_once_t *predicate, dispatch_block_t block);
This function takes a predicate (which is a long, that in reality acts as a BOOL) that the dispatch_once function uses to check if the block has already been dispatched. It also takes the block that you wish to only be dispatched once for the lifetime of the application, for us this is the instantiation of our shared instance.
Not only does dispatch_once mean that your code will only ever get run once, it is also thread safe, which means you don’t have to bother with using anything like @synchronized to stop things getting out of sync when using multiple threads and/or queues.
This is verified by Apple’s GCD Documentation:
If called simultaneously from multiple threads, this function waits synchronously until the block has completed.
So how would you use this in practise?
Well lets say you have a Account Manager class, and you want to access a shared instance of this class throughout your application. You can simply implement a class method like the one below:
+ (AccountManager *)sharedManager { static AccountManager *sharedAccountManagerInstance = nil; static dispatch_once_t predicate; dispatch_once(&predicate, ^{ sharedAccountManagerInstance = [[self alloc] init]; }); return sharedAccountManagerInstance; }
This means whenever you want access this shared instance all you need to do is:
AccountManager *accountManager = [AccountManager sharedManager];
And that’s all there is to it, you now have a shared instance that you can access throughout your application, which will only be created once.
This approach has many advantages:
- It is thread safe
- It will keep the static analyser happy
- It is compatible with Automatic Reference Counting (ARC)
- It only requires a small amount of code
The only disadvantage with this approach is that it will still allow a non shared instance to be created:
AccountManager *accountManager = [[AccountManager alloc] init];
Sometimes you will actually want this behaviour, but it is something you need to be aware of when you really only want one instance of a class to ever be instantiated.
Detecting Multitouch Gestures in iOS
0Before iOS 3.2 was introduced on the iPad (and latter with iOS 4.0 on the iPhone and iPod Touch), interpreting touch events as different gestures was an awkward task, and relied upon subclassing UIView, and then implementing the following methods.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
If you were trying to detect if a user was dragging a view (or panning as Apple would say), you would usually implement the touchesBegan:withEvent: to start dragging the piece, touchesMoved:withEvent: to adjust the view’s frame, and then touchesEnded:withEvent: and touchesCancelled:withEvent: to stop detecting the drag. While implementing dragging was not too complex, trying to detect a more advance gesture such as a swipe or pinch was a lot more complex … and a lot of hard work to make it replicates Apple’s version of the gestures accurately.
Thankfully Apple created UIGestureRecognizer:
UIGestureRecognizer is an abstract base class for concrete gesture-recognizer classes. A gesture-recognizer object (or, simply, a gesture recognizer) decouples the logic for recognizing a gesture and acting on that recognition. When one of these objects recognizes a common gesture or, in some cases, a change in the gesture, it sends an action message to each designated target object.
Taken from Apple
As UIGestureRecognizer is an abstract class, you will never use it directly. You will however use one of its subclasses, that have been designed specifically to capture a specific gesture.
UITapGestureRecognizer UIPinchGestureRecognizer UIRotationGestureRecognizer UISwipeGestureRecognizer UIPanGestureRecognizer UILongPressGestureRecognizer
So lets take UIPanGestureRecognizer for an example, and we will use it to move a view around.
Our example view controller has a view (obviously), and this has a subview called panView, which is the view we will move around.
First thing that you will need to do, is create a pan gesture recognizer (UIPanGestureRecognizer) and add it to the panView. This will allow you to detect the gesture. To get informed about the gesture getting detected, you have to set the view controller as the delegate, and then have it call the the panViewWithGestureRecognizer: selector. (Note: You can set any object as the delegate, as long as it implements UIGestureRecognizerDelegate protocol. For this example the view controller makes sence.)
UIPanGestureRecognizer *panGesture = nil; panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panViewWithGestureRecognizer:)]; [panGesture setMaximumNumberOfTouches:2]; [panGesture setDelegate:self]; [panView addGestureRecognizer:panGesture]; [panGesture release];
panViewWithGestureRecognizer: will get called when the gesture changes state, the possible states are:
UIGestureRecognizerStatePossible UIGestureRecognizerStateBegan UIGestureRecognizerStateChanged UIGestureRecognizerStateEnded UIGestureRecognizerStatePossible UIGestureRecognizerStateCancelled UIGestureRecognizerStateFailed UIGestureRecognizerStateRecognized UIGestureRecognizerStateEnded
For this example we only care about if gesture began (UIGestureRecognizerStateBegan) or has changed (UIGestureRecognizerStateChanged). When a pan gesture happens (begins or changes) we want to move the center of panView to be underneath the users finger, thus dragging the piece.
To do this we implement in the following way.
- (void)panViewWithGestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer { if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) { CGPoint translation = [gestureRecognizer translationInView:[panView superview]]; [piece setCenter:CGPointMake([panView center].x + translation.x, [panView center].y + translation.y)]; [gestureRecognizer setTranslation:CGPointZero inView:[panView superview]]; } }
What makes this super easy is the convenience methodtranslationInView:, which is found on UIPanGestureRecognizer. This gives you the value of where the center of the panView should be, without having to calculate it yourself. We then need to set the translation back to zero using setTranslation:, so when we calculate the next gesture, it will be from the new position of the panView. And thats all there is to it.
As mentioned before there are a few gesture recogniser that have already been prebuilt for you to use, and you can even add multiple gesture recogniser to the same view. If you do this, you will want to implement:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
This simply allows you to specify if a gesture should be detected alongside another gesture e.g. a pinch and a rotation gesture. If Apple’s pre-built gesture recognizers are not enough for you, you can always create your own subclass of UIGestureRecognizer.
Core Animation using blocks
7Previously I have written a blog post about doing simple animations with UIViews, which you can find here. Starting in iOS 4.0 you can now do this with blocks.
Blocks have two major benefits (although there are more):
- They can be used in conjunction with Grand Central Dispatch (GCD), as an alternative to threading.
- They can be used for callbacks, instead of NSNotifications, callback selectors, function pointers etc. This is usually done by providing a callback/completion block, and this is what we will be looking at here.
The most common method that you will use to animate views using blocks, is the following class method on UIView:
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
This method allows you to set a time interval, along with an animations block and a completion block.
Lets say we have a UIView variable called redView.
redView = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,320)]; redView.backgroundColor = [UIColor redColor];
We will then add redView as a subview of the view controller’s view
[self.view addSubview:redView];
A common situation you may find your self in, is when you finish animating a view (e.g. changing its alpha), you would then want to remove it from it’s superview, and then release it from memory.
So if you had (for example) a method called animate to trigger off the animation, you would need to add an “animation did stop selector” to the animation. In the following example I have called it cleanUp.
- (void)animate{ [UIView beginAnimations:@"" context:NULL]; [UIView setAnimationDuration:5.0]; [UIView setAnimationDidStopSelector:@selector(cleanUp)]; [redView setAlpha:0.0]; [UIView commitAnimations]; } - (void)cleanUp{ [redView removeFromSuperview]; //I do release and = nil on the same line as a coding convention //so I don't forget to "nil" the variable [redView release], redView = nil; }
Although this works well enough, it does mean that your code is split up into the animation code and the (completion) clean up code. If you end up doing this a lot, your code can end up becoming very fragmented and hard to follow.
Using blocks we can do the following:
- (void)animate{ [UIView animateWithDuration:5.0 animations:^{ redView.alpha = 0.0; } completion:^(BOOL completed){ [redView removeFromSuperview]; [redView release],redView = nil; } ]; }
This keeps things nice and simple, and it means that you can easily see what code will be executed when the animation completes.
All of the animations you wish to do are passed in using a block:
^{ redView.alpha = 0.0; }
This block takes no arguments, and it also does not have a return type (so it defaults to void).
Instead of calling the cleanUp selector, we can simply pass in a completion block:
^(BOOL completed){ [redView removeFromSuperview]; [redView release],redView = nil; }
This block takes one parameter, which is a bool signifying if the animation has completed when the block is executed. In this example we ignore the completed variable, and we assume that the animation has completed for simplicity reasons. In the body of the block we do same as we did in the cleanUp selector, we remove redView from its super view and then release it from memory.
In some situations you won’t need to use blocks for Core Animation, but when you do, you will find it cleans up your code no end.
Rounding the corners of an UIView
1Rounded corners are all the rage at the moment, so you would think that there would be an API on UIView to do such a thing. If you looked at all of the UIView documentation, you would come to the conclusion that there is not an API to do such a thing, and you would be (kind of) wrong.
Although UIView does not allow you round its corners, CALayer does, and every UIView is backed by a CALayer.
To access all of the functionality of CALayer, you will need to import QuartzCore into any of header/implementation files that you intend to use CALayer in.
#import <QuartzCore/QuartzCore.h>CALayer has a lot of APIs available to it, but the property we are interested in is:
@property CGFloat cornerRadiusAs the property suggests, this allows you to set the CALayer‘s corner radius in points:
UIView *view = [[UIView alloc] initWithFrame:frame]; view.layer.cornerRadius = 10.0;
CALayer also has properties allowing you to set its border, shadow etc, so all of these can save you a lot of code … and unwanted images.
Retrieving the currently being played music track
8One question I keep hearing is “Can you find out what music track a user is listening to? as I want to use it for …”, where the reason usually revolves around posting it to a social network network site, or using it as IM status. Thankfully retrieving the currently being played music track is very easy thanks to the MediaPlayer Framework.
Each application has its own MPMusicPlayerController, but it also has access to the iPod’s MPMusicPlayerController, using the class method iPodMusicPlayer.
MPMusicPlayerController *iPodMusicPlayerController = [MPMusicPlayerController iPodMusicPlayer];
After you have got the iPod music player, you can then get the now playing item
MPMediaItem *nowPlayingItem = [iPodMusicPlayerController nowPlayingItem];
If the now playing item is nil, you know that the user is not playing a music track on their iPod.
Unlike many of the other APIs in iOS, you can’t access information such as the track name, via a simple string property. You have to use one of the following keys:
NSString *const MPMediaItemPropertyPersistentID; NSString *const MPMediaItemPropertyMediaType; NSString *const MPMediaItemPropertyTitle; NSString *const MPMediaItemPropertyAlbumTitle; NSString *const MPMediaItemPropertyArtist; NSString *const MPMediaItemPropertyAlbumArtist; NSString *const MPMediaItemPropertyGenre; NSString *const MPMediaItemPropertyComposer; NSString *const MPMediaItemPropertyPlaybackDuration; NSString *const MPMediaItemPropertyAlbumTrackNumber; NSString *const MPMediaItemPropertyAlbumTrackCount; NSString *const MPMediaItemPropertyDiscNumber; NSString *const MPMediaItemPropertyDiscCount; NSString *const MPMediaItemPropertyArtwork; NSString *const MPMediaItemPropertyLyrics; NSString *const MPMediaItemPropertyIsCompilation; NSString *const MPMediaItemPropertyReleaseDate; NSString *const MPMediaItemPropertyBeatsPerMinute; NSString *const MPMediaItemPropertyComments; NSString *const MPMediaItemPropertyAssetURL;
And then query the Media Player Item, using the instance method valueForProperty:.
NSString *itemTitle = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle];
And thus you will end up with a code snippet like this:
MPMusicPlayerController *iPodMusicPlayerController = [MPMusicPlayerController iPodMusicPlayer]; MPMediaItem *nowPlayingItem = [iPodMusicPlayerController nowPlayingItem]; if(nowPlayingItem) { NSString *itemTitle = [nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]; NSLog(@"User is playing the following song: %@",itemTitle); }else { NSLog(@"User is not playing a song"); }
As always this is just a small code snippet to get you started. There are situations for instance, where the user can have a now playing item that has no title (strange I know). So as always you will have to handle these edge cases appropriately.
MapKit
1Location, Location, Location is all the rage at the moment. Where did someone last update their social network status ? Where is the nearest restaurant ? Where do I need to be in 5 mins ? the possibilities for location services are (nearly) endless. But what is a location ? well put simple it is just a co-ordinate value (longitude and latitude), and in the case of iPhone, it is in reference to the earth. But lets be honest, displaying -122.03, 37.33 in your sexy new iPhone application is not very exciting now is it ? That is were MKMapView comes in.
MKMapView allows you to display a Map on your iPhone’s screen. MKMapView is just a subclass of UIView so you can treat it like one. You can ether use it as a subview (such as on a profile page), or make it go full screen (using it with its own view controller) such as when you are viewing a map to get directions.
So like UIView, to create a MKMapView you need to initialise one with a frame:
MKMapView *mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0,0,320,480)];
Unlike UIView, you will also want to become the delegate of MKMapView.
mapView.delegate = self;The delegate methods for MKMapView include ones that inform you about when the map starts and finishes to load, in addition to if it fails to load at all. (These delegate methods are very similar to UIWebView if you have used that).
- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView; - (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView; - (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error;
In addition to the loading of the Map, the delegate also provides call backs for when the map’s region changes, it also provides callbacks regarding annotation information (more on that later).
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated; - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated;
After you have created a MKMapView, you will want to show a specific region on the map. The method to do this is:
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated;
This introduces a new MapKit data type MKCoordinateRegion. This is the centre point (that you want to show) expressed as a co-ordinate, and then the distance (span) around this co-ordinate that you want to be shown. In essence the span is the zoom level. The bigger the span, the bigger the area that is displayed on the map.
To make a new region you will need to use:
MKCoordinateRegionMake(CLLocationCoordinate2D centerCoordinate, MKCoordinateSpan span);
And thus you will probably have some code that looks like this:
CLLocationCoordinate2D coordinate; coordinate.latitude = -122.03; coordinate.longitude = 37.33; MKCoordinateSpan span = MKCoordinateSpanMake(0.003, 0.003); [mapView setRegion:MKCoordinateRegionMake(coordinate, MKCoordinateSpanMake(0.003, 0.003)) animated:YES];
After you have focused in on a specific region of the map, you will more than likely want to annotate a given point. To do this you need to create an object that conforms to the (informal) MKAnnotation protocol, and in particular implements the CLLocationCoordinate2D coordinate property.
e.g.
@interface MCSMMapAnnotation : NSObject { } @property (nonatomic) CLLocationCoordinate2D coordinate; @end
Then you just need to create an instance of this model object, and then add it to the map as an annotation:
MCSMMapAnnotation *annotation = [[MCSMMapAnnotation alloc] init]; CLLocationCoordinate2D annotationCoordinate; coordinate.latitude = -122.03; coordinate.longitude = 37.33; annotation.coordinate = annotationCoordinate; [self.mapView addAnnotation:annotation];
And that is it, nice and simple.
One more thing …
Before I mentioned that there are delegate methods regarding the annotations on the Map View. One that is particular useful is:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews;
Why is it useful ? Well if you didn’t notice, doing the above means that the annotation(s) just appear on the screen and they do not drop on. So in this delegate method, you can move the annotations off screen and then put them back to where MapKit said they should be.
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews{ //The final (correct) position of the annotation CGRect finalFrame; //The position we will drop the annotation from CGRect offScreenFrame; for(UIView *annotationView in annotationViews) { //MapKit has worked out the annotations final position so store it finalFrame = annotationView.frame; //We just want to move the annotation, so it is just above the top of the visible screen offScreenFrame = CGRectMake(finalFrame.origin.x,(finalFrame.size.height *-1) , finalFrame.size.width, finalFrame.size.height); //Set and therefore move the annotation to the off screen position annotationView.frame = offScreenFrame; //Set up an animation block to animate the drop [UIView beginAnimations:@"AnimateAnnotation" context:NULL]; [UIView setAnimationDuration:1.0]; //Set the final frame, to be the frame that map kit originally calculated annotationView.frame = finalFrame; [UIView commitAnimations]; } }
This is just a brief overview of what you can do with MapKit. The most obvious thing you can do, is to add more than one annotation to the map, and the above animation trick is designed to work with this. One feature you may want to do if your application heavily relies upon maps, is to create your own custom pins (that don’t even need to look like pins), to point out specific points on the map.
Simple UIView based Animations on the iPhone
1Although for complex animation sequences on the iPhone you need to use either the OpenGL or Apple’s very own Core Animation Framework, a lot of simple animations can be achieved with the methods found in the UIView class. All these animations are actually built upon Core Animation, but they have been wrapped up for you to use with very little code.
All animations triggered by the UIView class happen within a animation block.
To start an animation block you use the UIView Class method:
+ (void)beginAnimations:(NSString *)animationID context:(void *)context;
The animation ID is used in delegate call backs for such things as the beginning and end of animation blocks. The context is an additional piece of information that can be passed through.
As both of these parameters are optional, you comenly see:
[UIView beginAnimations:@"" context:NULL];
To commit these animations, and therefore end the animation block, you need to use the UIView class method:
+ (void)commitAnimations
So what types of animations can you do ? well quite a few.
You can animate views by:
- Changing their alpha
- Changing there size
- Changing there location
In a single animation block you can animate multiple views, and you are also able to nest animation blocks.
So lets do a common example. When a UITableViewCell is being edited you often want to make a UILabel’s (label) alpha change to 0 so it is hidden:
[UIView beginAnimations:@"" context:NULL];
[label setAlpha:(editing ? 0.0 : 1.0)];
[UIView commitAnimations];
For those new to C programming:
(editing ? 0.0 : 1.0)
Is equivlant to:
if(editing)
{
0.0;
}else{
1.0;
}
So for the second example we also have a UILabel (label), that we want to grow when the animation code is run. We also want this animation to last 2 seconds, so the user can admire our work. In addition to this, we want the animation to “ease in”. This is known as the animation curve. The animation curves that are available are
- ease in (slow then fast)
- ease out (fast then slow)
- ease in and out (slow, fast, slow)
- linear (constant speed)
We would do this animation using the following animation block:
[UIView beginAnimations:@"" context:NULL];
//The new frame size
[label setFrame: CGRectMake(0,0,320,100)];
//The animation duration
[UIView setAnimationDuration:2.0];
[UIView setAnimationDelay: UIViewAnimationCurveEaseIn];
[UIView commitAnimations];
This is just a brief overview of the animations you can do using the UIView class, but it should be enough to get you started. The UIView methods also include delegate call backs for when an animation starts and end. For more information see Apple’s Documentation.
If the UIView class does not have what you need, you will probably need to use the Core Animation framework. While using this framework is not trivial, it is not as hard as using OpenGL (which is used commonly for 3D games), and you can build some fantastic animations using it.
NSUserDefaults (Preferences)
0Not everybody likes their applications to behave in the same way, and this is why the majority of Mac and iPhone applications have preferences. The default way to handle these preferences is using the class NSUserDefaults.
NSUserDefaults are stored on the file system as .plist, which is simply an XML document. The way you access NSUserDefaults programatically is very much like accessing an NSMutableDictionary, that is by using keys.
Setting User Defaults
NSUserDefaults can be a bool, float, integer or an object. So if you wanted to set the autosave option (which is a BOOL) for your application to be YES, you would write:
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"AutoSave"];
Moreover if you wanted to set the person’s name to ObjColumnist you would write:
[[NSUserDefaults standardUserDefaults] setObject:@"ObjColumnist" forKey:@"PersonName"];
Reading User Defaults
Reading the user defaults is just as easy as setting them. If we wanted to retrieve the 2 values we stored above, you would simply write:
NSString *name = [ [NSUserDefaults standardUserDefaults] stringForKey:@"PersonName"]; BOOL autoSave = [ [NSUserDefaults standardUserDefaults] boolForKey:@"AutoSave"];
You should also notice that there is a convenience method for retrieving a string for a key, even though we set it as an object.
It is also important to be aware that if you attempt to retrieve a numeric value such as an integer, integerForKey: for a non existent key, you will get the value 0. This is obviously an issue if 0 would trigger a certain preference in you application.
Setting the default user defaults
So you can now set and read the user defaults, but how do you assign their default values ?
The answer is registerDefaults:
This method is usually called in the initialize (class) method of a given application’s AppController. The parameter for this method is an NSDictionary. The code below sets the default preference for the AutoSave option to YES, and the person’s name to @”unknown”.
NSMutableDictionary *defaults=[NSMutableDictionary dictionary]; [defaults setObject:[NSNumber numberWithBool:YES] forKey:@"AutoSave"]; [defaults setObject:@"unknown" forKey:@"PersonName"]; [[NSUserDefaults standardUserDefaults] registerDefaults: defaults];
One thing that Cocoa does for you automatically without needing any extra code, is that it only saves to the .plist the values that are different to the defaults values, that were registered using registerDefaults: . This means that if the user does not change any of their default preference settings, there will no be a preference file created.
Note: The code above just stores the preference for the AutoSave option, you still have to create the code to manage the saving of data yourself.
Helper Objects (Delegate and DataSource)
6Helper Objects are used throughout Cocoa and CocoaTouch, and usually take the form of a delegate or dataSource. They are commonly used to add functionality to an existing class without having to subclass it.
In software engineering, the delegation pattern is a technique where an object outwardly expresses certain behaviour but in reality delegates responsibility for implementing that behavior to an associated object in an Inversion of Responsibility. The delegation pattern is the fundamental abstraction that underpins composition (also referred to as aggregation), mixins and aspects.
http://en.wikipedia.org/wiki/Delegation_pattern
The most common use of helper objects in iPhone development is when using UITableViews. When you instantiate a UITableViewController this class is automatically assigned to be the delegate and dataSource of the table view it holds.
self.tableView.delegate=self; self.tableView.dataSource=self;
The UITableView then calls the appropriate delegate method when it needs information, such as:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
This ask the delegate, what is the height for the row at a given index path, so in your UITableViewController (the delegate) you would write the following, if you wanted the row to be 44 pixels high (the default).
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 44; }
Writing your own delegate.
Most delegates are declared as a protocol, so the compiler knows that the delegate implements the required methods.
Example:
You have created a subclass of UITextView (e.g. OBCTextView), that you use in your application to enter both Tweets and SMS. You want to use the same class for both, BUT tweets have a character limit of 140 and SMS have a character limit of 160 (we will pretend that we are still in the 90s).
So we need to create a delegate that asks for the character limit.
- (NSInteger)charachterLimitForTextView:(OBCTextView *)textView;
As mentioned before this should be declared as a protocol
@protocol OBCTextViewDelegate - (NSInteger) charachterLimitForTextView: (OBCTextView *)textView; @end
The OBCTextView would call this method like so:
- (void)askDelegateForCharachterLimit{ NSInterger charachterLimit = [delegate charachterLimitForTextView:self]; //Do something with character limit }
In our SMS view controller we would declare that we implement the delegate protocol.
@interface OBCSMSViewController : UIViewController < OBCTextViewDelegate > { } @end
And then in the implementation file you would implement the following method
- (NSInteger) charchterLimitForTextView: (OBCTextView *)textView{ return 160; }
This method is required for OBCTextView to work so we should also add the key word @required to the protocol
@protocol OBCTextViewDelegate @required - (NSInteger)charchterLimitForTextView:(OBCTextView *)textView; @end
Optional methods
In your delegate, you may also want to implement optional methods. So for this example we will ask for the text color. You can specify that a method is optional using @optional.
@protocol OBCTextViewDelegate @required - (NSInteger)charachterLimitForTextView:(OBCTextView *)textView; @optional - (UIColor *)textViewColorForTextView:(OBCTextView *)textView; @end
As it is optional the delegate class (helper object), does not have to implement it. If it does not implement it, and we attempt to call it, the application will crash. So how do we know if we should call the method or not ? The answer is respondsToSelector.
respondsToSelector allows you to ask an object at run time, if it implements a given selector. So for our example:
- (void)getTextColor { if( [delegate respondsToSelector: @selector(textViewColorForTextView:)] ) { UIColor *textColor = [delegate textViewColorForTextView:self]; //Do something with the text color } }
if your class wants to return a textview color, then you simply implement the method.
- (NSInteger)textViewColorForTextView:(OBCTextView *)textView{ return [UIColor redColor]; }
So that finishes off this post on helper objects, and how you can create your own. As always this example was a simple one to make it easy to follow. You could probably implement the above using setter methods.