SwiftUI makes it really easy to create custom UI components, because they are effectively just views that have some sort of @Binding
exposed for us to read.
To demonstrate this, we’re going to build a star rating view that lets the user enter scores between 1 and 5 by tapping images. Although we could just make this view simple enough to work for our exact use case, it’s often better to add some flexibility where appropriate so it can be used elsewhere too. Here, that means we’re going to make six customizable properties:
nil
for the off image, and a filled star for the on image; if we find nil
in the off image we’ll use the on image there too)We also need one extra property to store an @Binding
integer, so we can report back the user’s selection to whatever is using the star rating.
So, create a new SwiftUI view called “RatingView”, and start by giving it these properties:
@Binding var rating: Int
var label = ""
var maximumRating = 5
var offImage: Image?
var onImage = Image(systemName: "star.fill")
var offColor = Color.gray
var onColor = Color.yellow
Before we fill in the body
property, please try building the code – you should find that it fails, because our RatingView_Previews
struct doesn’t pass in a binding to use for rating
.
SwiftUI has a specific and simple solution for this called constant bindings. These are bindings that have fixed values, which on the one hand means they can’t be changed in the UI, but also means we can create them trivially – they are perfect for previews.
So, replace the existing previews
property with this:
static var previews: some View {
RatingView(rating: .constant(4))
}
Now let’s turn to the body
property. This is going to be a HStack
containing any label that was provided, plus as many stars as have been requested – although, of course, they can choose any image they want, so it might not be a star at all.
The logic for choosing which image to show is pretty simple, but it’s perfect for carving off into its own method to reduce the complexity of our code. The logic is this:
We can encapsulate that in a single method, so add this to RatingView
now:
func image(for number: Int) -> Image {
if number > rating {
return offImage ?? onImage
} else {
return onImage
}
}
And now implementing the body
property is surprisingly easy: if the label has any text use it, then use ForEach
to count from 1 to the maximum rating plus 1 and call image(for:)
repeatedly. We’ll also apply a foreground color depending on the rating, and add a tap gesture that adjusts the rating.
Replace your existing body
property with this:
HStack {
if label.isEmpty == false {
Text(label)
}
ForEach(1..<maximumRating + 1, id: \.self) { number in
image(for: number)
.foregroundColor(number > rating ? offColor : onColor)
.onTapGesture {
rating = number
}
}
}
That completes our rating view already, so to put it into action go back to AddBookView
and replace the second section with this:
Section {
TextEditor(text: $review)
RatingView(rating: $rating)
} header: {
Text("Write a review")
}
That’s all it takes – our default values are sensible, so it looks great out of the box. And the result is much nicer to use: there’s no need to tap into a detail view with a picker here, because star ratings are more natural and more common.
SPONSORED Thorough mobile testing hasn’t been efficient testing. With Waldo Sessions, it can be! Test early, test often, test directly in your browser and share the replay with your team.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.