UIImageView shakes in UIPinchGestureRecognizer event - ios

I implement pinch operation iOS 8.1 with Swift.
I reference the following links but the image shakes up, down, left and right by little and little.
UIPinchGestureRecognizer. Make zoom in location of fingers, not only center
UIPinchGestureRecognizer position the pinched view between the two fingers
It is my code in UIViewController and sender.view is UIImageView.
func pinchedView(sender:UIPinchGestureRecognizer) {
if sender.numberOfTouches() < 2 {
return
}
if sender.state == UIGestureRecognizerState.Began {
lastScale = 1.0
lastPoint = sender.locationInView(sender.view)
return
}
let scale = 1.0 - (lastScale - sender.scale)
sender.view!.transform = CGAffineTransformScale(sender.view!.transform, scale, scale)
let newPoint = sender.locationInView(sender.view)
var translation = CGPoint()
translation.x = newPoint.x - lastPoint.x
translation.y = newPoint.y - lastPoint.y
sender.view!.transform = CGAffineTransformTranslate(sender.view!.transform, translation.x, translation.y)
lastPoint = sender.locationInView(sender.view)
lastScale = sender.scale
}
Why does UIImageView shake?

Instead of
sender.locationInView(sender.view)
try
sender.locationOfTouch(0, inView: sender.view)
locationInView seems to be picking an undefined touch.

Related

UIPanGestureRecognizer to rotate mapView

I have a method for panning the map using a UIPanGestureRecognizer however it doesn't rotate correctly (I want to be able to make circle motions that cause the camera to move in a circle), is there a better approach here?
#objc func panMap (sender: UIPanGestureRecognizer) {
if sender.state == .began {
print("began")
} else if sender.state == .changed {
// rotating map camera
let position = sender.translation(in: mapView)
let newDirection = mapView.camera.heading.advanced(by: (Double(position.x) + Double(position.y)))
let newCamera: MKMapCamera = MKMapCamera(lookingAtCenter: mapView.camera.centerCoordinate, fromDistance: mapView.camera.altitude, pitch: mapView.camera.pitch, heading: newDirection)
mapView.setCamera(newCamera, animated: false)
// Eugene Dudnyk's solution to help fix logic error when appending distance
sender.setTranslation(.zero, in: mapView)
} else if sender.state == .ended {
print("end")
}
}
The issue is that translation of pan gesture recognizer accumulates relative to the position at the start of panning, but you interpret it as accumulation since the last state change, and always add to the camera angle.
In case if user moved finger to afa.x = 160, and then moves finger back, afa.x changes let's say from 160 to 150, but you will append to the angle 150 / 30 = 5 degrees where angle has to be negative, - 10 / 30 degrees instead.
Try this, maybe it will make things better:
#objc func panMap (sender: UIPanGestureRecognizer) {
if sender.state == .began {
print("began")
} else if sender.state == .changed {
// rotating map camera
let translation = sender.translation(in: mapView)
let location = sender.location(in: mapView)
let bounds = mapView.bounds
let vector1 = CGVector(dx: location.x - bounds.midX, dy: location.y - bounds.midY)
let vector2 = CGVector(dx: vector1.dx + translation.x, dy: vector1.dy + translation.y)
let angle1 = atan2(vector1.dx, vector1.dy)
let angle2 = atan2(vector2.dx, vector2.dy)
mapView.camera.heading += (angle2 - angle1) * 180.0 / Double.pi
} else if sender.state == .ended {
print("end")
}
sender.setTranslation(.zero, in: mapView)
}

Scaling a SCNNode in ARKit

