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

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

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

How to transfer the position and scale of an image to other view?

I have two views and an image in each view. In the first view I am able to scale, rotate and pan the image using gestures. I want to record each of these transformations and transfer them to the image in the second view, so that the second image would appear in exactly the same location as the first image in the first view, but would have an opposite scaling (if the first image is scaled up, then the second one needs to be scaled down by the same amount).
The code below works only if the image is panned or rotated, but not scaled. Scaling completely changes the location of the centre of the image and the code no longer works correctly. How can I fix it so that the location of the centre of the second image is the same as the first regardless of the scaling?
Here's the view controller of the first view:
import UIKit
class FirstViewController: UIViewController {
// MARK: Properties
#IBOutlet weak var photo: UIImageView!
// Variables for scaling
var max_scale: CGFloat = 3.0
var min_scale: CGFloat = 0.2
var current_scale: CGFloat = 1.0
var lastScale: CGFloat = 0.0
// Position variables
var translate_x: CGFloat = 0.0
var translate_y: CGFloat = 0.0
var scale: CGFloat = 1.0
var angle: CGFloat = 0.0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: Gesture actions
#IBAction func pinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) {
// Move the achor point of the view's layer to the touch point
// so that scaling the view and the layer becames simpler.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Scale the view by the current scale factor.
if(gestureRecognizer.state == .began) {
// Reset the last scale, necessary if there are multiple objects with different scales
lastScale = gestureRecognizer.scale
}
if (gestureRecognizer.state == .began || gestureRecognizer.state == .changed) {
let currentScale = gestureRecognizer.view!.layer.value(forKeyPath:"transform.scale")! as! CGFloat
// Constants to adjust the max/min values of zoom
let kMaxScale:CGFloat = 15.0
let kMinScale:CGFloat = 1.0
var newScale = 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 // Store the previous scale factor for the next pinch gesture call
scale = currentScale
}
}
#IBAction func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
// Move the achor point of the view's layer to the touch point
// so that movig the view becomes simpler.
let piece = gestureRecognizer.view
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Apply the pan to the view's transform.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
//print((photo.frame.origin.x), (photo.frame.origin.y))
// Get the distance moved since the last call to this method.
let translation = gestureRecognizer.translation(in: piece?.superview)
// Set the translation point to zero so that the translation distance
// is only the change since the last call to this method.
piece?.center = CGPoint(x: ((piece?.center.x)!+translation.x), y: ((piece?.center.y)!+translation.y))
translate_x = (piece?.center.x)!
translate_y = (piece?.center.y)!
gestureRecognizer.setTranslation(CGPoint.zero, in: piece?.superview)
}
}
#IBAction func rotationGesture(_ gestureRecognizer: UIRotationGestureRecognizer) {
// Move the achor point of the view's layer to the center of the
// user's two fingers. This creates a more natural looking rotation.
self.adjustAnchorPoint(gestureRecognizer: gestureRecognizer)
// Apply the rotation to the view's transform.
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
gestureRecognizer.view?.transform = (gestureRecognizer.view?.transform.rotated(by: gestureRecognizer.rotation))!
// Set the rotation to 0 to avoid compouding the
// rotation in the view's transform.
angle += gestureRecognizer.rotation
gestureRecognizer.rotation = 0.0
}
}
func adjustAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .began {
let view = gestureRecognizer.view
let locationInView = gestureRecognizer.location(in: view)
let locationInSuperview = gestureRecognizer.location(in: view?.superview)
// Move the anchor point to the the touch point and change the position of the view
view?.layer.anchorPoint = CGPoint(x: (locationInView.x / (view?.bounds.size.width)!), y: (locationInView.y / (view?.bounds.size.height)!))
view?.center = locationInSuperview
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let resultView = segue.destination as! SecondViewController
resultView.finalImage = photo.image!
resultView.translate_x_ = photo.frame.origin.x + UIScreen.main.bounds.width / 2
resultView.translate_y_ = photo.frame.origin.y + UIScreen.main.bounds.height / 2
resultView.scale_ = scale
resultView.angle_ = angle
}
}
Location parameters are then transferred to the second view controller like this:
override func viewDidLoad() {
super.viewDidLoad()
editedPhoto.image = finalImage
// Translate
editedPhoto.center = CGPoint(x: (UIScreen.main.bounds.width / 2), y: (UIScreen.main.bounds.height / 2))
editedPhoto.center = CGPoint(x: (translate_x_), y: (translate_y_))
// Scale
editedPhoto.transform = editedPhoto.transform.scaledBy(x: (1/scale_), y: (1/scale_))
// Rotate
editedPhoto.transform = editedPhoto.transform.rotated(by: angle_)
}
Hmmm... I tried running your code as-is, but I must be missing a setup step. If I don't make any changes to the view (no drag, pinch or rotate), the view in the next viewer is out-of-position.
However....
I think you're running into trouble because you're not accounting for changing the .layer.anchorPoint
Try this -
In SecondViewController, add a new property (it will be set during prepare for segue):
var anchorPoint_: CGPoint = CGPoint.zero
then, in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
editedPhoto.image = finalImage
// add this line
editedPhoto.layer.anchorPoint = anchorPoint_
// Translate
editedPhoto.center = CGPoint(x: (translate_x_), y: (translate_y_))
// Scale
editedPhoto.transform = editedPhoto.transform.scaledBy(x: (1/scale_), y: (1/scale_))
// Rotate
editedPhoto.transform = editedPhoto.transform.rotated(by: angle_)
}
In FirstViewController, prepare for segue becomes:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let resultView = segue.destination as! SecondViewController
resultView.finalImage = photo.image!
resultView.anchorPoint_ = photo.layer.anchorPoint
resultView.translate_x_ = photo.center.x
resultView.translate_y_ = photo.center.y
resultView.scale_ = scale
resultView.angle_ = angle
}
I think that should fix the issue.

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
}
}
}

