Skip to content

Instantly share code, notes, and snippets.

@christianselig
Created February 12, 2024 20:26
Show Gist options
  • Save christianselig/eda075422436f4e4934565da001455d4 to your computer and use it in GitHub Desktop.
Save christianselig/eda075422436f4e4934565da001455d4 to your computer and use it in GitHub Desktop.
import SwiftUI
struct ContentView: View {
var body: some View {
Slidey()
}
}
struct Slidey: View {
let width: CGFloat = 200.0
@State var progress = 0.5
@State var isGestureActive = false
var body: some View {
Capsule()
.foregroundStyle(Color.black)
.overlay(alignment: .leading) {
Capsule()
.foregroundStyle(Color.green)
.frame(width: width * progress)
}
.frame(width: width, height: isGestureActive ? 40.0 : 20.0)
.animation(.default, value: isGestureActive)
.gesture(DragGesture()
.onChanged { value in
if !isGestureActive {
isGestureActive = true
}
progress = value.location.x / width
}
.onEnded { value in
isGestureActive = false
}
)
}
}
@fabio914
Copy link

fabio914 commented Feb 12, 2024

Another version, but with a scaleEffect this time:

struct Slidey: View {
    let width: CGFloat = 200.0

    @State var progress = 0.5
    @State var heightToUse: CGFloat = 20

    var body: some View {
        HStack(spacing: .zero) {
            Capsule()
                .foregroundStyle(Color.green)
                .frame(width: width, height: heightToUse)
                .scaleEffect(CGSize(width: progress, height: 1.0), anchor: .leading)
            Spacer()
        }
        .background(
            Capsule()
                .foregroundStyle(Color.black)
        )
        .frame(width: width, height: heightToUse)
        .animation(.default, value: heightToUse)
        .gesture(DragGesture()
            .onChanged { value in
                heightToUse = 40
                progress = max(0.0, min(value.location.x / width, 1.0))
            }
            .onEnded { value in
                heightToUse = 20
            }
        )
    }
}

EDIT

The interesting thing about this is that it seems to work if we keep the width constant (.frame(width: width, height: heightToUse) as opposed to .frame(width: width * progress, height: heightToUse)), hence the .scaleEffect to change the width. Though perhaps we can use a scaleEffect for the entire thing (this distorts the capsule though):

struct Slidey: View {
    let width: CGFloat = 200.0

    @State var progress = 0.5
    @State var heightScale: CGFloat = 1.0

    var body: some View {
        HStack(spacing: .zero) {
            Capsule()
                .foregroundStyle(Color.green)
                .scaleEffect(CGSize(width: progress, height: 1.0), anchor: .leading)
            Spacer()
        }
        .background(Color.black)
        .clipShape(Capsule())
        .scaleEffect(CGSize(width: 1.0, height: heightScale), anchor: .leading)
        .frame(width: width, height: 20.0)
        .animation(.default, value: heightScale)
        .gesture(DragGesture()
            .onChanged { value in
                heightScale = 2.0
                progress = max(0.0, min(value.location.x / width, 1.0))
            }
            .onEnded { value in
                heightScale = 1.0
            }
        )
    }
}

Copy link

ghost commented Feb 12, 2024

Edit

struct Slidey: View {
    @State var size: CGSize = CGSize(width: 200, height: 20)
    
    @State var progress = 0.5
    @GestureState private var isGestureActive: Bool = false
    var body: some View {
        Capsule()
            .foregroundStyle(Color.black)
            .overlay(alignment: .leading) {
                Capsule()
                    .foregroundStyle(Color.green)
                    .frame(width: size.width * progress)
            }
            .frame(width: size.width, height: size.height)
            .gesture(DragGesture(minimumDistance: 0)
                .updating($isGestureActive, body: { _, out, _ in
                    out = true
                })
                .onChanged { value in
                    progress = value.location.x / size.width
                }
            )
            .onChange(of: isGestureActive) { oldValue, newValue in
                withAnimation(.default) { size.height = newValue ? 40 : 20 }
            }
    }
}

Copy link

ghost commented Feb 12, 2024

Edit: With Masking

struct Slidey: View {
    @State var size: CGSize = CGSize(width: 200, height: 20)
    
    @State var progress = 0.5
    @GestureState private var isGestureActive: Bool = false
    var body: some View {
        Capsule()
            .foregroundStyle(Color.black)
            .overlay(alignment: .leading) {
                Capsule()
                    .foregroundStyle(Color.green)
                    .mask(alignment: .leading) {
                        let cappedWidth = max(size.width * progress, .zero)
                        Capsule()
                            .frame(width: cappedWidth)
                    }
            }
            .frame(width: size.width, height: size.height)
            .gesture(DragGesture(minimumDistance: 0)
                .updating($isGestureActive, body: { _, out, _ in
                    out = true
                })
                .onChanged { value in
                    progress = value.location.x / size.width
                }
            )
            .onChange(of: isGestureActive) { oldValue, newValue in
                withAnimation(.default) { size.height = newValue ? 40 : 20 }
            }
    }
}

@lightandshadow68
Copy link

lightandshadow68 commented Feb 12, 2024

You probably want this in a clipShape anyway to handle the slider getting flattened at the beginning.

struct Slidey: View {
    let width: CGFloat = 200.0
    
    @State var progress = 0.5
    @State var isGestureActive = false
    
    var body: some View {
        ZStack(alignment: .leading) {
            Capsule()
                .foregroundStyle(Color.black)
            
            Capsule()
                .foregroundStyle(Color.green)
                .frame(width: width * progress)
                
        }
        .gesture(DragGesture()
            .onChanged { value in
                if !isGestureActive {
                    isGestureActive = true
                }
                
                progress = min(1.0, value.location.x / width)
            }
            .onEnded { value in
                isGestureActive = false
            }
        )
        .clipShape(
            Capsule()
        )
        .frame(width: width, height: isGestureActive ? 40.0 : 20.0)
        .animation(.default, value: isGestureActive)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment