UIView Tutorial for iOS: How To Make a Custom UIView in iOS 5: A 5 Star Rating View

Ray Wenderlich

This post is also available in: Russian

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 :)

Update 2/17/12: Fully updated for iOS 5.

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 UIView tutorial – especially since it would fit in well with our previous tutorial on how to rate babes with Facebook ;]

So in this UIView 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 project with the iOS\Application\Single View Application template and name it CustomView. Make sure the Device Family is set to iPhone, Use Storyboard and Use Automatic Reference Counting are checked, and click Next.

Project settings in Xcode

Now create a new file with the iOS\Cocoa Touch\Objective C class template. Enter RateView for the Class name, and UIView for Subclass of.

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
 
@property (strong, nonatomic) UIImage *notSelectedImage;
@property (strong, nonatomic) UIImage *halfSelectedImage;
@property (strong, nonatomic) UIImage *fullSelectedImage;
@property (assign, nonatomic) float rating;
@property (assign) BOOL editable;
@property (strong) NSMutableArray * imageViews;
@property (assign, nonatomic) int maxRating;
@property (assign) int midMargin;
@property (assign) int leftMargin;
@property (assign) CGSize minImageSize;
@property (assign) id <RateViewDelegate> delegate;
 
@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 properties:

  • 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.

Initialization and Cleanup

Next, we add in the boilerplate code for the construction of our classes. Replace RateView.m with the following:

#import "RateView.h"
 
@implementation RateView
 
@synthesize notSelectedImage = _notSelectedImage;
@synthesize halfSelectedImage = _halfSelectedImage;
@synthesize fullSelectedImage = _fullSelectedImage;
@synthesize rating = _rating;
@synthesize editable = _editable;
@synthesize imageViews = _imageViews;
@synthesize maxRating = _maxRating;
@synthesize midMargin = _midMargin;
@synthesize leftMargin = _leftMargin;
@synthesize minImageSize = _minImageSize;
@synthesize delegate = _delegate;
 
- (void)baseInit {
    _notSelectedImage = nil;
    _halfSelectedImage = nil;
    _fullSelectedImage = nil;
    _rating = 0;
    _editable = NO;    
    _imageViews = [[NSMutableArray alloc] init];
    _maxRating = 5;
    _midMargin = 5;
    _leftMargin = 0;
    _minImageSize = CGSizeMake(5, 5);
    _delegate = nil;    
}
 
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self baseInit];
    }
    return self;
}
 
- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self baseInit];
    }
    return self;
}
 
