Displaying stars in your custom UIControl object

So far, you have created a new UIControl subclass named RatingsView in your project. You have also assigned the class of the view object next to the 0.0 label in the Restaurant Detail screen to RatingsView. Henceforth, an instance of the RatingsView class will be known as a ratings view. In this section, you will add some code to the RatingsView class to make a ratings view display stars. Do the following steps:

  1. Click RatingsView.swift in the Project navigator.
  2. Type the following text under the RatingsView class declaration:
let imgFilledStar = Imag
  1. The autocomplete menu will appear. Choose Image Literal, as shown in the following screenshot:

  1. You'll see a placeholder graphic. Double-click it, as shown in the following screenshot:

  1. All the custom graphics in Assets.xcassets should appear, as can be seen in the following screenshot:

  1. Search for and select the red star. This is what the completed line of code should look like:

  1. Type in the following, using Image Literal to assign the graphics for imgHalfStar and imgEmptyStar:
let imgFilledStar = (picture of a filled star)
let imgHalfStar = (picture of a half-filled star)
let imgEmptyStar = (picture of an empty star)
let shouldBecomeFirstResponder = true
var rating:CGFloat = 0.0
var totalStars = 5

  1. This is what your code should look like:

The first three properties, imgFilledStar, imgHalfStar, and imgEmptyStar, are used to store the star images.

shouldBecomeFirstResponder is a Boolean value that is set to true. Later, you will use it in a method that determines whether a ratings view can accept touch events.

rating is used to keep track of the current rating. The number of stars that's drawn will be determined by the value of rating.

totalStars determines the total number of stars to be drawn.

  1. Now, you need to add initializers for this class. Type the following code after the property declarations:
override init(frame: CGRect) {
super.init(frame: frame)
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

These are the standard initializers for UIView. Remember that UIControl is a subclass of UIView.


You may wish to re-read Chapter 7, Classes, Structures, and Enumerations, if you need help understanding this chapter.

You can learn more about UIView at https://developer.apple.com/documentation/uikit/uiview.

The first initializer, init(frame:) is used if a UIView instance is added using code. It creates a UIView instance at a specified CGRect.

A CGRect is a structure that contains the location and dimension of a rectangle. Location is expressed in x, y coordinates with the origin (0,0), located at the top-left corner of the screen, while dimension is the width and height of the rectangle in points.

The second initializer, required init?(coder:) is used if a UIView instance is in a storyboard file. When you build your app, this is stored in the app bundle. The location and dimension are specified in the Size inspector.

Bear in mind that UIControl is a subclass of UIViewGenerally, it is good practice to call the superclass initializers inside the subclass initializers, which is why you're doing so here.

Now, let's create some methods that will draw the stars on the screen. You'll need a method to draw a filled star, a half-filled star, and an empty star. To do this, proceed as follows:

  1. Add the following extension after the last curly brace (outside of the class definition):
private extension RatingsView {
func drawStar(with frame:CGRect, highlighted:Bool) {
let image = highlighted ? imgFilledStar : imgEmptyStar
draw(with: image, and: frame)
}
func drawHalfStar(with frame:CGRect) {
draw(with: imgHalfStar, and: frame)
}
func draw(with image:UIImage, and frame:CGRect) {
image.draw(in: frame)
}
}

The drawStar(with:highlighted:) method's parameters are frame (a CGRect) and highlighted (a Bool). Depending on the value of highlighted, you set image to a filled or an empty star and pass it to the draw(with:and:) method, along with frame.

The drawHalfstar(with:) method passes imgHalfStar to draw(with:and:), along with frame.

The draw(with:and:) method uses the UIImage's draw(in:) method from UIImage to draw the appropriate star (filled, half-filled, or empty) in the CGRect specified by frame.

Now, let's add code to make RatingsView draw a ratings view on the screen. All the UIView subclasses have a draw(_:) method, which is responsible for drawing their views on the screen. You need to override the superclass implementation of this method for RatingsView:

  1. Add the following code after the init methods:
override func draw(_ rect: CGRect){
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color literal showing a white square)
context!.fill(rect)

let availWidth = rect.size.width
let cellWidth = availWidth / CGFloat(totalStars)
let starSide = (cellWidth <= rect.size.height) ?
cellWidth : rect.size.height

for index in 0...totalStars {
let value = cellWidth*CGFloat(index) + cellWidth/2
let center = CGPoint(x: value+1, y:rect.size.height/2)
let frame = CGRect(x: center.x - starSide/2,
y: center.y - starSide/2, width: starSide,
height: starSide)
let highlighted = (Float(index+1) <= ceilf(Float(self.rating)))
if highlighted && (CGFloat(index+1) > CGFloat(self.rating)) {
drawHalfStar(with: frame)
} else {
drawStar(with: frame, highlighted: highlighted)
}
}
}

