Float Rating View in Swift

Way back in January at Tab Payments we needed a simple iOS rating view so that users could rate their server and leave a review. After some searching I found a great tutorial by Ray Wenderlich, whose site has some of the best iOS tutorials I've come across. The only problem was it didn't do float ratings (to show averages) and I didn't like how it needed a 3rd image for half ratings.

FloatRatingView

FloatRatingView is rather simple to use, returns the live float rating to a delegate and only needs 2 images to setup (an empty and full rating image). This was accomplished using the CALayerclass and its optional masklayer. For full details on how this control works, be sure to check out the tutorial linked above.

CALayer and Masks

Behind every UIView there is a CALayer class which manages the image based content such as animations, colour and alpha channels. The maskproperty of CALayer is completely optional and lets you control how much of the background layer is shown by adjusting the mask’s frame.

So while the original control used one set of UIImageViews to show the current rating, FloatRatingView actually has 2 sets of UIImageViews laid directly on top of each other. One to show the empty rating images and one on top to show the full rating images.

The full rating image is then masked to only partially show its image for floating point ratings. All of this is done in a refresh() function which responds to user touches and layout changes. The refresh() function iterates through all the full image views and does 3 things:

If the rating is greater than the current full image view index, we show the full image view.

If the rating is less than the current full image view index, we hide the full image view.

If the rating is a float value somewhere between the current and next full image view index, we create a mask to partially hide it.

func refresh() {for i in 0..<self.fullImageViews.count {let imageView = self.fullImageViews[i // Show the full rating image if rating is greater than index if self.rating>=Float(i+1) { imageView.layer.mask = nil imageView.hidden = false } // Use a mask CALayer to partially show the full rating imageelse if self.rating>Float(i) && self.rating<Float(i+1) {// Create a mask layer for full imagelet maskLayer = CALayer() // Calculate the mask width as a fraction of the full imagelet maskWidth = CGFloat(self.rating—Float(i)) * imageView.frame.size.widthlet maskHeight = imageView.frame.size.height // Set the mask frame maskLayer.frame = CGRectMake(0, 0, maskWidth , maskHeight ) // The mask layer needs a colour to show the full image maskLayer.backgroundColor = UIColor.blackColor().CGColor // Set the full image view's mask and unhide it imageView.layer.mask = maskLayer imageView.hidden = false } // Hide the full rating image if rating is less than indexelse { imageView.layer.mask = nil imageView.hidden = true } }}

Originally I had used masks on all the full rating images but found that performance suffered when showing many FloatRatingViews at the same time. Setting the hidden property and only using masks when needed ended up being the way to go.

Swift Property Observers

One convenient Swift feature used in FloatRatingView is Property Observers. Whenever a property’s value will be changed or has been changed, we can respond to these events by overriding their respective observers, willSet and didSet. In this case, whenever an empty or full image is set for FloatRatingView we can respond by setting the empty and full UIImageView images.

/** Sets the full image that is overlayed on top of the empty image. Should be same size and shape as the empty image. */var fullImage: UIImage? {didSet {// Update full image viewsfor imageView in self.fullImageViews { imageView.image = fullImage } self.refresh() }}

This is all that’s required to initialize everything. The full rating image views are laid on top of the empty rating image views and are shown or hidden depending on the rating. The function refresh() is then called to update the image view masks.

Some Notes on Swift

A couple of small things I came across writing FloatRatingView. In Swift, you specify an optional value by placing a question mark (?) right after it, or a forced unwrapped value with an exclamation mark (!). This means you need to watch your spacing when writing equality statements or you’ll get an error.

For example

// This works fineif rating>=i {}

// But this won't compileif rating!=i {}

While the above example looks innocent enough, the compiler will think the exclamation mark in the 2nd if-statement is a forced unwrapping. This means it should be written like so:

// Mind the spacingif rating != i { // Do something}

Another example

// 0 is not an optional valuelet rating = i==0? 0:5

This statement looks like something any Objective-C programmer would write and not think twice about. This also gives an error however, as the compiler looks at 0? and thinks it’s for an optional value. Again, just need to be mindful of the spacing whenever you’re using question or exclamation marks.

// What we actually needlet rating = i==0 ? 0:5

That’s all for now!

There are some layout functions that aren't covered here but are fully explained in the Ray Wenderlich tutorial. Feel free to use or fork FloatRatingView at anytime or even send a pull request!