@end

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 < self.imageViews.count; ++i) {
        UIImageView *imageView = [self.imageViews objectAtIndex:i];
        if (self.rating >= i+1) {
            imageView.image = self.fullSelectedImage;
        } else if (self.rating > i) {
            imageView.image = self.halfSelectedImage;
        } else {
            imageView.image = self.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 (self.notSelectedImage == nil) return;
 
    float desiredImageWidth = (self.frame.size.width - (self.leftMargin*2) - (self.midMargin*self.imageViews.count)) / self.imageViews.count;
    float imageWidth = MAX(self.minImageSize.width, desiredImageWidth);
    float imageHeight = MAX(self.minImageSize.height, self.frame.size.height);
 
    for (int i = 0; i < self.imageViews.count; ++i) {
 
        UIImageView *imageView = [self.imageViews objectAtIndex:i];
        CGRect imageFrame = CGRectMake(self.leftMargin + i*(self.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 < self.imageViews.count; ++i) {
        UIImageView *imageView = (UIImageView *) [self.imageViews objectAtIndex:i];
        [imageView removeFromSuperview];
    }
    [self.imageViews removeAllObjects];
 
    // Add new image views
    for(int i = 0; i < maxRating; ++i) {
        UIImageView *imageView = [[UIImageView alloc] init];
        imageView.contentMode = UIViewContentModeScaleAspectFit;
        [self.imageViews addObject:imageView];
        [self addSubview:imageView];
    }
 
    // Relayout and refresh
    [self setNeedsLayout];
    [self refresh];
}
 
- (void)setNotSelectedImage:(UIImage *)image {
    _notSelectedImage = image;
    [self refresh];
}
 
- (void)setHalfSelectedImage:(UIImage *)image {
    _halfSelectedImage = image;
    [self refresh];
}
 
- (void)setFullSelectedImage:(UIImage *)image {
    _fullSelectedImage = image;
    [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 (!self.editable) return;
 
    int newRating = 0;
    for(int i = self.imageViews.count - 1; i >= 0; i--) {
        UIImageView *imageView = [self.imageViews objectAtIndex:i];        
        if (touchLocation.x > imageView.frame.origin.x) {
            newRating = i+1;
            break;
        }
    }
 
    self.rating = newRating;
}
 
- (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 {
    [self.delegate rateView:self ratingDidChange:self.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

Now finally time to try this out! Open MainStoryboard.storyboard and go to Editor\Canvas\Show Bounds Rectangles (this will make it easier to see what we’re doing).

Then drag a UIView and a UILabel from the Object Library into the view controller, and resize them to look like the following:

Adding a UIView and a status label

Then click on the UIView you added and go to the Identity Inspector. Set the class to RateView:

Setting the class name for a UIView

Now let’s connect these to outlets. Bring up the Assistant editor, and make sure that ViewController.h is visible. Control-drag from the UIView down between the @interface and @end lines, and connect it to an Outlet named rateView.

Connecting view to an outlet

Repeat this for the label, but connect it to an outlet called statusLabel.

We want to make the View Controller implement the RateViewDelegate protocol, so also import the RateViewHeader and mark it as doing so. At this point ViewController.h should look like the following:

#import <UIKit/UIKit.h>
#import "RateView.h"
 
@interface ViewController : UIViewController <RateViewDelegate>
 
@property (weak, nonatomic) IBOutlet RateView *rateView;
@property (weak, nonatomic) IBOutlet UILabel *statusLabel;
 
@end

Next switch to ViewController.m and make the following changes:

// Replace viewDidLoad with the following:
- (void)viewDidLoad
{
    [super viewDidLoad];
    self.rateView.notSelectedImage = [UIImage imageNamed:@"kermit_empty.png"];
    self.rateView.halfSelectedImage = [UIImage imageNamed:@"kermit_half.png"];
    self.rateView.fullSelectedImage = [UIImage imageNamed:@"kermit_full.png"];
    self.rateView.rating = 0;
    self.rateView.editable = YES;
    self.rateView.maxRating = 5;
    self.rateView.delegate = self;
}
 
// Add to bottom
- (void)rateView:(RateView *)rateView ratingDidChange:(float)rating {
    self.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!

4 out of 5 Stars says Kermit!

Where To Go From Here?

Here is a sample project with all of the code from the above UIView 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 UIView tutorial!

Let me know how you’ve been using custom UIViews in your apps!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments

37 Comments

[ 1 , 2 , 3 ]
  • Hey man!
    Thank you for your nice turorial!

    I have one issue:
    I'm trying to have more than one star rating in one view controller.
    How can I get different actions from these different star ratings?

    Thanks in advance!
    Erick
    erick_rio
  • As a VB developer learning Objective C in a vacuum, this site has been unbelievably helpful. So straight off Thanks.


    Based on the above initial code for marking half stars, and Ray's response I tried my hand at writing something that seemed to work. (I originally had the >1 ratio as the conditional for the Full Star, but I found that .75 had better "action" to it.


    Code: Select all
    - (void)handleTouchAtLocation:(CGPoint)touchLocation {
        if (!self.editable) return;
       
        //The rating starts out as 0 and then builds from there.
        CGFloat newRating = 0;

        //loop through the image collection backwards so if it exits the loop it will have identified the MAX
        for(int i = self.imageViews.count - 1; i >= 0; i--) {
            UIImageView *imageView = [self.imageViews objectAtIndex:i];
       
            CGFloat distance = touchLocation.x - imageView.frame.origin.x;
           
            CGFloat frameWidth = imageView.frame.size.width;
           
            if (distance <= 0){
                //this means that the click was to the left of the frame
               continue;
            }
            if (distance /frameWidth >.75) {
                //If this ratio is >.75 then you are to the right 3/4 of the image or past th image
                newRating = i + 1;
                break;
            } else {
                //you didn't drop out or mark the entire star, so mark it half.
                newRating = i + 0.5;
                break;
            }
        }
       
        self.rating = newRating;
    }



    Hope some one finds this helpful.

    -Steve
    sesproul
  • Hello,

    I modified your code a bit, I don't use usually _underscore variables, and my synthesize looks like this:
    Code: Select all
    @synthesize notSelectedImage, halfSelectedImage,...

    and the setters like this:
    Code: Select all

    - (void)setNotSelectedImage:(UIImage *)image {
        notSelectedImage = image;
        [self refresh];
    }

    - (void)setHalfSelectedImage:(UIImage *)image {
        halfSelectedImage = image;
        [self refresh];
    }


    but I get this exception when I run the program:

    013-08-22 11:54:20.139 CustomView[1168:c07] -[UIView setNotSelectedImage:]: unrecognized selector sent to instance 0x8286940
    2013-08-22 11:54:20.140 CustomView[1168:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIView setNotSelectedImage:]: unrecognized selector sent to instance 0x8286940'

    Can someone please help??
    pseudonym27
  • rwenderlich wrote:This is the official thread to discuss the following blog post: UIView Tutorial for iOS: How To Make a Custom UIView in iOS 5: A 5 Star Rating View



    Hello,

    thanks for nice tutorials!!

    I was always interested, is not this article violating the MVC pattern?

    I have heard in MVC Views can't/shouldn't talk to the model, in this article
    however, the view (5 star view) talks to the model, e.g., it stores some
    data like properties etc. Isn't it?

    Any clarification appreciated. Thanks.
    pseudonym27
  • in layoutSubviews() , float desiredImageWidth = (self.frame.size.width - (self.leftMargin*2) - (self.midMargin*self.imageViews.count)) / self.imageViews.count;
    self.midMargin * (self.imageViews.count - 1)???
    wczmatthew
  • in this tutorial, use delegate mode to handle the touch event, and also refer to use block. Could you show me how to do it. I am a fresh in ios dev
    wczmatthew
  • The main reason for doing this 5 Star Rating tutorial is that it is basic material for Chapter 16 of the iOS 6 By Tutorial book.

    After doing the tutorial by cutting and pasting (only adding some code comments throughout) I ran the app and mechanically works, except it doesn't refresh the kermit stars which remain as not checked.

    I also missed where the setMaxRating: method is called in the app.

    NOTE: The forum currently reports 36 comments, but only shows 4 of 4 in my browser.
    intentionseeker
[ 1 , 2 , 3 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in September: iOS 8 App Extensions!

Sign Up - September

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Tammy Coron
  • Kirill Muzykov

... 49 total!

Update Team

Editorial Team

... 23 total!

Code Team

  • Orta Therox

... 3 total!

Translation Team

  • Heejun Han
  • Jesus Guerra
  • David Xie

... 33 total!

Subject Matter Experts

... 4 total!