Star Rating
EasySwiftUISwiftUIAccessibility 30 min read
Build a reusable star rating control in SwiftUI. It has two modes:
- Editable: the user taps a star to give a rating from 1 to 5.
- Read-only: it shows a value, like an average rating of
3.5.
struct StarRating: View {
init(rating: Binding<Double>, maxRating: Int = 5) // editable
init(rating: Double, maxRating: Int = 5) // read-only
}Examples
@State private var myRating: Double = 0
StarRating(rating: $myRating) // tap the 4th star -> myRating == 4
StarRating(rating: 3.5) // 3 full stars, 1 half star, 1 empty star
StarRating(rating: 4.2) // 4 full stars, 1 empty star (4.2 rounds to 4.0)
StarRating(rating: 3.3) // 3 full stars, 1 half star (3.3 rounds to 3.5)Requirements
- Draw
maxRatingstars in a row. The default is 5. - Show full, half and empty stars. Round the value to the nearest 0.5 first.
- Tapping star number
nsets the rating ton. - Read-only mode does not react to taps.
- Each star has a tap area of at least 44 × 44 points.
- The stars grow with Dynamic Type (the user's text size setting).
- VoiceOver reads the whole control as one element: "Rating, 3.5 out of 5 stars".
- In editable mode, VoiceOver users swipe up or down to change the rating. This is an adjustable action.
What a good answer handles
- Bad values.
-1or7must not crash. Taps only set values from 1 tomaxRating. - No rating yet.
0means "not rated". VoiceOver says "Not rated". - Right-to-left languages. In Arabic or Hebrew the half star fills from the right side.
- Feedback. A small haptic when the rating changes is a nice extra.
Notes
- Target iOS 17 or later. Use SF Symbols:
star,star.fill,star.leadinghalf.filled. starter.swifthas the API and a preview. Fill in the body and the helper.
Starter code
Copy this into a new Xcode project (or Swift Playgrounds) and build from here.
import SwiftUI
/// A row of stars.
/// Editable: the user taps a star to rate (1...maxRating).
/// Read-only: shows a value like an average, with half stars.
struct StarRating: View {
@Binding private var rating: Double
private let maxRating: Int
private let isEditable: Bool
/// Editable control. Tapping star `n` sets `rating` to `n`.
init(rating: Binding<Double>, maxRating: Int = 5) {
_rating = rating
self.maxRating = max(maxRating, 1)
self.isEditable = true
}
/// Read-only display, for example an average rating of 3.5.
init(rating: Double, maxRating: Int = 5) {
_rating = .constant(rating)
self.maxRating = max(maxRating, 1)
self.isEditable = false
}
var body: some View {
// TODO:
// 1. Draw `maxRating` stars: "star.fill", "star.leadinghalf.filled" or "star".
// 2. In editable mode, tapping star n sets `rating` to n.
// 3. Each star needs a 44 x 44 pt tap area.
// 4. VoiceOver: one element with a label, a value and an adjustable action.
Text("Rating: \(rating)")
}
/// Which SF Symbol to use for star number `star` (1-based).
private func symbolName(for star: Int) -> String {
// TODO: round `rating` to the nearest 0.5, then compare with `star`.
"star"
}
}
// MARK: - Demo
struct StarRatingDemo: View {
@State private var myRating: Double = 0
var body: some View {
VStack(alignment: .leading, spacing: 32) {
VStack(alignment: .leading, spacing: 8) {
Text("Average rating").font(.headline)
StarRating(rating: 3.5)
StarRating(rating: 4.2)
}
VStack(alignment: .leading, spacing: 8) {
Text("Your rating").font(.headline)
StarRating(rating: $myRating)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
}
#Preview {
StarRatingDemo()
}Solution
Try to build it first. Then compare your design with ours.