Skip to content

Instantly share code, notes, and snippets.

@achernoprudov
Created February 3, 2025 13:40
Show Gist options
  • Save achernoprudov/adf33ba81df74fd685a8d0cbc2861958 to your computer and use it in GitHub Desktop.
Save achernoprudov/adf33ba81df74fd685a8d0cbc2861958 to your computer and use it in GitHub Desktop.
Playground with draggable and interactable bubbles view
import SwiftUI
import PlaygroundSupport
struct ContainerView: View {
var body: some View {
BubbleContainer()
}
}
struct BubbleContainer: View {
@State
var positions: BubblePositions = .init(positions: [
.init(x: 100, y: 100),
.init(x: 200, y: 200),
.init(x: 300, y: 100),
.init(x: 400, y: 200),
.init(x: 500, y: 100),
])
var body: some View {
ZStack {
ForEach(positions.positions.indices, id: \.self) { index in
let location = Binding<CGPoint> {
positions.positions[index]
} set: { newValue in
positions.positions[index] = newValue
withAnimation(.spring) {
positions.rearrange(except: index, pinDragged: false)
}
}
BubbleView(
location: location.projectedValue,
label: index.description
) {
withAnimation(.spring) {
positions.rearrange(except: index, pinDragged: true)
}
}
}
}
.frame(width: 700, height: 300, alignment: .center)
}
}
struct BubblePositions: Sendable {
var initialPositions: [CGPoint]
var positions: [CGPoint]
init(positions: [CGPoint]) {
self.initialPositions = positions
self.positions = positions
}
// resets positions to the `initialPositions`
// starts from the position at `priorIndex` - finds the closest position to it
// then goes to the other positions in the list and finds closest positions(unassigned to other items) and puts it there
mutating func rearrange(except priorIndex: Int, pinDragged: Bool) {
guard !positions.isEmpty, !initialPositions.isEmpty else { return }
var usedIndices = Set<Int>()
var newPositions = Array(repeating: CGPoint.zero, count: positions.count)
func findClosest(from startIndex: Int) -> Int {
let currentPosition = positions[startIndex]
var minDistance = CGFloat.greatestFiniteMagnitude
var closestIndex: Int = 0
for index in 0..<initialPositions.count {
if usedIndices.contains(index) {
continue
}
let distance = initialPositions[index].distance(to: currentPosition)
if distance < minDistance {
minDistance = distance
closestIndex = index
}
}
return closestIndex
}
// find for initial but keep original position
let initialClosest = findClosest(from: priorIndex)
usedIndices.insert(initialClosest)
newPositions[priorIndex] = pinDragged
? initialPositions[initialClosest]
: positions[priorIndex]
// find for others
for index in 0..<initialPositions.count {
if index == priorIndex {
continue
}
let closestIndex = findClosest(from: index)
usedIndices.insert(closestIndex)
newPositions[index] = initialPositions[closestIndex]
}
positions = newPositions
}
}
extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
let dx = self.x - point.x
let dy = self.y - point.y
return sqrt(dx * dx + dy * dy)
}
}
struct BubbleView: View {
@Binding
var location: CGPoint
let label: String
let onDragEnded: () -> Void
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
self.location = value.location
}
.onEnded { value in
onDragEnded()
}
}
var body: some View {
Circle()
.foregroundColor(.pink)
.overlay(content: {
Text(label)
})
.frame(width: 100, height: 100)
.position(location)
.gesture(simpleDrag)
}
}
PlaygroundPage.current.setLiveView(ContainerView())
@achernoprudov
Copy link
Author

Example

Screen.Recording.2025-02-03.at.14.38.12.mov

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