Skip to content

Instantly share code, notes, and snippets.

@wildthink
Forked from ohayon/DraggableView.swift
Last active October 2, 2021 10:02
Show Gist options
  • Save wildthink/c2c0ef4f9052cc4feafff1cd4e6ed011 to your computer and use it in GitHub Desktop.
Save wildthink/c2c0ef4f9052cc4feafff1cd4e6ed011 to your computer and use it in GitHub Desktop.
Example of making a reusable `draggable()` modifier for SwiftUI Views
//
// DraggableView.swift
// Created by Jason Jobe on 9/15/21.
//
import SwiftUI
public struct DraggableView: ViewModifier {
public enum Axis { case x, y, xy }
@Binding var offset: CGPoint
var axes: Axis = .xy
var adjust: ((CGPoint) -> CGPoint)? = nil
var onEnd: ((CGPoint) -> Void)? = .none
public func body(content: Content) -> some View {
content
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
var newOffset = self.offset
switch axes {
case .x:
newOffset.x += value.location.x - value.startLocation.x
case .y:
newOffset.y += value.location.y - value.startLocation.y
case .xy:
newOffset.x += value.location.x - value.startLocation.x
newOffset.y += value.location.y - value.startLocation.y
}
// final adjustment here
newOffset = adjust(newOffset)
self.offset = newOffset
}
.onEnded { value in
onEnd?(offset)
}
)
.offset(x: offset.x, y: offset.y)
}
private func adjust(_ pt: CGPoint) -> CGPoint {
adjust?(pt) ?? pt
}
}
// Wrap `draggable()` in a View extension to have a clean call site
public extension View {
func draggable(axes: DraggableView.Axis = .xy,
offset: Binding<CGPoint>,
adjust: ((CGPoint) -> CGPoint)? = nil,
onEnd: ((CGPoint) -> Void)?) -> some View {
return modifier(DraggableView(offset: offset, axes: axes, adjust: adjust, onEnd: onEnd))
}
}
struct DraggableView_Previews: PreviewProvider {
struct Demo: View {
@State var pt: CGPoint = .zero
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 50)
.draggable(offset: $pt, onEnd: .none)
}
}
static var previews: some View {
Demo()
.ignoresSafeArea()
}
}
import SwiftUI
public struct DraggableView: ViewModifier {
public typealias UpdateCall = (inout CGPoint, DragGesture.Value, Bool) -> Void
@Binding var offset: CGPoint
var onChanged: UpdateCall
public init(_ offset: Binding<CGPoint>, update: @escaping UpdateCall) {
self._offset = offset
self.onChanged = update
}
public func body(content: Content) -> some View {
content
.gesture(DragGesture(minimumDistance: 0)
.onChanged { value in
onChanged(&offset, value, false)
}
.onEnded { value in
onChanged(&offset, value, true)
}
)
.offset(x: offset.x, y: offset.y)
}
func calculateNewOffset(from value: DragGesture.Value) -> CGPoint {
CGPoint(x: offset.x + value.translation.width,
y: offset.y + value.translation.height)
}
}
// Wrap `draggable()` in a View extension to have a clean call site
public extension View {
func onDragGesture(_ pt: Binding<CGPoint>, update: @escaping DraggableView.UpdateCall)
-> some View
{
modifier(DraggableView(pt, update: update))
}
}
struct DraggableView_Previews: PreviewProvider {
struct Demo: View {
@State var pt: CGPoint = .zero
@State var msg: String = "<msg>"
@State var frame: CGRect = .zero
var body: some View {
ZStack (alignment: .bottom){
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 50)
.rectReader($frame)
.onDragGesture($pt) { (pt, _, end) in
print(pt, end)
}
Text(msg)
}
}
}
static var previews: some View {
Demo()
.ignoresSafeArea()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment