At times you may find that you need a control that has not yet been provided for you. In my case I needed a star rating control for iWoman. Ladies will be able to star their love encounters. And the way how such a thing is done nowadays is via selecting between 0 and 5 stars. (If you now ask me to rig it such that your girl can only rate you with 5 stars, then I’ll bitch-slap you)
So I decided to make a UIView that would take UIImages for empty, half-full and full stars. Also I wanted to have some customizability like different numbers of stars and to be able to turn half-full stars on and off. That’s where I created multiple properties so that these values can be set from outside the instance. The steps are always: instance variable (“IVAR”), @property, @synthesize and if the property is a retained object then a line in dealloc.
I made this YouTube video to give you a demonstration and guided tour of DTStarRatingView.
Artwork Required
You could have the graphics for your controls custom designed, but for this example I went to DryIcons to find a nice specimen of a star in the three required states. To use these in an iPhone app the cost is $5 per icon, so that’s extremely affordable. If you fancy one of their pieces as app icon, then it’s slightly more expensive at $300. But $15 for an extended license for 3 icons I see myself paying very happily.
Now a star rating control is not very difficult to make. If you only ever need a static number of icons you can simply click it all together in Interface Builder. But I delight in reusability and so I did all setup and positioning in code. There I have to do the basic setup in awakeFromNib as well as the initWithFrame initializer because this way I am able to both instantiate it from a Xib file as well as from code and it’s always set up correctly.
Getting in Control
Now for making a UIControl out of it. If you check the UIControl documentation, then you see it’s just a descendant of UIView. That means that UIControls can do everything that UIViews can PLUS the target-action mechanism. You could say that UIView is to UIControl as UIImageView is to UIButton. Hooray for object-oriented programming!
So to convert the UIView to a UIControll all I needed was to do two things: 1) change the subclass in the header to be a UIControl instead of a UIView and 2) add one line of code to notified registered targets when the rating value changes.
@interface DTStarRatingView : UIControl { UIImage *emptyStar; UIImage *halfFullStar; UIImage *fullStar; float rating; BOOL allowHalfStars; NSUInteger numberOfStars; UIEdgeInsets edgeInsets; } @property (nonatomic, retain) UIImage *emptyStar; @property (nonatomic, retain) UIImage *halfFullStar; @property (nonatomic, retain) UIImage *fullStar; @property (nonatomic, assign) float rating; @property (nonatomic, assign) BOOL allowHalfStars; @property (nonatomic, assign) NSUInteger numberOfStars; @property (nonatomic, assign) UIEdgeInsets edgeInsets; - (void) updateStars; @end |
In the setRating property I needed to add this line with sendActionsForControlEvents.
#pragma mark Properties - (void) setRating:(float)newRating { if (rating != newRating) { rating = newRating; [self updateStars]; [self sendActionsForControlEvents:UIControlEventValueChanged]; } } |
I was myself astonished how easy it was. This is actually not mentioned clearly in the UIControl documentation, on the first read of it you are bound to read over it. So it was Google who told me the simple answer. Now that I know it, I know how to understand this part from the docs:
When a user touches the control in a way that corresponds to one or more specified events, UIControl sends itself sendActionsForControlEvents:. This results in UIControl sending the action to UIApplication in a sendAction:to:from:forEvent: message.
This actually means that I can call sendActionsForControlEvents myself (being DTStarRatingView) for any kind of control event that I want to trigger, a change in “value” in our case. I previously mentioned all the possible kinds of predefined control events.
We could even go as far as creating 255 of our own kinds of events in the range described by UIControlEventApplicationReserved. Though I have yet to see a good use for this that’s not better served with a delegation mechanism.
A Touch of Bling
Simple changing images between the star states was not enough, an unobtrusive animation goes a long way to making a control look more professional. So I added a CAKeyframeAnimation whenever a change of image is detected. There are two kinds of transformations that you can animate: the CGAffineTransform that’s to be found on the UIView level and the CATransform3D at the layer level. This uses that latter.
// star is a UIImageView with 1 star CAKeyframeAnimation *boundsOvershootAnimation = nil; if (star.image && star.image != newImage) { // animate boundsOvershootAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; CATransform3D startingScale = CATransform3DMakeScale( 1, 1, 1); CATransform3D overshootScale = CATransform3DMakeScale( 1.1, 1.1, 1.0); CATransform3D undershootScale = CATransform3DMakeScale(0.9, 0.9, 1.0); CATransform3D endingScale = CATransform3DIdentity; NSArray *boundsValues = [NSArray arrayWithObjects:[NSValue valueWithCATransform3D:startingScale], [NSValue valueWithCATransform3D:overshootScale], [NSValue valueWithCATransform3D:undershootScale], [NSValue valueWithCATransform3D:endingScale], nil]; [boundsOvershootAnimation setValues:boundsValues]; NSArray *times = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0f], [NSNumber numberWithFloat:0.5f], [NSNumber numberWithFloat:0.9f], [NSNumber numberWithFloat:1.0f], nil]; [boundsOvershootAnimation setKeyTimes:times]; NSArray *timingFunctions = [NSArray arrayWithObjects:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], nil]; [boundsOvershootAnimation setTimingFunctions:timingFunctions]; boundsOvershootAnimation.fillMode = kCAFillModeForwards; } star.image = newImage; if (boundsOvershootAnimation) { [star.layer addAnimation:boundsOvershootAnimation forKey:@"scale"]; } |
Lots of code for this animation but I copy/pasted it from a StackOverflow question without much modification. I only had to correct some syntax errors and iPhone specialities.
There you have it, now you know how to give me 5 stars.
Categories: Recipes
Thanks for good Tut π I refactored the updateStarsFromTouch() so it wasn’t dependent on a hitTest and thus finger doesn’t have to get in way when dragging. something along these lines – Hope it helps…
-(void) updateStarsFromTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint locationInView = [touch locationInView:self];
if (locationInView.x self.bounds.size.width – edgeInsets.right) {
self.rating = 1;
}
else {
CGFloat xPos = locationInView.x – edgeInsets.left;
CGFloat ratingTemp = (ceil(xPos/((sizePerStar.width/2.0)+(widthBetween/2.0)))/2.0)/_numberOfStars;
if (allowHalfStars) round(ratingTemp);
self.rating = ratingTemp;
}
}
Hum code didn’t paste correctly, second try
-(void) updateStarsFromTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint locationInView = [touch locationInView:self];
if (locationInView.x self.bounds.size.width – _edgeInsets.right) {
self.rating = 1;
}
else {
CGFloat xPos = locationInView.x – _edgeInsets.left;
CGFloat ratingTemp = (ceil(xPos/((sizePerStar.width/2.0)+(widthBetween/2.0)))/2.0)/_numberOfStars;
if (_allowHalfStars) round(ratingTemp);
self.rating = ratingTemp;
}
}
nope, code still not right -email me and I’ll send you the files if you’d like… π
Thanks for the info! The project “DTStarRatingView” is mentioned, but I can’t find a link to the source. Am I missing something obvious? π
Thanks
Terry
All the code is there anyway. Just copy and paste the two files.