Swift : Set UIPanGestureRecognizer to UIView

Im trying to set up a UIPanGestureRecognizer to a specific UIView
self.halfViewBlue.frame = CGRectMake(0, 0, self.bounds.width / 2, self.bounds.height)
self.halfViewBlue.backgroundColor = UIColor.blueColor()
halfViewBlue.alpha = 1.0
self.addSubview(self.halfViewBlue)
self.halfViewRed.frame = CGRectMake(self.frame.midX, 0, self.bounds.width / 2, self.bounds.height)
self.halfViewRed.backgroundColor = UIColor.redColor()
halfViewRed.alpha = 1.0
self.addSubview(self.halfViewRed)
//Pan Gesture for Red
let panGestureRed = UIPanGestureRecognizer()
panGestureRed.addTarget(self, action: "handlePanRed:")
self.addGestureRecognizer(panGestureRed)
self.userInteractionEnabled = true
//Pan Gesture for Blue
let panGestureBlue = UIPanGestureRecognizer()
panGestureBlue.addTarget(self, action: "handlePanBlue:")
self.addGestureRecognizer(panGestureBlue)
self.userInteractionEnabled = true
}
func handlePanRed(gesture : UIPanGestureRecognizer){
if (gesture.state == UIGestureRecognizerState.Changed || gesture.state == UIGestureRecognizerState.Ended){
let velocity : CGPoint = gesture.velocityInView(halfViewRed)
//Pan Down
if(velocity.y > 0){
print("Panning down")
hideRedBar()
produceBlueBar()
}
//Pan Up
if(velocity.y < 0){
print("panning up")
hideBlueBar()
produceRedBar()
}
}
}
func handlePanBlue(gesture : UIPanGestureRecognizer){
if (gesture.state == UIGestureRecognizerState.Changed || gesture.state == UIGestureRecognizerState.Ended){
let velocity : CGPoint = gesture.velocityInView(halfViewBlue)
//Pan Down
if(velocity.y > 0){
print("Panning down")
hideBlueBar()
produceRedBar()
}
if(velocity.y < 0){
print("panning up")
hideRedBar()
produceBlueBar()
}
}
}
Here I simply create two UIViews, both split vertically across the screen(equally).
I then add two panGestures and a function to follow it. My issue is that the only panGesture that gets recognized is the "panGestureBlue" and weirdly i can control it on any part of the screen, not just the half where the "halfViewBlue" is.
So I'm assuming that both panGestures are being added to self.view and not the UIView it is suppose to be put with, and the "panGestureBlue" is being read in because it is added after "panGestureRed". Instead of "self" in the "panGestureBlue.addTarget" i tried putting "halfViewBlue", but it crashed.
How can i fix this issue!?
You should add the pan gesture on the subviews not the superview, like this:
self.halfViewRed.addGestureRecognizer(panGestureRed)
self.halfViewBlue.addGestureRecognizer(panGestureBlue)

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