Skip to content

Instantly share code, notes, and snippets.

@ken-itakura
Last active December 9, 2024 19:37
Show Gist options
  • Save ken-itakura/4f3891808601300fc28daa5c6f74eacd to your computer and use it in GitHub Desktop.
Save ken-itakura/4f3891808601300fc28daa5c6f74eacd to your computer and use it in GitHub Desktop.
SwiftUI drag gesture. Scale the x translation by the y translation, or vice versa.
//
// ScaledDragGesture.swift
//
// Created by Ken Itakura on 2024/12/09.
//
import SwiftUI
struct ScaledDragGesture: Gesture {
enum ScaleDirection {
case x
case y
}
let scaleDirection: ScaleDirection // direction to use as scale
let bounds: ClosedRange<CGFloat> // Scale range
let onChanged: (CGSize) -> Void
let onEnded: (() -> Void)?
@State private var previousDragLocation: CGPoint? = nil
@State private var accumulatedTranslation: CGSize = .zero
var body: some Gesture {
DragGesture()
.onChanged { value in
// initial drag location
if previousDragLocation == nil {
previousDragLocation = value.startLocation
}
// calculate delta from previous position
if let previousLocation = previousDragLocation {
let delta = CGSize(
width: value.location.x - previousLocation.x,
height: value.location.y - previousLocation.y
)
let scaleFactor: CGFloat
switch scaleDirection {
case .x:
scaleFactor = abs(value.translation.width)
case .y:
scaleFactor = abs(value.translation.height)
}
// bound to scale range
let scale = max(bounds.lowerBound, min(bounds.upperBound, 1 - scaleFactor / 100))
// scale delta
let scaledDelta = CGSize(
width: delta.width * scale,
height: delta.height * scale
)
// update accumlated value
accumulatedTranslation.width += scaledDelta.width
accumulatedTranslation.height += scaledDelta.height
switch scaleDirection {
case .x:
onChanged(CGSize(width: 0, height: accumulatedTranslation.height + value.startLocation.y))
case .y:
onChanged(CGSize(width: accumulatedTranslation.width + value.startLocation.x, height: 0))
}
}
previousDragLocation = value.location
}
.onEnded { _ in
previousDragLocation = nil
onEnded?()
}
}
}
struct ScaledDragGestureExample: View {
@State private var scaledTranslation: CGSize = .zero
@State private var scaleByX: Bool = false
var body: some View {
ZStack {
VStack{
Spacer()
HStack{
Spacer()
Toggle("move y, scale x", isOn: $scaleByX)
Text("move x, scale y")
Spacer()
}
Text("Scaled Translation: \(scaledTranslation.width, specifier: "%.1f"), \(scaledTranslation.height, specifier: "%.1f")")
.foregroundColor(.blue)
}
Circle()
.fill(Color.red)
.frame(width: 20, height: 20)
.offset(x: scaledTranslation.width, y: scaledTranslation.height)
Circle()
.fill(Color.blue)
.frame(width: 20, height: 20)
.gesture(
ScaledDragGesture(
scaleDirection: scaleByX ? .y :.x,
bounds: 0.1...1.0,
onChanged: { scaled in
self.scaledTranslation = scaled
},
onEnded: {
print("Drag ended")
}
)
)
}
.padding()
}
}
#Preview {
ScaledDragGestureExample()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment