How do I drag a UIImage in swift 4 - swift

I currently have the code to move the UIImage to wherever I tap on the screen, however my app requires the user to be able to drag the image about on the screen and my current code doesn't do that. I am new to the language so any help would be appreciated. Here is my current code for getting the location of the touch:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?{ if let touch = touches.first { let location = touch.location(in: self.view)

You could do it yourself by implementing both touchesBegan() and touchesMoved(), but you'd be better off using a UIPanGestureRecognzier. I'd suggest finding a sample project that lets you drag views using UIPanGestureRecognzier. It will save you some head-scratching.

I had made cube which is basically a UIView that can be dragged around and the info changes inside the cube. I used the following function to drag the cube in the view. See if it helps
var draggableCube = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
#objc func panGestureDetected(panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: self.view)
var changeX : CGFloat = 0
var changeY : CGFloat = 0
if translation.x + self.draggableCube.frame.maxX > self.view.bounds.maxX {
// prevents it to go outside of the bounds from right side
changeX = self.view.bounds.maxX - self.draggableCube.frame.maxX
} else if translation.x + self.draggableCube.frame.minX < self.view.bounds.minX{
// prevents it to go outside of the bounds from right side
changeX = self.view.bounds.minX - self.draggableCube.frame.minX
} else {
// translation is within limits
changeX = translation.x
}
if translation.y + self.draggableCube.frame.maxY > self.view.bounds.maxY {
// prevents it to go outside of the bounds from bottom
changeY = self.view.bounds.maxY - self.draggableCube.frame.maxY
} else if translation.y + self.draggableCube.frame.minY < self.view.bounds.minY {
// prevents it to go outside of the bounds from top
changeY = self.view.bounds.minY - self.draggableCube.frame.minY
} else {
// translation is within limits
changeY = translation.y
}
self.draggableCube.center = CGPoint(x: self.draggableCube.center.x + changeX, y: self.draggableCube.center.y + changeY)
panGestureRecognizer.setTranslation(CGPoint.zero, in: self.view)
if panGestureRecognizer.state == .ended {
// implement action what you want to do after the gragging ended
}
}

Related

I Want to pinch zoom two image views at the same time

I am working on a photo editor app. In this app an effect is something like a Ring shape.
I have two image views and I want to pinch zoom both image views in a way that shows like only one image view zooming. And the ring is completing by two image views.
So main issue is that the ring is behaving like half on one image view and half on other i want to attach them.
#objc func handlePan6(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: shapeImageView4)
let translation2 = gestureRecognizer.translation(in: shapeImageView2)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation.x, y: gestureRecognizer.view!.center.y + translation.y)
gestureRecognizer.view!.center = CGPoint(x: gestureRecognizer.view!.center.x + translation2.x, y: gestureRecognizer.view!.center.y + translation2.y)
gestureRecognizer.setTranslation(CGPoint.zero, in: shapeImageView4)
gestureRecognizer.setTranslation(CGPoint.zero, in: shapeImageView2)
}
}
#objc func pinchRecognized(pinch: UIPinchGestureRecognizer) {
if let view = pinch.view {
view.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
pinch.scale = 1
}
}
#objc func pinchRecognized2(pinch: UIPinchGestureRecognizer) {
if let view = pinch.view {
neonImageView2.transform = view.transform.scaledBy(x: pinch.scale, y: pinch.scale)
pinch.scale = 1
}
}

How to use pinch gesture in game scene

