Skip to content

Instantly share code, notes, and snippets.

@rdev
Created January 22, 2021 08:27
Show Gist options
  • Save rdev/ea0c53448e12835b29faa11fec8e0388 to your computer and use it in GitHub Desktop.
Save rdev/ea0c53448e12835b29faa11fec8e0388 to your computer and use it in GitHub Desktop.
import SwiftUI
// Number clamping
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
struct Ruler: View {
var body: some View {
HStack(alignment: .center, spacing: 0) {
Spacer()
ForEach(0...39, id: \.self) { index in
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: index % 5 == 4 && index != 39 ? 5 : 3))
path.closeSubpath()
}
.stroke()
.foregroundColor(Color.white.opacity(index % 10 == 9 && index != 39 ? 1 : 0.5))
}
}
.frame(maxWidth: .infinity)
}
}
struct RulerSlider: View {
@State var hovering = false
@State var dragging = false
@State var percentage = 50.0
var name: String
var body: some View {
GeometryReader { geometry in
ZStack {
// Ruler lines
if hovering || dragging {
Ruler()
}
// Labels
HStack {
Text(self.name)
Spacer()
Text(String(format: "%.0f", self.percentage))
}.padding(.horizontal, 6)
// Background value layers
// - Left side
HStack {
Spacer()
Rectangle()
.fill(hovering || dragging ? Color.red.opacity(0.25) : Color.gray.opacity(0.25))
.frame(width: (geometry.size.width * CGFloat((100 - self.percentage) / 100) - (geometry.size.width / 2)).clamped(to: 0...300), height: 26)
.offset(x: -(geometry.size.width / 2))
}
// - Right side
HStack {
Rectangle()
.fill(hovering || dragging ? Color.red.opacity(0.25) : Color.gray.opacity(0.25))
.frame(width: (geometry.size.width * CGFloat(self.percentage / 100) - (geometry.size.width / 2)).clamped(to: 0...300), height: 26)
.offset(x: geometry.size.width / 2)
Spacer()
}
// Knob
if hovering || dragging {
Path { path in
path.move(to: CGPoint(x: 0, y: 2))
path.addLine(to: CGPoint(x: 0, y: 23))
path.closeSubpath()
}
.stroke(style: StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round))
.foregroundColor(.accentColor)
// Calculate width based on total slider width, subtract 2px to center the knob properly
// and clamp to >2 for nice looking left side
.offset(x: ((geometry.size.width * CGFloat(self.percentage / 100)) - 2).clamped(to: 2...300))
}
// Initial Value non-hover knob
if !hovering && !dragging && 49...51 ~= percentage {
Path { path in
path.move(to: CGPoint(x: 0, y: 2))
path.addLine(to: CGPoint(x: 0, y: 23))
path.closeSubpath()
}
.stroke(style: StrokeStyle(lineWidth: 4.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.gray.opacity(0.25))
.offset(x: geometry.size.width / 2 - 2)
}
}
.frame(height: 26)
.background(Color.black)
.cornerRadius(3)
.onHover { hover in
withAnimation(.easeOut(duration: 0.1)) {
self.hovering = hover
}
}
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
dragging = true
self.percentage = min(max(0, Double(value.location.x / geometry.size.width * 100)), 100)
}.onEnded { _ in
withAnimation(.easeOut(duration: 0.1)) {
self.dragging = false
}
})
.simultaneousGesture(TapGesture(count: 2).onEnded {
self.percentage = 50
})
}
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 12) {
RulerSlider(name: "some value 1")
RulerSlider(name: "some value 2")
RulerSlider(name: "some value 3")
RulerSlider(name: "some value 4")
RulerSlider(name: "some value 5")
RulerSlider(name: "some value 6")
RulerSlider(name: "some value 7")
RulerSlider(name: "some value 8")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment