Star Rating View on iOS with UIKit
Detailed step-by-step guide to creating Star Rating View on iOS
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! 😊👨💻👩💻