I am trying to scale a 3D model of a chair in ARKit using SceneKit. Here is my code for the pinch gesture:
#objc func pinched(recognizer :UIPinchGestureRecognizer) {
var deltaScale :CGFloat = 0.0
deltaScale = 1 - self.lastScale - recognizer.scale
print(recognizer.scale)
let sceneView = recognizer.view as! ARSCNView
let touchPoint = recognizer.location(in: sceneView)
let scnHitTestResults = self.sceneView.hitTest(touchPoint, options: nil)
if let hitTestResult = scnHitTestResults.first {
let chairNode = hitTestResult.node
chairNode.scale = SCNVector3(deltaScale,deltaScale,deltaScale)
self.lastScale = recognizer.scale
}
}
It does scale but for some weird reason it inverts the 3D model upside down. Any ideas why? Also although the scaling works but it is not as smooth and kinda jumps from different scale factors when used in multiple progressions using pinch to zoom.
Here is how I scale my nodes:
/// Scales An SCNNode
///
/// - Parameter gesture: UIPinchGestureRecognizer
#objc func scaleObject(gesture: UIPinchGestureRecognizer) {
let location = gesture.location(in: sceneView)
let hitTestResults = sceneView.hitTest(location)
guard let nodeToScale = hitTestResults.first?.node else {
return
}
if gesture.state == .changed {
let pinchScaleX: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.x))
let pinchScaleY: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.y))
let pinchScaleZ: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.z))
nodeToScale.scale = SCNVector3Make(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ))
gesture.scale = 1
}
if gesture.state == .ended { }
}
In my example current node refers to an SCNNode, although you can set this however you like.

How to drag and zoom UIImageView inside UIView in swift 3.0?

I want to drag and zoom UIImageView inside UIView not in UIViewController (self.view) using swift 3.0.
Here I have declare mainView and imageView,
var lastScale: CGFloat = 0.0
#IBOutlet var mainView: UIView!
#IBOutlet var imageView: UIImageView!
I have set Pan and Pinch Gesture for zoom and drag imageview inside mainView
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(ViewController.handlePinchGesture(_:)))
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(pinchGestureRecognizer)
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePanGesture(recognizer:)))
imageView.addGestureRecognizer(panGestureRecognizer)
Function for pan and pinch gesture
func handlePinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began {
lastScale = gestureRecognizer.scale
}
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let currentScale: CGFloat = (gestureRecognizer.view!.layer.value(forKeyPath: "transform.scale")! as! CGFloat)
// Constants to adjust the max/min values of zoom
//let kMaxScale: CGFloat = 2.0
let kMinScale: CGFloat = 1.0
var newScale: CGFloat = 1 - (lastScale - gestureRecognizer.scale)
//newScale = min(newScale, kMaxScale / currentScale)
newScale = max(newScale, kMinScale / currentScale)
let transform = gestureRecognizer.view?.transform.scaledBy(x: newScale, y: newScale)
gestureRecognizer.view?.transform = transform!
lastScale = gestureRecognizer.scale
}
}
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: mainView)
var recognizerFrame = recognizer.view?.frame
recognizerFrame?.origin.x += translation.x
recognizerFrame?.origin.y += translation.y
// Check if UIImageView is completely inside its superView
if mainView.bounds.contains(recognizerFrame!) {
recognizer.view?.frame = recognizerFrame!
}
else {
// Check vertically
if (recognizerFrame?.origin.y)! < mainView.bounds.origin.y {
recognizerFrame?.origin.y = 0
}
else if (recognizerFrame?.origin.y)! + (recognizerFrame?.size.height)! > mainView.bounds.size.height {
recognizerFrame?.origin.y = mainView.bounds.size.height - (recognizerFrame?.size.height)!
}
// Check horizantally
if (recognizerFrame?.origin.x)! < mainView.bounds.origin.x {
recognizerFrame?.origin.x = 0
}
else if (recognizerFrame?.origin.x)! + (recognizerFrame?.size.width)! > mainView.bounds.size.width {
recognizerFrame?.origin.x = mainView.bounds.size.width - (recognizerFrame?.size.width)!
}
}
// Reset translation so that on next pan recognition
// we get correct translation value
recognizer.setTranslation(CGPoint(x: CGFloat(0), y: CGFloat(0)), in: mainView)
}
When I run this code, And I'm zoom imageView it's go outside of mainView.
So, How can I fixed?
Whenever you scale your subview or move your subview, the frame value of subview needs to be changed. Because frame is a property of UIView, you can use KVO to observe it and set some rules for its changes like your way done for pan etc.
For KVO of frame, you can find better explanation from here