I need your help guys. I have game scene and func which allow to move camera using panGesture. Also i need pinchGesture to zoom in and out my SKScene. I found some code here, but it lags. Can plz someone help me to improve this code?
`
#objc private func didPinch(_ sender: UIPinchGestureRecognizer) {
guard let camera = self.camera else {return}
if sender.state == .changed {
previousCameraScale = camera.xScale
}
camera.setScale(previousCameraScale * 1 / sender.scale)
sender.scale = 1.0
}
`
try this pinch code.
//pinch -- simple version
#objc func pinch(_ recognizer:UIPinchGestureRecognizer) {
guard let camera = self.camera else { return } // The camera has a weak reference, so test it
if recognizer.state == .changed {
let deltaScale = (recognizer.scale - 1.0)*2
let convertedScale = recognizer.scale - deltaScale
let newScale = camera.xScale*convertedScale
camera.setScale(newScale)
//reset value for next time
recognizer.scale = 1.0
}
}
although i would recommend this slightly more complicated version which centers the pinch around the touch point. makes for a much nicer pinch in my experience.
//pinch around touch point
#objc func pinch(_ recognizer:UIPinchGestureRecognizer) {
guard let camera = self.camera else { return } // The camera has a weak reference, so test it
//cache location prior to scaling
let locationInView = recognizer.location(in: self.view)
let location = self.convertPoint(fromView: locationInView)
if recognizer.state == .changed {
let deltaScale = (recognizer.scale - 1.0)*2
let convertedScale = recognizer.scale - deltaScale
let newScale = camera.xScale*convertedScale
camera.setScale(newScale)
//zoom around touch point rather than center screen
let locationAfterScale = self.convertPoint(fromView: locationInView)
let locationDelta = location - locationAfterScale
let newPoint = camera.position + locationDelta
camera.position = newPoint
//reset value for next time
recognizer.scale = 1.0
}
}
//also need these extensions to add and subtract CGPoints
extension CGPoint {
static func + (a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x + b.x, y: a.y + b.y)
}
static func - (a:CGPoint, b:CGPoint) -> CGPoint {
return CGPoint(x: a.x - b.x, y: a.y - b.y)
}
}

UIViewPropertyAnimator's bounce effect

