In an update I have planned for one of my apps, I had a need to develop a 5-star rating view so people could rate items they make with the app.
As I was working on this control, I thought this might make a good tutorial – especially since it would fit in well with our previous tutorial on how to rate babes with Facebook ;]
So in this tutorial, we’ll go through a short example of making a 5-star rating control and in the process we’ll learn the ropes of creating custom UIViews in general.
Diving In
Let’s dive right in by creating a new View-based application, named CustomView. Go to File\New File…, choose Objective-C class, choose Subclass of UIView, and click Next. Name the file RateView, and click Finish.
Next, let’s fill in our header file. Replace the contents of RateView.h with the following:
#import <UIKit/UIKit.h> @class RateView; @protocol RateViewDelegate - (void)rateView:(RateView *)rateView ratingDidChange:(float)rating; @end @interface RateView : UIView { UIImage *_notSelectedImage; UIImage *_halfSelectedImage; UIImage *_fullSelectedImage; float _rating; BOOL _editable; NSMutableArray *_imageViews; int _maxRating; int _midMargin; int _leftMargin; CGSize _minImageSize; id <RateViewDelegate> _delegate; } @property (retain) UIImage *notSelectedImage; @property (retain) UIImage *halfSelectedImage; @property (retain) UIImage *fullSelectedImage; @property float rating; @property BOOL editable; @property int maxRating; @property (assign) id <RateViewDelegate> delegate; @property int leftMargin; @end |
First we set up a delegate so we can notify our view controller when the rating changes. We could have done this via blocks or a target/selector as well, but I thought this was simpler.
Next, we set up a bunch of instance variables:
- Three UIImages to represent not selected, half selected, and fully selected.
- A variable to keep track of our current rating.
- A boolean to keep track of whether this view should be editable or not. For example, sometimes we may wish to just display a rating without letting the user edit it.
- An array to keep track of the image views we’ll have as children of this view. Note we could have implemented this by just drawing the images in drawRect, but this was simpler (albeit slower performance).
- The maximum value for a rating. This actually allows us to support other numbers of stars than 5 – for example, maybe we want 3 stars, or 10 stars? Also note the minimum is an assumed 0.
- Variables to keep track of spacing in case our user wants to change it: the margin between images, the left margin, and a minimum image size.
- Finally a variable to keep track of our delegate.
We then create properties for each of these, and that’s it!
Initialization and Cleanup
Next, we add in the boilerplate code for the construction of our classes. Add this to the beginning of RateView.m:
#import "RateView.h" @implementation RateView @synthesize notSelectedImage = _notSelectedImage; @synthesize halfSelectedImage = _halfSelectedImage; @synthesize fullSelectedImage = _fullSelectedImage; @synthesize rating = _rating; @synthesize editable = _editable; @synthesize maxRating = _maxRating; @synthesize delegate = _delegate; @synthesize leftMargin = _leftMargin; #pragma mark Main - (void)baseInit { _imageViews = [[NSMutableArray array] retain]; _notSelectedImage = nil; _halfSelectedImage = nil; _fullSelectedImage = nil; _rating = 0; _editable = NO; _maxRating = 0; _leftMargin = 0; _midMargin = 5; _minImageSize = CGSizeMake(5, 5); self.backgroundColor = [UIColor clearColor]; } - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self baseInit]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { [self baseInit]; } return self; } - (void)dealloc { [_notSelectedImage release]; _notSelectedImage = nil; [_halfSelectedImage release]; _halfSelectedImage = nil; [_fullSelectedImage release]; _fullSelectedImage = nil; [_imageViews release]; _imageViews = nil; [super dealloc]; } |
This is all pretty boilerplate stuff where we initialize our instance variables to default values. Note that we support both initWithFrame and initWithCoder so that our view controller can add us via a XIB or programatically.
Refreshing Our View
Pretend that the view controller has set up our instance variables with valid images, ratings, maxRating, etc, and that we’ve created our UIImageView subviews. Well we need to write a method to refresh the display based on the current rating. Add this method to the file next:
- (void)refresh { for(int i = 0; i < _imageViews.count; ++i) { UIImageView *imageView = [_imageViews objectAtIndex:i]; if (_rating >= i+1) { imageView.image = _fullSelectedImage; } else if (_rating > i) { imageView.image = _halfSelectedImage; } else { imageView.image = _notSelectedImage; } } } |
Pretty simple stuff here – we just loop through our list of images and set the appropriate image based on the rating.
Layout Subviews
Probably the most important function in our file is the implementation of layoutSubviews. This function gets called whenever the frame of our view changes, and we’re expected to set up the frames of all of our subviews to the appropriate size for that space. So add this function next:
- (void)layoutSubviews { [super layoutSubviews]; if (_notSelectedImage == nil) return; float desiredImageWidth = (self.frame.size.width - (_leftMargin*2) - (_midMargin*_imageViews.count)) / _imageViews.count; float imageWidth = MAX(_minImageSize.width, desiredImageWidth); float imageHeight = MAX(_minImageSize.height, self.frame.size.height); for (int i = 0; i < _imageViews.count; ++i) { UIImageView *imageView = [_imageViews objectAtIndex:i]; CGRect imageFrame = CGRectMake(_leftMargin + i*(_midMargin+imageWidth), 0, imageWidth, imageHeight); imageView.frame = imageFrame; } } |
We first bail if our notSelectedImage isn’t set up yet.
But if it is, we just do some simple calculations to figure out how to set the frames for each UIImageView.
The images are laid out like the following to fill the entire frame: left margin, image #1, mid margin, …, image #n, left margin.
So if we know the full size of the frame, we can subtract out the margins and divide by the number of images to figure out how wide to make each of the UIImageViews.
Once we know that, we simply loop through and update the frames for each UIImageView.
Setting properties
Since we don’t know the order in which the view controller is going to set our properties (especially since they could even change mid-display), we have to be careful about how we construct our subviews, etc. This is the approach I took to solve the problem, if anyone else has a different approach I’d love to hear!
- (void)setMaxRating:(int)maxRating { _maxRating = maxRating; // Remove old image views for(int i = 0; i < _imageViews.count; ++i) { UIImageView *imageView = (UIImageView *) [_imageViews objectAtIndex:i]; [imageView removeFromSuperview]; } [_imageViews removeAllObjects]; // Add new image views for(int i = 0; i < maxRating; ++i) { UIImageView *imageView = [[[UIImageView alloc] init] autorelease]; imageView.contentMode = UIViewContentModeScaleAspectFit; [_imageViews addObject:imageView]; [self addSubview:imageView]; } // Relayout and refresh [self setNeedsLayout]; [self refresh]; } - (void)setNotSelectedImage:(UIImage *)image { [_notSelectedImage release]; _notSelectedImage = [image retain]; [self refresh]; } - (void)setHalfSelectedImage:(UIImage *)image { [_halfSelectedImage release]; _halfSelectedImage = [image retain]; [self refresh]; } - (void)setFullSelectedImage:(UIImage *)image { [_fullSelectedImage release]; _fullSelectedImage = [image retain]; [self refresh]; } - (void)setRating:(float)rating { _rating = rating; [self refresh]; } |
The most important method is the setMaxRating method – because this determines how many UIImageView subviews we have. So when this changes, we remove any existing image views and create the appropriate amount. Of course, once this happens we need to make sure layoutSubviews and refresh is called, so we call setNeedsLayout and refresh.
Similarly, when any of the images or the rating changes, we need to make sure our refresh method gets called so our display is consistent.
Touch Detection
Finally the fun stuff: touch detection! Add this to the bottom of your file:
- (void)handleTouchAtLocation:(CGPoint)touchLocation { if (!_editable) return; _rating = 0; for(int i = _imageViews.count - 1; i >= 0; i--) { UIImageView *imageView = [_imageViews objectAtIndex:i]; if (touchLocation.x > imageView.frame.origin.x) { _rating = i+1; break; } } [self refresh]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint touchLocation = [touch locationInView:self]; [self handleTouchAtLocation:touchLocation]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint touchLocation = [touch locationInView:self]; [self handleTouchAtLocation:touchLocation]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [_delegate rateView:self ratingDidChange:_rating]; } |
Our main code is in handleTouchAtLocation, the rest of the methods either call this or notify our delegate that something has changed.
In handleTouchAtLocation, we simply loop through our subviews (backwards) and compare the x coordinate to that of our subviews. If the x coordinate is greater than the current subview, we know the rating is the index of the subview+1.
Note this doesn’t support “half-star” ratings but could easily be modified to do so.
Using the 5 Star Rating View
Open up CustomViewViewController.h and replace the contents with the following:
#import <UIKit/UIKit.h> #import "RateView.h" @interface CustomViewViewController : UIViewController <RateViewDelegate> { RateView *_rateView; UILabel *_statusLabel; } @property (retain) IBOutlet RateView *rateView; @property (retain) IBOutlet UILabel *statusLabel; @end |
Then double click CustomViewViewController.xib, double click the View, and drag a UIView and a UILable from the library inside the view. Then resize it the views like the following:
Click on the UIView you added, and go to the fourth tab in the Inspector (the Identity Inspector), and set the class to RateView. Then control-drag from the File’s Owner to the Rate View and Label, and connect them each to their corresponding outlets.
Save the XIB, return to CustomViewViewController.m, and make the following changes:
// Under @implementation @synthesize rateView = _rateView; @synthesize statusLabel = _statusLabel; // In dealloc AND viewDidUnload self.rateView = nil; self.statusLabel = nil; // Replace viewDidLoad with the following: - (void)viewDidLoad { [super viewDidLoad]; _rateView.notSelectedImage = [UIImage imageNamed:@"star_empty.jpg"]; _rateView.halfSelectedImage = [UIImage imageNamed:@"star_half.jpg"]; _rateView.fullSelectedImage = [UIImage imageNamed:@"star_full.jpg"]; _rateView.rating = 0; _rateView.editable = YES; _rateView.maxRating = 5; _rateView.delegate = self; } // Add to bottom - (void)rateView:(RateView *)rateView ratingDidChange:(float)rating { _statusLabel.text = [NSString stringWithFormat:@"Rating: %f", rating]; } |
What about the star images, you might ask? Well feel free to snag a copy of these kermit stars made by my lovely wife, or use your own!
Once you have the images, add them to your project, compile and run, and if all works well you should be able to happily rate away!
Where To Go From Here?
Here is a sample project with all of the code from the above tutorial.
Note that you can also set the other options on the view like dynamically setting the rating or making it editable or not. And feel free to customize it for your own needs as well!
What I didn’t cover was custom drawing inside the drawRect for the UIView – a very common case as well. Perhaps I’ll cover that in a future tutorial!
Let me know how you’ve been using custom UIViews in your apps!
Category: iPhone
![L'Escapadou took all the stars, so I was just left with this Kermit ;] L'Escapadou took all the stars, so I was just left with this Kermit ;]](http://d1xzuxjlafny7l.cloudfront.net/wp-content/uploads/2010/07/RateView.jpg)








Interesting approach. Wondering why you didn’t base this on UIControl…? Maybe this control is simple enough that it was simpler this way. UIControl baseclass would make target linking in IB possible though…
Nice guide.
I think this might be one of those cases where doing it in Javascript, using a UIWebView and parsing the debug data is a lot easier.
That is a lot of variables!
Your blog is the best blog I have ever read!
I have been programming since long but I am new to iPhone development however your blog and tutorial is the best, I am so much thankful to you.
-best
Since I got a freelance to do a huge iPad app (shame: and I was only used to Cocos, not UIKit), this article fits perfectly my needs!
As always, thanks!
@Dad: Hm I guess that just didn’t occur to me to go that route. I’ll have to try that out sometime. Do you make a lot of custom UIControls?
@ManiacDev: Yeah perhaps! I’m still newish to web dev so this way is easier for me ATM though :] Out of curiosity, under what circumstances do you use web views for UI in your projects? It looks like it would be pretty useful if you need to get dynamic UI from a server for example…
@Ashu: Thanks so much for the kind words!
@Alfred: Awesome, congrats on your freelance job, good luck! :]
Nice work!
Can you help me to get out how to add Admob to the cocos2d.I follow the essay with http://pocketworx.com/?p=107.But i cannot click the Admob ad. I add the Admob ad to a CClayer but i cannot set the visible of the CClayer.
@Tony: I’d suggest trying the admob SDK forums or the cocos2D forums for this.
Hey Ray,
Very nice work!
I want to make a simple app where I can double-click on an image and then bring up another image overlayed on it (or perhaps just a UIView like your Rating example). The other thing that I need is to be able to zoom in and out at will.
I was playing around with the example 1_TapToZoom which is on the Apple website.
I was hoping the superview coordinates would always give me a reliable result any any zoom level, but that does not seem to be the case. If you put this method in RootViewController.m you can see what I mean.
Any feedback would be greatly appreciated!
Keep up the good work!
- Nick
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
// single tap does nothing for now
//CGPoint pt = [gestureRecognizer locationInView:imageScrollView];
CGPoint pts = [gestureRecognizer locationInView:[imageScrollView superview]];
// It seems like the superview location should never changes but the local view does
printf(“lastZoomScale: %f\n”, lastZoomScale);
float x = pts.x;
float y = pts.y;
CGRect myRect;
myRect.origin.x = 160 – 50;
myRect.origin.y = 229 – 50;
myRect.size.width = 100;
myRect.size.height = 100;
printf (“pt.x: %f pt.y: %f\n”, x, y);
if(CGRectContainsPoint(myRect, pts) == TRUE){
printf(“INSIDE!!\n”);
} else { //user didn’t tap inside image}
printf(“NOT inside\n”);
}
CGRect myBounds = [[self view] bounds];
}
@Nick: My initial thoughts are why do the collision detection yourself when the OS can do it for you? If you put your image inside a UIImageView, and put the UIImageView inside a UIScrollView to enable zooming, you can put a UIGestureRecognizer on the UIImageView to detect a tap, etc.
Hi,
Iam new to iphone development.
I want use your rating control in a table view.
Let me know is it possible to integrate your control in table view.
Ravi.
@Ravi: Yes, this control can be used in a table view. Specifically, you’ll need to make a custom UITableViewCell, and then add this view as a subview to the cell.
Hi Ray,
I have a q: is it possible to do the same, but ease the view layout management by using a XIB file for it somehow, but still make the view reusable?
@Sagi: Yes, it is! You have two options:
1) If you just need the view in a single view controller, add a UIView to your view controller, then in the Identity Inspector change its name to the name of your custom view class. You can declare IBOutlets in your view class, and connect them to the UI elements that you add to the view in Interface builder.
2) If you want to use the view in multiple view controllers, you should make a XIB just for that one view. Then when you want to add the view to a view controller, you’ll have to programmatically load the XIB containing the view with loadNibNamed:owner:options (passing in self as owner). It will return to you an array of elements inside your XIB – you can look through there for your view, then add it to your view controller..
hey,
i m new in iphone development and your blog is really very helpful for me.
thanx for it.
I have a que:
pls tell me how we can add rating system in table view?
@shilpa: Funny you asked that, since I just had an example of doing that in the class I just ran at 360iDev. Check out the source code for Section 3 here:
http://www.raywenderlich.com/360iDev
I really love your site, great examples and tutorials. Thanks
Lars
@Ortim: Thanks man and welcome! :]
Hi Ray,
actually I tried to do a custom view. But when I try to IBOutlet my custom view I get this:
Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key progressView.’
But when I use ur View it works. Do u might have an hint for that?
Nick
Nevermind. I found the problem. Maybe good for others. When u use a TabBar FOR SOME reasons IB reset the UIViewController for the first to a generic one. After I added a CustomView to my UIViewController no idea how that happened.
That caused all the trouble. So if anyone is facing that trouble check your TabBar.
@Nick: Awesome, glad you got it working, and thank you for posting the solution here so that others may benefit!
hey sir that was really a nice blog!!!
but i tried the same code without any change but its not working for half stars!
so i modified some thing in handle touch at location function but its actually modifying only first star and for rest all stars its works again same as if its a full star !!! could you please help me with this!!!
ohh k sir its working now i changed loops actually!!
for(int i = _imageViews.count – 1; i >= 0; i–)
{
UIImageView *imageView = [_imageViews objectAtIndex:i];
if (touchLocation.x > imageView.frame.origin.x && touchLocation.x imageView.center.x && touchLocation.x < completedistance )
{
_rating= i+1;
break;
}
}