I am working on a simple jigsaw puzzle application. I already implemented jigsaw creation, basic detection of correct position on the picture.
Now I want to try to handle interactions between puzzle pieces. My puzzle element has 4 segments: top, bottom, left, right, which are UIBezpierPath
.
After verifying different possible solutions UIKit Dynamics
occurred as a possible solution.
Unluckily I am struggling a lot with bunch of unexpected behaviors.
First of all code sample:
class CanvasView {
private var animator: UIDynamicAnimator!
private let dynamicBehavior: UIDynamicItemBehavior
private let collision: UICollisionBehavior
override init(frame: CGRect) {
self.dynamicBehavior = UIDynamicItemBehavior()
self.collision = UICollisionBehavior()
super.init(frame: frame)
self.collision.collisionDelegate = self
self.collision.collisionMode = .boundaries
self.collision.translatesReferenceBoundsIntoBoundary = true
self.animator = UIDynamicAnimator(referenceView: self)
let longpressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longpressGesture.minimumPressDuration = 0.15
stackView.addGestureRecognizer(longpressGesture)
longpressGesture.delegate = self
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(_:)))
workBench.addGestureRecognizer(panGesture)
}
func buildPuzzles() {
puzzle.buildPuzzles { puzzleElements in
puzzleElements.enumerated().forEach { (index, puzzleElement) in
// Puzzle creation loop, insignificant fir this example
self.activePuzzles[index] = puzzleView
// We store puzzles in stackview and there is a dedicated UILongPress + UIPanGestre recognizer to handle moving it out of the stackTrak
self.puzzleTray.addArrangedSubview(puzzleView, desiredSize: puzzleView.image?.size ?? .zero)
self.collision.addItem(puzzleView)
self.dynamicBehavior.addItem(puzzleView)
}
self.animator.addBehavior(self.collision)
}
}
func prepareViewForReordering(at location: CGPoint) {
self.actualPuzzle = findClickedImageView(at: location)
self.snapshotView = self.actualPuzzle?.snapshotView(afterScreenUpdates: true)
// Resize View to fit actual size
if let snapshotView, let actualPuzzle {
// Some visual effects, scaling the puzzle on pick nothing significant
{...}
self.collision.addItem(snapshotView)
self.dynamicBehavior.addItem(snapshotView)
} else {
return
}
self.originalPosition = location
}
func dragTheTemporaryView(_ gestureRecognizer: UIGestureRecognizer) {
guard let actualPuzzle, let snapshotView else {
return
}
// Drag the temporaryView
var newLocation = gestureRecognizer.location(in: self)
// Some calculations to detect boundires etc. nothing significant
{...}
snapshotView.center = newLocation
animator.updateItem(usingCurrentState: snapshotView)
// Some detection to snap to correct place nothing significant
{...}
// Reset the translation to avoid cumulative effect
originalPosition = newLocation
}
func dropTheTemporaryView() {
guard let actualPuzzle, let snapshotView else { return }
// Some code to handle putting the puzzle on the board on back into a tray, nothing significant
{...}
actualPuzzle.frame = snapshotView.frame
animator.updateItem(usingCurrentState: actualPuzzle)
snapshotView.removeFromSuperview()
self.dynamicBehavior.removeItem(snapshotView)
self.collision.removeItem(snapshotView)
self.snapshotView = nil
self.actualPuzzle = nil
}
@objc func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
let location = gestureRecognizer.location(in: self)
prepareViewForReordering(at: location)
case .changed:
dragTheTemporaryView(gestureRecognizer)
case .ended, .cancelled:
dropTheTemporaryView()
// Reenable scroll and gesture recognizer
gestureRecognizer.isEnabled = true
default:
break
}
}
@objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
let location = gestureRecognizer.location(in: self)
prepareViewForReordering(at: location)
case .changed:
dragTheTemporaryView(gestureRecognizer)
case .ended, .cancelled:
dropTheTemporaryView()
gestureRecognizer.isEnabled = true
default: break
}
}
We’re working with 2 gesture recognizers. UILongPressGestureRecognizer
and UIPanGestureRecognizer
. LongPress is responsible for initial item pick-up from the tray (scrollable UIStackView
) and PanGesture is responsible for further moving element when it’s picked from the board.
My current problem seems to be directly connected with not fully undestanding the UIDynamic
kit.
-
All of my objects seems to be affected by some basic physics/gravity. They fall downwards. I do not care about down-force, bouncing between objects or screen edges.
-
UIPanGestureRecognizer
works correctly however, the objects are rotated. It looks lke the gravity factor and/or object mass seems to be in action. -
Objects when are about to snap into the final position with code below are distorted:
actualPuzzle.frame = snapshotView.frame actualPuzzle.isHidden = false animator.updateItem(usingCurrentState: actualPuzzle)
- Finally I would like to detect only collisions based on the defined
UIBezpierPath
edges but prevent the objects of interaction one with another. As an interactions I mean physical collisions, pushing each other away etc. They still should overlap with each other. Collisions are only to determine if objects are neighbours and should they snap.