Reputation: 1273
I'm trying to add a rating system to an application I'm writing. My approach is less than elegant by using a Stepper
. I'd like an interactive way of dragging to fill the star rating if possible.
I'm not sure if DragGesture
is what I'm looking for, would this suffice? Also, I'm unsure how I would convert the coordinates to the proper width to fill the stars. Any help would be appreciated.
struct ContentView: View {
@State var rating: Double = 0.0
var body: some View {
VStack {
HStack(spacing: 3) {
let stars = HStack(spacing: 0) {
ForEach(0..<5) { _ in
Image(systemName: "star.fill")
.font(.title3)
}
}
stars.overlay(
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: CGFloat(rating / 2) / 5 * geometry.size.width)
.foregroundColor(Color.yellow)
}
}.mask(stars)
).foregroundColor(Color.secondary)
}
Stepper("Rating \(String(format: "(%.0f%%)", rating * 10))", value: $rating, in: 0...10, step: 0.5)
}
.padding()
Spacer()
.frame(height: 100)
HStack(spacing: 3) {
let stars = HStack(spacing: 0) {
ForEach(0..<5) { _ in
Image(systemName: "star.fill")
.font(.title3)
}
}
stars.overlay(
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.frame(width: CGFloat(rating / 2) / 5 * geometry.size.width)
.foregroundColor(Color.yellow)
}
}.mask(stars)
).foregroundColor(Color.secondary)
.gesture(
DragGesture(minimumDistance: 0)
.onEnded { end in
if (end.location.x - end.startLocation.x) > 0 {
print("Right")
} else {
print("Left")
}
})
}
}
}
Upvotes: 2
Views: 1565
Reputation: 12125
I used circles as clip shape, but the rest should do what you want. If you change the @State rating to @Binding you can call it as a subview.
struct ContentView: View {
@State var rating = 50.0
var body: some View {
VStack {
Text("Your Rating \(rating, specifier: "%.0f")")
GeometryReader { geo in
HStack(spacing: 0) {
Color.yellow
.frame(width: geo.size.width * rating / 100)
Color.gray
.frame(width: geo.size.width * (100 - rating) / 100)
}
.gesture(
DragGesture()
.onChanged({ value in
rating = ((value.translation.width + value.startLocation.x)
/ geo.size.width * 100.0)
rating = min(100, max(rating, 0))
})
)
.clipShape(RatingClipShape())
}
.frame(height: 80)
}
.padding(30)
}
}
struct RatingClipShape: Shape {
let paddingAmount = 5.0 // %
func path(in rect: CGRect) -> Path {
let padding = rect.width * paddingAmount / 100
let diameter = (rect.width - (4 * padding)) / 5
let step = diameter + padding
var path = Path()
for i in 0..<5 {
path.addEllipse(in: CGRect(x: Double(i)*step, y: 0, width: diameter, height: diameter))
}
return path
}
}
Upvotes: 3