Last active
December 9, 2024 19:37
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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