snapchat-like pinch zoom on iOS

I've published iOS app in swift whose main functions are:
1) add a photo / take a photo
2) add emoji on the photo
3) zoom, rotate, drag emoji to decorate photo
4) share it on instagram.
Emojis can be rotated, zoomed, and dragged. I've implemented these functions using UIGestureRecognizers such as UIRoationGestrueRecognizer, UIPinchGestureRecognizer, and UIPanGesstureRecognizer.
Now I am trying to update app with snapchat-like pinch zoom feature where users can zoom in / out emojis between two fingers to the extreme. Current pinch gesture works only when users' fingers are on the imageView (emoji).
Any idea / example code how to do snapchat-like pinch zoom? Below codes are how I handled rotation, pinch, and drag. Thanks in advance.
// UI Gesture Recognizers
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if(deleteMode) {
return
}
if let view = recognizer.view {
view.transform = CGAffineTransformScale(view.transform,
recognizer.scale, recognizer.scale)
recognizer.scale = 1
}
}
#IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
if(deleteMode) {
return
}
if let view = recognizer.view {
view.transform = CGAffineTransformRotate(view.transform, recognizer.rotation)
recognizer.rotation = 0
}
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
if(deleteMode) {
return
}
let translation = recognizer.translationInView(self.view)
var centerX: CGFloat!
var centerY: CGFloat!
if let view = recognizer.view {
// limit the boundary - using backgroundPanel.frame.width, height, origin.x, origin.y
if(view.center.x + translation.x < panelBackground.frame.origin.x) {
centerX = view.center.x + translation.x + 10
} else if(view.center.x > panelBackground.frame.size.width){
centerX = view.center.x + translation.x - 10
} else {
centerX = view.center.x + translation.x
}
if(view.center.y < panelBackground.frame.origin.y - 60){
// set y that I can use below
centerY = view.center.y + translation.y + 10
} else if(view.center.y > panelBackground.frame.size.height){
centerY = view.center.y + translation.y - 10
} else {
centerY = view.center.y + translation.y
}
// set final position
view.center = CGPoint(x:centerX,
y:centerY)
recognizer.setTranslation(CGPointZero, inView: self.view)
}
}
#IBAction func handleLongPress(recognizer: UILongPressGestureRecognizer) {
if(recognizer.state == UIGestureRecognizerState.Began) {
if(!deleteMode) {
print("LongPress - Delete Shows")
for (_, stickers) in self.backgroundImage.subviews.enumerate() {
for (_, deleteButtons) in stickers.subviews.enumerate() {
if let delete:UIImageView = deleteButtons as? UIImageView{
if(delete.accessibilityIdentifier == "delete") {
delete.alpha = 0.5
}
}
}
}
deleteMode = true
} else {
deleteButtonHides()
}
}
}
I'm also looking drag, pan and zoom at the same time like snapchat but if you are just looking for zoom. I'm using below function for a label to zoom via pinch. It is not smooth but do the zooming job.
func handlePinch(recognizer: UIPinchGestureRecognizer) {
if let view = recognizer.view as? UILabel {
let pinchScale: CGFloat = recognizer.scale
view.transform = view.transform.scaledBy(x: pinchScale, y: pinchScale)
recognizer.scale = 1.0
}
}
For drag,pan and zoom at the same time, checkout my below post:
Pinch, drag and pan at the same time
To achieve this snapchat like pinch zooming, add pinch gesture on Parent view also and instead of recognizer transform the selected sticker as written below:
#objc func mainImgPinchGesture(_ recognizer: UIPinchGestureRecognizer) {
print("----pinchGestureAction")
if let view = recognizer.view {
if selectedSubView != nil{
self.selectedSubView.transform = view.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
self.selectedSubView.contentScaleFactor = 1
}
}
}