Let's say I have an animator that moves a view from (0, 0) to (-120, 0):
let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8)
animator.addAnimations {
switch state:
case .normal: view.frame.origin.x = 0
case .swiped: view.frame.origin.x = -120
}
}
I use it together with UIPanGestureRecognizer, so that I can resize the view continuously along with the finger movements.
The issue comes when I want to add some sort of bouncing effect at the start or at the end of the animation. NOT just the damping ratio, but the bounce effect. The easiest way to imagine this is Swipe-To-Delete feature of UITableViewCell, where you can drag "Delete" button beyond its actual width, and then it bounces back.
Effectively what I want to achieve, is the way to set fractionComplete property outside of [0, 1] segment, so when the fraction is 1.2, the offset becomes 144 instead of its 120 maximum.
And right now the maximum value for fractionComplete is exactly 1.
Below are some examples to have this issue visualized:
What I currently have:
What I want to achieve:
EDIT (19 January):
Sorry for my delayed reply. Here are some clarifications:
I don't use UIView.animate(...), and use UIViewPropertyAnimator instead for a very specific reason: it handles for me all the timings, curves and velocities.
For example, you dragged the view halfway through. This means that duration of the remaining part should be two times less than total duration. Or if you dragged though the 99% of the distance, it should complete the remaining part almost instantly.
As an addition, UIViewPropertyAnimator has such features as pause (when user starts dragging once again), or reverse (when user started dragging to the left, but after that he changed his mind and moved the finger to the right), that I also benefit from.
All this is not available for simple UIView animations, or requires TONS of effort at best. It is only capable of simple transitions, and this is not the case.
That's why I have to use some sort of animator.
And as I mentioned in the comments thread in the answer that was removed by its publisher, the most complex part for me here is to simulate the friction effect: the further you drag, the less the view actually moves. Just as when you're trying to drag any UIScrollView outside of it's content.
Thanks for your effort guys, but I don't think any of these 2 answers is relevant. I will try to implement this behaviour using UIDynamicAnimator whenever I have time. Probably in the nearest week or two. I will publish my approach in case I have any decent results.
EDIT (20 January):
I just uploaded a demo project to the GitHub, which includes all the transitions that I have in my project. So now you can actually have an idea why do I need to use animators and how I use them: https://github.com/demon9733/bouncingview-prototype
The only file you are actually interested in is MainViewController+Modes.swift. Everything related to transitions and animations is contained there.
What I need to do is to enable user to drag the handle area beyond "Hide" button width with a damping effect. "Hide" button will appear on swiping the handle area to the left.
P.S. I didn't really test this demo, so it can have bugs that I don't have in my main project. So you can safely ignore them.
you need to allow pan gesture to get to needed x position and at the end of pan an animation is needed to be triggered
one way to do this would be:
var initial = CGRect.zero
override func viewDidLayoutSubviews() {
initial = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initial
let open = initial.offsetBy(dx: -120, dy: 0)
// 1 manage panning along x direction
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 2 animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = closed
})
} else {
UIView.animate(withDuration: 1 , animations: {
sender.view?.frame = open
})
}
}
}
Edit 20 Jan
For simulating dampening effect and make use of UIViewPropertyAnimator specifically,
var initialOrigin = CGRect.zero
override func viewDidLayoutSubviews() {
initialOrigin = animatedView.frame
}
#IBAction func pan(_ sender: UIPanGestureRecognizer) {
let closed = initialOrigin
let open = initialOrigin.offsetBy(dx: -120, dy: 0)
// 1. to simulate dampening
var multiplier: CGFloat = 1.0
if animatedView?.frame.origin.x ?? CGFloat(0) > closed.origin.x || animatedView?.frame.origin.x ?? CGFloat(0) < open.origin.x {
multiplier = 0.2
} else {
multiplier = 1
}
// 2. animate panning
sender.view?.center = CGPoint(x: (sender.view?.center.x)! + sender.translation(in: sender.view).x * multiplier, y: (sender.view?.center.y)! )
sender.setTranslation(CGPoint.zero, in: self.view)
// 3. animate to needed position once pan ends
if sender.state == .ended {
if (sender.view?.frame.origin.x)! > initialOrigin.origin.x {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = closed.origin.x
})
animate.startAnimation()
} else {
let animate = UIViewPropertyAnimator(duration: 0.3, curve: .easeOut, animations: {
self.animatedView.frame.origin.x = open.origin.x
})
animate.startAnimation()
}
}
}
Here is possible approach (simplified & a bit scratchy - only bounce, w/o button at right, because it would much more code and actually only a matter of frames management)
Due to long delay of UIPanGestureRecognizer at ending, I prefer to use UILongPressGestureRecognizer, as it gives faster feedback.
Here is demo result
The Storyboard of used below ViewController has only gray-background-rect-container view, everything else is done in code provided below.
class ViewController: UIViewController {
#IBOutlet weak var container: UIView!
let imageView = UIImageView()
var initial: CGFloat = .zero
var dropped = false
private func excedesLimit() -> Bool {
// < set here desired bounce limits
return imageView.frame.minX < -180 || imageView.frame.minX > 80
}
#IBAction func pressHandler(_ sender: UILongPressGestureRecognizer) {
let location = sender.location(in: imageView.superview).x
if sender.state == .began {
dropped = false
initial = location - imageView.center.x
}
else if !dropped {
if (sender.state == .changed) {
imageView.center = CGPoint(x: location - initial, y: imageView.center.y)
dropped = excedesLimit()
}
if sender.state == .ended || dropped {
initial = .zero
// variant with animator
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
let stickTo: CGFloat = self.imageView.frame.minX < -100 ? -100 : 0 // place for button at right
self.imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: self.imageView.frame.origin.y), size: self.imageView.frame.size)
}
animator.isInterruptible = true
animator.startAnimation()
// uncomment below - variant with UIViewAnimation
// UIView.beginAnimations("bounce", context: nil)
// UIView.setAnimationDuration(0.2)
// UIView.setAnimationTransition(.none, for: imageView, cache: true)
// UIView.setAnimationBeginsFromCurrentState(true)
//
// let stickTo: CGFloat = imageView.frame.minX < -100 ? -100 : 0 // place for button at right
// imageView.frame = CGRect(origin: CGPoint(x: stickTo, y: imageView.frame.origin.y), size: imageView.frame.size)
// UIView.setAnimationDelegate(self)
// UIView.setAnimationDidStop(#selector(makeBounce))
// UIView.commitAnimations()
}
}
}
// #objc func makeBounce() {
// let bounceAnimation = CABasicAnimation(keyPath: "position.x")
// bounceAnimation.duration = 0.1
// bounceAnimation.repeatCount = 0
// bounceAnimation.autoreverses = true
// bounceAnimation.fillMode = kCAFillModeBackwards
// bounceAnimation.isRemovedOnCompletion = true
// bounceAnimation.isAdditive = false
// bounceAnimation.timingFunction = CAMediaTimingFunction(name: "easeOut")
// imageView.layer.add(bounceAnimation, forKey:"bounceAnimation");
// }
override func viewDidLoad() {
super.viewDidLoad()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.image = UIImage(named: "cat")
imageView.contentMode = .scaleAspectFill
imageView.layer.borderColor = UIColor.red.cgColor
imageView.layer.borderWidth = 1.0
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
container.addSubview(imageView)
imageView.centerXAnchor.constraint(equalTo: container.centerXAnchor).isActive = true
imageView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 1).isActive = true
imageView.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 1).isActive = true
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler(_:)))
pressGesture.minimumPressDuration = 0
pressGesture.allowableMovement = .infinity
imageView.addGestureRecognizer(pressGesture)
}
}