Let's break this down, as follows:

  • let context = UIGraphicsGetCurrentContext()
    You can think of context as a scratchpad, which you will use to draw something:
  • context!.setFillColor(color literal showing a white square)
    When typing this line, type Colo inside the braces. The Autocomplete menu should show Color Literal, as follows:


 Choose Color Literal. A small box will appear between the braces, as follows:


This box represents the color of the fill and is white by default. Double-clicking it should display a color picker, which allows you to select the color you want (white, in this case).

  • context!.fill(rect)
    This line fills the rectangular area specified by rect with the fill color, which is white in this case.
  • let availWidth = rect.size.width
    let cellWidth = availWidth / CGFloat(totalStars)
    let starSide = (cellWidth <= rect.size.height) ? cellWidth : rect.size.height
    These three lines get the dimension of the stars to be drawn by dividing the width of the ratings view by the number of stars that need to be drawn and assigning it to cellWidth. If cellWidth is less than or equal to the ratings view's height, starSide is set to cellWidth; otherwise, it's set to be the same as ratings view's height.
    For example, let's assume the ratings view is 200 points wide and 50 points high. cellWidth would be 200/5 = 40. Since 40 <= 50 evaluates to true, starSide will be set to 40.
  • for index in 0...totalStars {
    Since totalStars is set to 5, this for loop repeats five times.
  • let value = cellWidth*CGFloat(index) + cellWidth/2
    let center = CGPoint(x: value+1, y:rect.size.height/2)
    let frame = CGRect(x: center.x - starSide/2, y: center.y - starSide/2,
    width: starSide, height: starSide)
    These three lines calculate the location and size of the rectangle where each star should be drawn inside the ratings view. The location values are offset from the top-left corner of the ratings view, and the width and height are set to starSide.
    For example, for the first star, value is (40*0.0 + 40/2) = 20. center is a CGPoint where x is 20+1 = 21 and y is 50/2 = 25. frame would thus be a CGRect where x is 21 - 40/2 = 1, y is 25 - 20 = 5, width is 40, and height is 40.
  • let highlighted = (Float(index+1) <= ceilf(Float(self.rating)))
    if highlighted && (CGFloat(index+1) > CGFloat(self.rating)) {
    drawHalfStar(with: frame)
    } else {
    drawStar(with: frame, highlighted: highlighted)
    }

Depending on the value of the ratings view's rating property, this code determines whether the star is filled, half-filled, or empty.

For example, let's assume rating is 3.5.
The first star has an index of 0. This means highlighted will be set to Float(0 + 1) <= 3.5 rounded up to the next integer value, becoming 1.0 <= 4.0, which evaluates to true. The next line evaluates true && 1.0 > 3.5, becoming true && false, which evaluates to false, so drawStar(with:) is passed the frame for the first star (x=1, y=5, width=40, height=40), with highlighted set to true. This means the first star that's drawn will be a filled star. The same is true for the second and third stars.

The fourth star has an index of 3. This means highlighted will be set to Float(3 + 1) <= 3.5 rounded up to the next integer value, becoming 4.0 <= 4.0, which evaluates to true. The next line evaluates true && 4.0 > 3.5, becoming true && true, which evaluates to true, so drawHalfStar(with:) is passed the frame for the fourth star. This means the fourth star drawn will be a half-filled star.

The fifth star has an index of 4. This means highlighted will be set to Float(4 + 1) <= 3.5 rounded up to the next integer value, becoming 5.0 <= 4.0, which evaluates to false. The next line evaluates false && 5.0 > 3.5, becoming false && true, which evaluates to false, so drawStar(with:) is passed the frame for the fifth star, with highlighted set to false. This means the fifth star that's drawn will be an empty star.

That's all the code that's needed for RatingsView. Now, let's add an outlet to RestaurantDetailViewController so that it can manage what the ratings view displays. To do this, follow these steps:

  1. Click RestaurantDetailViewController.swift in the Project navigator.
  2. Type in the following code after the lblOverallRating outlet:
@IBOutlet weak var ratingView: RatingsView!

This creates an outlet in RestaurantDetailViewController for the ratings view. You now have an outlet named ratingView of type RatingsView that you will connect to the ratings view in the storyboard later. 

While you're still in RestaurantDetailViewController, let's add some code to assign a value of 3.5 to ratings view's rating property for testing:

  1. Type the following in your private extension after the first curly brace to set the value of rating to 3.5:
func createRating() {
ratingView.rating = 3.5
}
  1. Call this method in your initialize() method. It should look as follows:
func initialize() {
setupLabels()
createMap()
createRating()
}
  1. Open RestaurantDetail.storyboard and select Restaurant Detail View Controller in the document outline. Click the Connections inspector. Drag from the ratingView outlet to the ratings view object, as shown in the following screenshot:

  1. Build and run your project and go to RestaurantDetailView for any restaurant. The ratings view should display 3.5 stars, as shown in the following screenshot:

This looks great, but at the moment, the ratings view does not respond when you tap on it. You will enable it to respond to touch events in the next section so that the user can select a rating.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.187.121