Swift - Jigsaw Snap to Place - ios

So I am developing an Ipad app that allows the user to solve a jigsaw puzzle. I've worked out getting the panning motion for each piece, but getting them where I want to has not worked properly. I am trying to make a piece snap into it's final destination when it's within a small range, which is followed by a clicking sound.
Here is a bit of code for a single puzzle piece. When my new game button is pressed, an Image View gets set to the corresponding picture, and randomly placed on the canvas.
#IBAction func NewGameTapped(sender: UIButton){
let bounds = UIScreen.mainScreen().bounds
let height = bounds.size.height
let width = bounds.size.width
image1.image = UIImage(named:"puzzleImage1.png")
image1.center.x = CGFloat(100 + arc4random_uniform(UInt32(width)-300))
image1.center.y = CGFloat(100 + arc4random_uniform(UInt32(height)-300))
//Create Panning (Dragging) Gesture Recognizer for Image View 1
let panRecognizer1 = UIPanGestureRecognizer(target: self, action: "handlePanning1:")
// Add Panning (Dragging) Gesture Recognizer to Image View 1
image1.addGestureRecognizer(panRecognizer1)
}
This is where I am having some issues.
func handlePanning1(recognizer: UIPanGestureRecognizer) {
let center = dict1_image_coordinates["puzzleImage1"] as![Int]
let newTranslation: CGPoint = recognizer.translationInView(image1)
recognizer.view?.transform = CGAffineTransformMakeTranslation(lastTranslation1.x + newTranslation.x, lastTranslation1.y + newTranslation.y)
if recognizer.state == UIGestureRecognizerState.Ended {
lastTranslation1.x += newTranslation.x
lastTranslation1.y += newTranslation.y
}
checkPosition(image1, center: center)
}
func checkPosition(image: UIImageView, center: [Int]){
let distance: Double = sqrt(pow((Double(image.center.x) - Double(center[0])),2) + pow((Double(image.center.y) - Double(center[1])),2))
//if the distance is within range, set image to new location.
if distance <= 20{
image.center.x = CGFloat(center[0])
image.center.y = CGFloat(center[1])
AudioServicesPlaySystemSound(clickSoundID)
}
For whatever reason, the puzzle piece only wants to snap to it's spot when the piece begins the game within the acceptable snap distance. I have tried checking for the object position in various different parts of my program, but nothing has worked so far. Any help or other tips are greatly appreciated.

The issue is likely caused by this line
image1.addGestureRecognizer(panRecognizer1)
Usually people add gestureRecognizer on the parentView, or the rootView of the view controller instead of the image1 itself. The benefit is that the parentView never moves, where as the image1 is constantly being transformed, which may or may not affect the recognizer.translationInView(x) method return value.
do this instead:
self.view.addGestureRecognizer(panRecognizer1)
and change to this line in handlePanning1 function:
image1.transform = CGAffineTransformMakeTranslation(lastTranslation1.x + newTranslation.x, lastTranslation1.y + newTranslation.y)

Related

ScrollView inertia effect manually, iOS, Swift

I have UICollectionView which I'm dragging from code (don't ask me why it's very long story:)).
And my code is working pretty well:
func move(prevPoint: CGPoint, curPoint: CGPoint) {
let xDiff = curPoint.x - prevPoint.x
let yDiff = curPoint.y - prevPoint.y
let xSign = xDiff == 0 ? 1 : (xDiff / abs(xDiff))
let ySign = yDiff == 0 ? 1 : (yDiff / abs(yDiff))
let x = max(min(abs(xDiff), maxPickerStep), minPickerStep) * -xSign * xMultiplier
let y = max(min(abs(yDiff), maxPickerStep), minPickerStep) * -ySign
let offset = CGPoint(x: collectionView.contentOffset.x + x, y: collectionView.contentOffset.y)
let cell = (collectionView.visibleCells.first as? ColorsCollectionViewCell)
let innerOffset = cell?.colorCollectionView.contentOffset ?? .zero
let inset = (cell?.colorCollectionView.contentInset.top ?? 0) * 2
let innerYContentOffset = min(max(innerOffset.y + y, -inset), (cell?.colorCollectionView.contentSize.height ?? 0) - inset)
cell?.colorCollectionView.contentOffset = CGPoint(x: innerOffset.x, y: innerYContentOffset)
collectionView.contentOffset = offset
}
But in addition to scrolling, I want to achieve the same effect as in UICollectionView when scrollView moves by inertia after user takes away finger. Thanks.
First thing first, I think that moving the scroll view manually is most certainly a thing I would avoid.
Probably there is something much simpler to fulfill the behavior you need.
So I highly suggest you, and any other reader, to not go further in the reading of this post and, instead, go ahead and try to solve the problem that guided you here in the first place.
You could also ask another question here on Stack Overflow to maybe get help to try to avoid you to manually update the scrollView position.
So if you are still reading, this article is probably the way to go with implementing something that really feels like a UIScrollView. Doing anything else will probably really look and feel awful.
Basically it consists of using UIKit Dynamics to control the inertia.
So you can create an object that conforms to UIDynamicItem (with a non-zero CGRect), and change its center instead of the scrollView contentOffset, than use a UIDynamicAnimator and its UIDynamicBehavior to set up the inertia and to connect the changes during the animation to the corresponding contentOffset in the scrollView using the UIDynamicBehavior's action block.
Assuming that you have an item that is a UIDynamicItem, and an animator that is a UIDynamicAnimator, the handling of the panGesture recognizer would look something like this:
func handlGestureRecognizer(panGesture: UIPanGestureRecognizer) {
switch panGesture.state {
case .began:
self.animator.removeAllBehaviors()
case .changed:
// Update scroll view position
break
case .ended:
var velocity = panGesture.velocity(in: panGesture.view!)
velocity.x = -velocity.x
velocity.y = -velocity.y
// You probably need to check for out of bound velocity too, and also put velocity.x to 0 if the scroll is only scrolling vertically
// This is done to just save the current content offset and then change it alongside the animation from this starting point
item.center = scrollView.contentOffset
let decelerationBehavior = UIDynamicItemBehavior(items: [item])
decelerationBehavior.addLinearVelocity(velocity, for: item)
decelerationBehavior.resistance = 2.0
decelerationBehavior.action = {
// Connect the item center to the scroll contentOffset. Probably something like this:
scrollView.contentOffset = item.center
}
self.animator.addBehavior(decelerationBehavior)
default:
break
}
}
You than just need to play up with the values of the behavior and be careful with the velocity you put into the behavior having extra care in looking at the edge cases (if you scroll over the min/max for example)
PS: After all I've written, I still believe you should strongly consider not doing this and, instead, go with the standard scrollView scrolling, avoiding manual updates.
You can try to play with decelerationRate and see if it satisfies your needs.
collectionView.decelerationRate = UIScrollView.DecelerationRate(rawValue: 1)

ARKit - Object stuck to camera after tap on screen

I started out with the template project which you get when you choose ARKit project. As you run the app you can see the ship and view it from any angle.
However, once I allow camera control and tap on the screen or zoom into the ship through panning the ship gets stuck to camera. Now wherever I go with the camera the ship is stuck to the screen.
I went through the Apple Guide and seems like the don't really consider this as unexpected behavior as there is nothing about this behavior.
How to keep the position of the ship fixed after I zoom it or touch the screen?
Well, looks like allowsCameraControl is not the answer at all. It's good for SceneKit but not for ARKit(maybe it's good for something in AR but I'm not aware of it yet).
In order to zoom into the view a UIPinchGestureRecognizer is required.
// 1. Find the touch location
// 2. Perform a hit test
// 3. From the results take the first result
// 4. Take the node from that first result and change the scale
#objc private func handlePan(recognizer: UIPinchGestureRecognizer) {
if recognizer.state == .changed {
// 1.
let location = recognizer.location(in: sceneView)
// 2.
let hitTestResults = sceneView.hitTest(location, options: nil)
// 3.
if let hitTest = hitTestResults.first {
let shipNode = hitTest.node
let newScaleX = Float(recognizer.scale) * shipNode.scale.x
let newScaleY = Float(recognizer.scale) * shipNode.scale.y
let newScaleZ = Float(recognizer.scale) * shipNode.scale.z
// 4.
shipNode.scale = SCNVector3(newScaleX, newScaleY, newScaleZ)
recognizer.scale = 1
}
}
Regarding #2. I got confused a little with another hitTest method called hitTest(_:types:)
Note from documentation
This method searches for AR anchors and real-world objects detected by
the AR session, not SceneKit content displayed in the view. To search
for SceneKit objects, use the view's hitTest(_:options:) method
instead.
So that method cannot be used if you want to scale a node which is a SceneKit content

Enumerate through UIImageView

In SpriteKit you are able to enumerate through each node with a specific name. Say I have 10 sprites with the name property set to "foo" I can then run the code below and it will move each "food" node up 5 pixels every time the function is called.
enumerateChildNodesWithName("foo"){node, stop in
let sprite:SKSpriteNode = node as! SKSpriteNode
sprite.position.x += 5
}
Now, I would like to do this with UIImageView (if possible).
Here's my current setup
In my app I have code that runs every second. It is supposed to add a UIImageView using the following code
var mView:UIImageView = UIImageView(image: UIImage(named: "swirl"))
self.view.addSubview(mView)
Finally, I have a for loop that doesn't seem to be targeting each specific view. But it should be moving each individual image view in a circle. Currently it only looks like a single image is moving even while I think I've added more image views.
for view in self.view.subviews as [UIView] {
if let ind = view as? UIImageView {
let OrCe:CGPoint = mView.center
mView.center = CGPoint(x: OrCe.x + sin(tick)*50,
y: OrCe.y + cos(tick)*50)
}
}
I feel like what I'm doing is really wrong :( Is it possible for me to do what I am trying to do? I would like to do this so that I do not have to use SpriteKit. I want to try and create graphics at a lower level framework. Can I go lower then this even? How can I most efficiently render these 10 moving images?
I assume all the two peaces are consecutive.
var mView:UIImageView = UIImageView(image: UIImage(named: "swirl"))
self.view.addSubview(mView)
for view in self.view.subviews as [UIView] {
if let ind = view as? UIImageView {
let OrCe:CGPoint = view.center
view.center = CGPoint(x: OrCe.x + sin(tick)*50,
y: OrCe.y + cos(tick)*50)
}
}
If got you correct this should work.

Why doesn't the UIImageView.center change when panning with UIPanGestureRecognizer (Swift)?

When I use the 'UIPanGestureRecognizer' to move a 'UIImageView' object, I notice the 'center' attribute doesn't change. Why is this, am I doing something wrong? Here is the code:
func handlePanning1(recognizer: UIPanGestureRecognizer)
{
var index: Int = recognizer.view!.tag - 1 // index in the arrays for this piece
var newTranslation: CGPoint = recognizer.translationInView(pieces[index])
recognizer.view?.transform = CGAffineTransformMakeTranslation(lastTranslations[index].x + newTranslation.x, lastTranslations[index].y + newTranslation.y)
// THIS ALWAYS PRINTS OUT THE SAME WHILE I'M PANNING
// AND IF I PAN MULTIPLE TIMES IN DIFFERENT DIRECTIONS (AKA IT NEVER CHANGES)
print(Int(pieces[index].center.x))
print("\n")
if recognizer.state == UIGestureRecognizerState.Ended {
lastTranslations[index].x += newTranslation.x
lastTranslations[index].y += newTranslation.y
}
}
You are applying a transform to the view, you are not actually moving it. Think about it as though it is in a certain place but when it gets rendered, there are instructions to skew how it is show. If you want the view to have a different position, then you have to change its center property instead of transforming it.

Prevent dragged object from leaving view its assigned to

I'm trying to write a piece of code in iOS using swift that creates a square where the user touches and lets them drag it around. The catch is I want the area it can move around in to be confined to the UIView it was created it.
The code below almost works. You can only create the square by pressing within the box, but then you can just drag it where you want. I'm not picky about if the box stays in the "fence" and tracks with your finger or just disappears until you move your finger back in, but I can't have it all over the screen.
I'm pretty new to this, so if there's a better way to go about it, I'm happy to be corrected.
class ViewController: UIViewController {
var dragableSquare = UIView() // a square that will appear on press and be dragged around
var fence = UIView() // a view that the square should be confined within
override func viewDidLoad() {
super.viewDidLoad()
// define the fence UIView and it to view
fence.frame = CGRectMake(view.frame.width/2 - 100, view.frame.height/2 - 100, 200, 200)
fence.backgroundColor = UIColor.grayColor()
view.addSubview(fence)
// give the fence a gesture recognizer
var pressRecog = UILongPressGestureRecognizer(target: self, action: "longPress:")
pressRecog.minimumPressDuration = 0.001
fence.addGestureRecognizer(pressRecog)
}
func longPress(gesture: UILongPressGestureRecognizer) {
print("press!")
// get location of the press
var touchPoint = gesture.locationInView(fence)
// When the touch begins place the square at that point
if gesture.state == UIGestureRecognizerState.Began {
print("began")
// create and add square to fence view
dragableSquare.frame = CGRectMake(touchPoint.x-5, touchPoint.y-5, 10, 10)
dragableSquare.backgroundColor = UIColor.blueColor()
self.fence.addSubview(dragableSquare)
// While the press continues, update the square's location to the current touch point
} else {
print("moving")
dragableSquare.center = touchPoint
}
}
I just joined stack overflow and I've been really impressed with how generous and helpful the community is. I hope I'll get enough experience to start helping others out soon too.
You can use CGRectIntersection to get the size of the intersection rectangle between to views. In your case, you want to keep moving the square as long as the intersection rectangle between the square and the fence is the same size as the square (meaning the square is still wholly within the fence). So your else clause should look like this,
} else {
print("moving")
if CGRectIntersection(dragableSquare.frame, fence.bounds).size == dragableSquare.frame.size {
dragableSquare.center = touchPoint
}
}

Resources