How do I rotate a colour sprite by moving finger along y-axis in the BOTTOM HALF of the screen?

Code:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if currentGameType == .wobble{
main.zRotation = (location.y)/512
if location.y > maxTouchHeight { return }
}
}
}
So I am trying to rotate a paddle depending on where your finger is on the y-axis in a type of pong game. However, I have been unable to make it so that it only works in the bottom half of the screen i.e. the two extremities of rotation are at the very top and very bottom of the screen but I want the top extremity to be at the top of the bottom half of the screen. (main is a pong paddle).
Any help is appreciated :)
After followed discussion from comments and repeated answer updates, I have a solution for you. Please let me know if questions:
class GameScene: SKScene {
let paddle = SKSpriteNode()
let maximumRotation = CGFloat(45)
override func didMove(to view: SKView) {
paddle.color = .blue
paddle.size = CGSize(width: 200, height: 15)
let deg45 = CGFloat(0.785398)
let degNeg45 = -deg45
let constraint = [SKConstraint.zRotation(SKRange(lowerLimit: degNeg45, upperLimit: deg45))]
paddle.constraints = constraint
addChild(paddle)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: self) else { return }
guard let lastLocation = touches.first?.previousLocation(in: self) else { return }
guard location.y < self.frame.midY else { return }
// Our vertical plots:
let yVal = location.y
let lastY = lastLocation.y
// How much we moved in a certain direction this frame:
let deltaY = yVal - lastY
// This value represents 100% of a 45deg angle:
let oneHundredPercent = self.frame.height/2
assert(oneHundredPercent != 0)
// The % of 100%Val (45degrees) that we moved (in radians):
let absY = abs(deltaY)
let radToDegFactor = CGFloat(0.01745329252)
let multiplier = (absY / oneHundredPercent) * radToDegFactor
// I suggest a sensitivity of 2-4:
let sensitivity = CGFloat(3)
let amountToRotate = maximumRotation * (multiplier * sensitivity)
// Rotate the correct amount in the correct direction:
if deltaY > 0 {
// Rotate counter-clockwise:
paddle.run(.rotate(byAngle: amountToRotate, duration: 0))
} else {
// Rotate clockwise:
paddle.run(.rotate(byAngle: -amountToRotate, duration: 0))
}
}
}
Just open up a new project and try it out!
There may be an easier way to do this, but this is what popped in my head. It's a little verbose too so hopefully you can see each process step-by-step.
As far as doing something using the bottom half only, you can do something like this
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let fingerPosition = touch.location(in: self)
//this is the bottom
if fingerPosition.y < self.size.height/2 {
//just replace print with your own code
print("bottom half")
}
//this is the top half
if fingerPosition.y > self.size.width/2 {
//you can just leave this as it is or if ever you want to do something with the top part, just add your own code!
print("not bottom half")
}
}
}
Hopefully this helps.
put this inside of your touches functions:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if location.y > maxTouchHeight { return }
if currentGameType == .wobble{
main.zRotation = (location.y)/512
}
}

