Star Rating View on iOS with UIKit

Detailed step-by-step guide to creating Star Rating View on iOS

Ruslan Dzhafarov
6 min readMay 29, 2024
Design by F.I Suhan on Dribbble

In this tutorial, we’ll create a customizable star rating view using UIKit. This will involve creating a custom control that displays stars, allows user interaction, and updates its appearance based on the rating provided. We’ll use a combination of custom views, layout management, and touch handling to achieve this.

Let’s get started!

Step 1: Create the StarView Class

First, let’s start with the implementation of the StarView which will handle the appearance of individual stars based on whether they are selected or unselected.

import UIKit

class StarView: UIView {

// ImageView to display the star
private let imageView = UIImageView()

// Images for selected and unselected states
private let selectedImage: UIImage
private let unselectedImage: UIImage

init() {
selectedImage = UIImage(systemName: "star.fill")!.withTintColor(.yellow, renderingMode: .alwaysOriginal)
unselectedImage = UIImage(systemName: "star")!.withTintColor(.gray, renderingMode: .alwaysOriginal)
super.init(frame: .zero)
loadLayout()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func loadLayout() {
// Disable user interaction to ignore touch events on the individual star views.
// Touch events will be handled in the parent view (StarsRateView).
isUserInteractionEnabled = false

// Setup the layout by adding the image view
addSubview(imageView)
}

override func layoutSubviews() {
super.layoutSubviews()
// Layout the imageView
imageView.frame = bounds
}

func setSelected(_ isSelected: Bool) {
// Update the star image based on the selection state
imageView.image = isSelected ? selectedImage : unselectedImage
}
}

Step 2: Create the StarsRateView Class

Initialize the StarsRateView with a specified number of stars.

import UIKit

class StarsRateView: UIControl {

// Array to hold the StarView instances
private var starsViews: [StarView] = []

// Number of stars to display
private let starsCount: Int

init(starsCount: Int = 5) {
self.starsCount = starsCount
super.init(frame: .zero)
loadLayout()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func loadLayout() {
// Setup the layout by creating and adding StarView instances
starsViews = (0..<starsCount).map { _ in StarView() }
starsViews.forEach { addSubview($0) }
}
}

Step 3. Define the Constants

The Appearance struct holds the layout details for the stars, including size, spacing, and insets.

class StarsRateView: UIControl {

// Other properties and methods...


private struct Appearance {
static let starSize = CGSize(width: 34, height: 34)
static let spacing: CGFloat = 12
static let contentInsets = UIEdgeInsets(top: 12, left: 30, bottom: 12, right: 30)
}
}

Step 4: Layout the Stars

Override layoutSubviews to properly layout the stars.

class StarsRateView: UIControl {

// Other properties and methods...


override func layoutSubviews() {
super.layoutSubviews()

guard !starsViews.isEmpty else { return }

// Start point for the first star
var startPoint: CGPoint = CGPoint(x: Appearance.contentInsets.left,
y: Appearance.contentInsets.top)

// Position each star view
for star in starsViews {
star.frame = .init(origin: startPoint, size: Appearance.starSize)

// Update the start point for the next star
startPoint.x += Appearance.starSize.width + Appearance.spacing
}
}
}

Step 5: Handle Star Selection

Manage the selection state of stars and handle user interaction to update the rating.

class StarsRateView: UIControl {

// Other properties and methods...


// Variable to store the selected star count
private var _starsSelected: Int = 0

// Property to get and set the selected star count
var starsSelected: Int {
get {
_starsSelected
}
set {
// Ensure the new value is within the valid range
let newValue = min(max(newValue, 0), starsCount)

// Update the selected star count only if it has changed
if newValue != _starsSelected {
_starsSelected = newValue

// Update the appearance of the star views
updateStarViewsState()
}
}
}

private func updateStarViewsState() {
// Update the state of each star view based on the selected star count
for (index, view) in starsViews.enumerated() {
let rating = index + 1
let isSelected = rating <= starsSelected
view.setSelected(isSelected)
}
}
}

Step 6: Handle Touches

Override touch handling methods to update the rating based on user input.

class StarsRateView: UIControl {

// Other properties and methods...



// Handle the beginning of a touch
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }

// Update the rating based on the touch location
handleTap(inPoint: touch.location(in: self))
}

// Handle a touch move
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }

// Update the rating based on the touch location
handleTap(inPoint: touch.location(in: self))
}

// Handle the end of a touch
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }

// Update the rating based on the touch location
handleTap(inPoint: touch.location(in: self))
}

// Handle a tap gesture to update the rating based on the touch point
private func handleTap(inPoint point: CGPoint) {
// Calculate the point inside the stars' bounding rectangle
let pointInsideStarsRect = CGPoint(
x: point.x - Appearance.contentInsets.left,
y: point.y - Appearance.contentInsets.top
)

// Calculate the width available for the stars
let starsWidth = bounds.width - Appearance.contentInsets.left - Appearance.contentInsets.right

// Determine the number of stars selected based on the touch point
starsSelected = Int(floor((pointInsideStarsRect.x / starsWidth) * CGFloat(starsCount))) + 1
}
}

Step 7: Implement Size Calculation

Override the sizeThatFits method to calculate the required size for the view.

class StarsRateView: UIControl {

// Other properties and methods...


// Calculate the size that fits the view's content
override func sizeThatFits(_ size: CGSize) -> CGSize {
// Calculate the total width based on star size, spacing, and content insets
let width = Appearance.starSize.width * CGFloat(starsCount)
+ Appearance.spacing * CGFloat(starsCount - 1)
+ Appearance.contentInsets.left
+ Appearance.contentInsets.right

// Return the calculated size with the height based on star size and content insets
return CGSize(width: width, height: StarsRateView.height())
}

// Calculate the height required for the view
static func height() -> CGFloat {
return Appearance.starSize.height + Appearance.contentInsets.top + Appearance.contentInsets.bottom
}
}

Conclusion

By following these steps, you’ve implemented a customizable star rating view using UIKit. The StarsRateView allows for flexible styling and interaction, making it suitable for various use cases within your app. Customize the appearance and behavior further as needed to fit your specific requirements.

Thank you for reading until the end. If you have any questions or feedback, feel free to leave a comment below.

If you enjoyed reading this article, please press the clap button 👏 . Your support encourages me to share more of my experiences and write more articles like this. Follow me here on medium for more updates!

Happy coding! 😊👨‍💻👩‍💻

--

--

Ruslan Dzhafarov

Senior iOS Developer since 2013. Sharing expert insights, best practices, and practical solutions for common development challenges