cole
cole

Reputation: 1273

SwiftUI drag gesture coordinates

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.

3

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

Answers (1)

ChrisR
ChrisR

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
    }
}

Result:
enter image description here

Upvotes: 3

Related Questions