snapchat-like pinch zoom on iOS - 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
}
}
}

Related

Running swift condition just once on translation

I created a UIView and a UIImageView which is inside the UIView as a subview, then I added a pan gesture to the UIImageView to slide within the UIView, the image slides now but the problem I have now is when the slider gets to the end of the view if movex > xMax, I want to print this just once print("SWIPPERD movex"). The current code I have there continues to print print("SWIPPERD movex") as long as the user does not remove his/her hand from the UIImageView which is used to slide
private func swipeFunc() {
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(acknowledgeSwiped(sender:)))
sliderImage.addGestureRecognizer(swipeGesture)
swipeGesture.delegate = self as? UIGestureRecognizerDelegate
}
#objc func acknowledgeSwiped(sender: UIPanGestureRecognizer) {
if let sliderView = sender.view {
let translation = sender.translation(in: self.baseView) //self.sliderView
switch sender.state {
case .began:
startingFrame = sliderImage.frame
viewCenter = baseView.center
fallthrough
case .changed:
if let startFrame = startingFrame {
var movex = translation.x
if movex < -startFrame.origin.x {
movex = -startFrame.origin.x
print("SWIPPERD minmax")
}
let xMax = self.baseView.frame.width - startFrame.origin.x - startFrame.width - 15 //self.sliderView
if movex > xMax {
movex = xMax
print("SWIPPERD movex")
}
var movey = translation.y
if movey < -startFrame.origin.y { movey = -startFrame.origin.y }
let yMax = self.baseView.frame.height - startFrame.origin.y - startFrame.height //self.sliderView
if movey > yMax {
movey = yMax
// print("SWIPPERD min")
}
sliderView.transform = CGAffineTransform(translationX: movex, y: movey)
}
default: // .ended and others:
UIView.animate(withDuration: 0.1, animations: {
sliderView.transform = CGAffineTransform.identity
})
}
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
You may want to use the .ended state instead of .changed state, based on your requirements. And you've mentioned you want to get the right direction only. You could try below to determine if the swipe came from right to left, or vice-versa, change as you wish:
let velocity = sender.velocity(in: sender.view)
let rightToLeftSwipe = velocity.x < 0

UIImageView disappears after rotate and move to edge

I have written a custom class which offers the feature to move and rotates images.
I need to restrict the movement to the boundaries of parent view or Superview.
So, I wrote below code to restrict it.
This works fine before an image is rotated. If I try to rotate the image and then move the image to an edge, Image disappears leaving no log or traces.
Why does it disappear and how do I avoid it?
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
If I remove the above code, nothing disappears but image moves out of boundaries. So I feel something wrong in only above lines.
So help me to correctly implement this feature after rotation.
Full Movable Image Class code :
class movableImageView: UIImageView
{
var CenCooVar = CGPoint()
override init(image: UIImage!)
{
super.init(image: image)
self.userInteractionEnabled = true
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(moveImageFnc(_:)))
let rotateImage = UIRotationGestureRecognizer(target: self, action: #selector(rotateImageFnc(_:)))
self.gestureRecognizers = [moveImage,rotateImage]
}
func moveImageFnc(moveImage: UIPanGestureRecognizer)
{
if moveImage.state == UIGestureRecognizerState.Began
{
CenCooVar = self.center
}
if moveImage.state == UIGestureRecognizerState.Changed
{
let moveCooVar = moveImage.translationInView(self.superview!)
self.center = CGPoint(x: CenCooVar.x + moveCooVar.x, y: CenCooVar.y + moveCooVar.y)
if(frame.origin.x < 1)
{
frame.origin.x = 1
}
if(frame.origin.y < 1)
{
frame.origin.y = 1
}
if(frame.maxX > superview!.frame.width)
{
frame.origin.x = superview!.frame.width - frame.width - 1
}
if(frame.maxY > superview!.frame.height)
{
frame.origin.y = superview!.frame.height - frame.height - 1
}
}
if moveImage.state == UIGestureRecognizerState.Ended
{
CenCooVar = self.center
}
}
func rotateImageFnc(rotateImage: UIRotationGestureRecognizer)
{
if rotateImage.state == UIGestureRecognizerState.Changed
{
self.transform = CGAffineTransformRotate(self.transform, rotateImage.rotation)
rotateImage.rotation = 0
}
}
}

Make a pan gesture respond on vertical swipe downs

I implemented a pan gesture in my application to dismiss a view. Basically when a user swipes down, the view dismisses.
However, I want it to work so that it doesn't dismiss if a user swipes diagonally down, or semi-down. I only want it to be responsive on a strict vertical swipe down. Here is my code so far.
var originalPosition: CGPoint?
var currentPositionTouched: CGPoint?
func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
if currentScreen == 0 {
let translation = panGesture.translation(in: view)
if panGesture.state == .began {
originalPosition = view.center
//currentPositionTouched = panGesture.location(in: view)
} else if panGesture.state == .changed {
view.frame.origin = CGPoint(
x: view.frame.origin.x,
y: view.frame.origin.y + translation.y
)
panGesture.setTranslation(CGPoint.zero, in: self.view)
} else if panGesture.state == .ended {
let velocity = panGesture.velocity(in: view)
if velocity.y >= 150 {
UIView.animate(withDuration: 0.2
, animations: {
self.view.frame.origin = CGPoint(
x: self.view.frame.origin.x,
y: self.view.frame.size.height
)
}, completion: { (isCompleted) in
if isCompleted {
self.dismiss(animated: false, completion: nil)
}
})
} else {
UIView.animate(withDuration: 0.2, animations: {
self.view.center = self.originalPosition!
})
}
}
}
}
You are on the right track with currentPositionTouched. In your .began block, set currentPositionTouched to the panGesture's location
currentPositionTouched = panGesture.location(in: view)
Then, in your .changed and .ended block, use a check to determine if the x value is the same and develop your logic accordingly.
if currentPositionTouched.x == panGesture.location(in: view).x
You can use gestureRecognizerShouldBegin. You can set it up to only recognize vertical gestures (i.e. those for which the angle is less than a certain size). For example, in Swift, it is:
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let gesture = gestureRecognizer as? UIPanGestureRecognizer else { return false }
let translation = gesture.translation(in: gesture.view!)
if translation.x != 0 || translation.y != 0 {
let angle = atan2(abs(translation.x), translation.y)
return angle < .pi / 8
}
return false
}
}
Just make sure to set the delegate of the pan gesture recognizer.
Note, I'm not checking to see if it's absolutely vertical (because of variations in the gesture path, it will likely rarely be perfectly vertical), but within some reasonable angle from that.
FYI, this is based upon this Objective-C rendition.

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