When I rotate an imageview the size changes

I want to essentially draw a line of width 10 from one spot on the screen to another (where the touch started and where the touch currently is) and I figured the best way to do that was to find the height and width and then rotate it to be in the right spot. Well I got it eventually but some weird things were happening and I don't know a ton about Swift so I'm hoping someone here has an explanation. Here is the relevant code
var touchBox: UIImageView!
var touchOrigin: CGPoint!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let position = touch?.location(in: self.view)
touchOrigin = CGPoint(x: (position?.x)!, y: floors[fromFloor].image.frame.origin.y + 10)
touchBox = UIImageView(image: UIImage(named: "lineImg"))
touchBox.frame = CGRect(x: (position?.x)!, y: (position?.y)!, width: 20, height: 20)
touchBox.alpha = 0.5
self.view.addSubview(touchBox)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let position = touch?.location(in: self.view)
let x = min(touchOrigin!.x, (position?.x)!)
let y = min(touchOrigin!.y, (position?.y)!)
let w = max(touchOrigin!.x, (position?.x)!) - x
let h = max(touchOrigin!.y, (position?.y)!) - y
let angle = atan(h / w)
touchBox.removeFromSuperview()
touchBox = UIImageView(image: UIImage(named: "lineImg"))
let hyp = sqrt(pow(h, 2) + pow(w, 2))
// since it rotates around the center I have to find a new x, y
let x1 = (2 * x + w) / 2
let y1 = (2 * y + h) / 2 - (hyp / 2)
touchBox.frame = CGRect(x: x1, y: y1, width: 10, height: hyp)
// my image has arrows which is why I need to flip it sometimes
if (touchOrigin!.y < (position?.y)!) {
touchBox.transform = CGAffineTransform.init(scaleX: -1, y: -1)
}
if ((touchOrigin!.x < (position?.x)!) == (touchOrigin!.y < (position?.y)!)) {
touchBox.transform = touchBox.transform.rotated(by: -1 * (CGFloat(M_PI_2) - angle))
} else {
touchBox.transform = touchBox.transform.rotated(by: (CGFloat(M_PI_2) - angle))
}
touchBox.alpha = 0.5
self.view.addSubview(touchBox)
}
This version works the way I want it to. However, what is weird is that every time through touchesMoved I must redefine touchBox in order for it to work correctly. If I remove this (it has already been defined anyways) and the other lines that concern it (removeFromSuperview, alpha, addSubview), the width of the resulting image on the screen is way larger than 10 (and when I print the width I can confirm this). Alternatively, if I do this and remove the lines that rotate the image, it doesn't rotate obviously but the width does stay at 10.
I have tried rotating it with this instead
touchBox.transform = CGAffineTransform(rotationAngle: (CGFloat(M_PI_2) - angle))
but the same problem happens. Does anyone have any idea? Thanks in advance!
Could it be you're over-thinking this? There's no need for an image view in order to do this, and replacing the image view on every touchesMoved call is just plain madness. This is just a self-drawing view that implements the three touches methods: