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 #Preview
code 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 preview code with this:
#Preview {
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 {
offImage ?? onImage
} else {
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 wrap each star inside a button 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
Button {
rating = number
} label: {
image(for: number)
.foregroundStyle(number > rating ? offColor : onColor)
}
}
}
That completes our rating view already, so to put it into action go back to AddBookView
and replace the second section with this:
Section("Write a review") {
TextEditor(text: $review)
RatingView(rating: $rating)
}
Our default values are sensible, so it looks great out of the box – go ahead and try it now!
Chances are you'll see things don't quite work right: no matter which star rating you press, it will select 5 stars!
I've seen this problem hit countless hundreds of people, no matter how much experience they have. The problem is that when we have rows inside a form or a list, SwiftUI likes to assume the rows themselves are tappable. This makes selection easier for users, because they can tap anywhere in a row to trigger the button inside it.
In our case we have multiple buttons, so SwiftUI is tapping them all in order – rating
gets set to 1, then 2, then 3, 4, and 5, which is why it ends up at 5 no matter what.
We can disable the whole "tap the row to trigger its buttons" behavior with an extra modifier attached to the whole HStack
:
.buttonStyle(.plain)
That makes SwiftUI treat each button individually, so everything works as planned. 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 Ready to dive into the world of Swift? try! Swift Tokyo is the premier iOS developer conference will be happened in April 9th-11th, where you can learn from industry experts, connect with fellow developers, and explore the latest in Swift and iOS development. Don’t miss out on this opportunity to level up your skills and be part of the Swift community!
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.