Drag behaviour wrong after UIView transformation

I have a custom DraggableView that subclasses UIImageView. I take a photo with the camera, add the resulting UIImage to a DraggableView and then I can happily drag it around the screen, as intended.
Now, if the original photo was taken in landscape, I rotate it using:
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
When I apply this transformation, the drag behaviour still works, but the directions are all wrong - if I drag left, the image moves up, not left. If I drag up, the image moves right, not up.
How do I fix this? I guess it is something to do with the UIPanGestureRecognizer being bound to the non-transformed view?
Edit: Current UIPanGestureRecognizer handler:
func onPhotoDrag(recognizer: UIPanGestureRecognizer?)
{
let translation = recognizer!.translationInView(recognizer?.view)
recognizer!.view!.center = CGPointMake(recognizer!.view!.center.x
+ translation.x, recognizer!.view!.center.y + translation.y);
recognizer?.setTranslation(CGPointZero, inView: recognizer?.view)
if (recognizer!.state == UIGestureRecognizerState.Ended)
{
let velocity = recognizer!.velocityInView(recognizer?.view)
let magnitude = sqrt((velocity.x * velocity.x)
+ (velocity.y * velocity.y))
let slideMult = magnitude / 300;
let slideFactor = 0.1 * slideMult;
let finalPoint = CGPointMake(recognizer!.view!.center.x
+ (velocity.x * slideFactor),
recognizer!.view!.center.y + (velocity.y * slideFactor));
// Animate the drag, and allow the drag delegate to do its work
DraggableView.animateWithDuration(Double(slideFactor),
delay: 0, options: UIViewAnimationOptions.CurveEaseOut,
animations: { recognizer?.view?.center = finalPoint },
completion: {(_) -> Void in self.dragDelegate?.onDragEnd(self)})
} // if: gesture ended
}
Update:
Thanks for posting your code. I pasted your code into my DraggableImageView and reproduced your problem. Although my version was handling the rotated view (without the animation), yours was going sideways.
The difference is that my code asks for the translationInView in the superview of the draggable view. You need to ask for the translationInView and velocityInView in the superview of your draggable view.
Change this line:
let translation = recognizer!.translationInView(recognizer?.view)
to:
let translation = recognizer!.translationInView(recognizer?.view?.superview)
and change this:
let velocity = recognizer!.velocityInView(recognizer?.view)
to:
let velocity = recognizer!.velocityInView(recognizer?.view?.superview)
and all will be happy.
Previous Answer:
Try this version:
class DraggableImageView: UIImageView {
override var image: UIImage? {
didSet {
if (image?.size.width > image?.size.height)
{
self.transform = CGAffineTransformMakeRotation(CGFloat(M_PI_2))
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setup()
}
func setup() {
self.userInteractionEnabled = true
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.addTarget(self, action: #selector(draggedView(_:)))
self.addGestureRecognizer(panGestureRecognizer)
}
func moveByDeltaX(deltaX: CGFloat, deltaY: CGFloat) {
self.center.x += deltaX
self.center.y += deltaY
}
func draggedView(sender:UIPanGestureRecognizer) {
if let dragView = sender.view as? DraggableImageView, superview = dragView.superview {
superview.bringSubviewToFront(dragView)
let translation = sender.translationInView(superview)
sender.setTranslation(CGPointZero, inView: superview)
dragView.moveByDeltaX(translation.x, deltaY: translation.y)
}
}
}
Use example:
override func viewDidLoad() {
super.viewDidLoad()
let dragView = DraggableImageView(frame: CGRect(x: 50, y: 50, width: 96, height: 128))
dragView.image = UIImage(named: "landscapeImage.png")
self.view.addSubview(dragView)
}

Resources