Changing height of UIView in response to finger drag

I understand that the title is a bit confusing so I'll try my best to explain it well. I have a UIView that has a fixed width of 100, and a variable height that starts at 0. What I want to be able to do is drag my finger from the base of the UIView, towards the top of the screen, and the UIView changes its height/extends to the position of my finger. If that's still to complicated just imagine it as a strip of paper being pulled out from under something.
If you can help, that would be really great. I don't think it should be too hard, but I'm only a beginner and I can understand if I haven't explained it well!
This should be straight-forward with a UIPanGestureRecognizer and a little math. To change the view to the correct frame (I'm using the name _viewToChange, so replace that with your view later), simply add:
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
pan.maximumNumberOfTouches = pan.minimumNumberOfTouches = 1;
[self addGestureRecognizer:pan];
to your init method for the super view, and define the method:
- (void)pan:(UIPanGestureRecognizer *)aPan; {
CGPoint currentPoint = [aPan locationInView:self];
[UIView animateWithDuration:0.01f
animations:^{
CGRect oldFrame = _viewToChange.frame;
_viewToChange.frame = CGRectMake(oldFrame.origin.x, currentPoint.y, oldFrame.size.width, ([UIScreen mainScreen].bounds.size.height - currentPoint.y));
}];
}
This should animate the view up as the users finger moves. Hope that Helps!
Swift version using UIPanGestureRecognizer
Using PanGesture added to UIView from Storyboard and delegate set to self.
UIView is attached to bottom of the screen.
class ViewController: UIViewController {
var translation: CGPoint!
var startPosition: CGPoint! //Start position for the gesture transition
var originalHeight: CGFloat = 0 // Initial Height for the UIView
var difference: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
originalHeight = dragView.frame.height
}
#IBOutlet weak var dragView: UIView! // UIView with UIPanGestureRecognizer
#IBOutlet var gestureRecognizer: UIPanGestureRecognizer!
#IBAction func viewDidDragged(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = gestureRecognizer.location(in: dragView) // the postion at which PanGestue Started
}
if sender.state == .began || sender.state == .changed {
translation = sender.translation(in: self.view)
sender.setTranslation(CGPoint(x: 0.0, y: 0.0), in: self.view)
let endPosition = sender.location(in: dragView) // the posiion at which PanGesture Ended
difference = endPosition.y - startPosition.y
var newFrame = dragView.frame
newFrame.origin.x = dragView.frame.origin.x
newFrame.origin.y = dragView.frame.origin.y + difference //Gesture Moving Upward will produce a negative value for difference
newFrame.size.width = dragView.frame.size.width
newFrame.size.height = dragView.frame.size.height - difference //Gesture Moving Upward will produce a negative value for difference
dragView.frame = newFrame
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something
}
}
}
This is my swift solution in doing it using autolayout and a height constraint on the view you want to resize, while setting a top and bottom limit based on the safe areas
private var startPosition: CGPoint!
private var originalHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewDidDrag(_:)))
resizableView.addGestureRecognizer(panGesture)
}
#objc private func viewDidDrag(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = sender.location(in: self.view)
originalHeight = detailsContainerViewHeight.constant
}
if sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
var newHeight = originalHeight - difference
if view.bounds.height - newHeight < topSafeArea + backButtonSafeArea {
newHeight = view.bounds.height - topSafeArea - backButtonSafeArea
} else if newHeight < routeDetailsHeaderView.bounds.height + bottomSafeArea {
newHeight = routeDetailsHeaderView.bounds.height
}
resizableView.constant = newHeight
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something if interested when dragging ended.
}
}

Resources