Get distance from the edge in swift 3 - ios

I've got a moveable image using UIPanGestureRecognizer and i need to make the image more transparent the closer it get's to the edge of the screen.
Below is the code I'm using to move and rotate the image the further it get's away from the center.
Adding the UIPanGestureRecognizer to the UIView the image is in:
let moveImage = UIPanGestureRecognizer(target: self, action: #selector(self.detectPan))
moveImage.cancelsTouchesInView = false
MainImageView.addGestureRecognizer(moveImage)
The function that get's called when the UIPanGestureRecognizer starts.
func detectPan(gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began || gesture.state == UIGestureRecognizerState.changed {
let translation = gesture.translation(in: self.view)
gesture.view!.center = CGPoint(x: gesture.view!.center.x + translation.x, y: gesture.view!.center.y)
gesture.setTranslation(CGPoint(x: 0,y: 0), in: self.view)
let newValue = CGFloat(((gesture.view!.center.x + translation.x) - (self.view.bounds.width * 0.50)) / 500)
MainImageView.transform = MainImageView.transform.rotated(by: -lastValue)
MainImageView.transform = MainImageView.transform.rotated(by: newValue)
lastValue = newValue
}
}

I'm assuming that " further it get's away from the center" is relative to the center of the view's parent (or if it's relative to something else, you can adjust my example accordingly).
Since you seem to only be moving the image horizontally, the distance should be a simple first degree calculation.
Something like:
let distanceToCenter = abs(gesture.view!.center.x - gesture.view!.parent!.center.x)
If you were moving both vertically and horizontally, you would need a second degree calculation to get the distance:
let deltaX = gesture.view!.center.x - gesture.view!.parent!.center.x
let deltaY = gesture.view!.center.y - gesture.view!.parent!.center.y
let distanceToCenter = sqrt(deltaX*deltaX + deltaY*deltaY)
You could then compute the alpha for fading out as follows:
let alpha = 1 - 2 * distanceToCenter / gesture.view!.parent!.bounds.size.width
Note that Swift may need some type casting for these calculations (at least until it goes to math school and figures out number theory)

Related

Issue with scaling when UIView is rotated upside down

I'm trying to rotate and scale UIView which has UIImageView inside as subView with one finger using UIPanGestureRecognizer. both rotation and scaling works fine in regular situations but when view is rotated upside down then it works in opposite direction instead of scaling up it scales down and vice versa, also when rotated at corner points it kind of flickers, don't work properly. results are shown in these gifs :
issue
another issue
here is how i'm trying to achieve this :
#objc func ScaleGest(gesture: UIPanGestureRecognizer) {
guard let gestureSuperView = gesture.view?.superview else { return }
let center = frameScaleBut.center
let touchLocation = gesture.location(in: self.view)
if gesture.state == UIPanGestureRecognizer.State.began {
self.initialDistance = distance(center, touchLocation)
} else if gesture.state == UIPanGestureRecognizer.State.changed {
let dist = distance(center, touchLocation) - initialDistance
let smallScale: CGFloat = editingMode == .sticker ? 90 : 190
let nextScale: CGSize = CGSize(width: gestureSuperView.bounds.width + dist, height: gestureSuperView.bounds.height + dist)
if (nextScale.width >= (self.view.frame.width * 0.8)) || nextScale.width <= smallScale {
return
}
gestureSuperView.bounds.size = CGSize(width: gestureSuperView.bounds.width + dist, height: gestureSuperView.bounds.height + dist)
}
}
i get distance with this method from this answer https://stackoverflow.com/a/1906659/20306199
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(sqrt(xDist * xDist + yDist * yDist))
}
this works fine when object has no rotation, but i think when object is rotated upside down it still works with same coordinates but in reality it should those values in opposite manner, can anyone please help me to understand or achieve this, thanks.
The problem is not in distance(_:_:) function, but in the center parameter. Your center parameter is determined in small images coordinate space, but gestureLocation is determined in view’s coordinate space. You can use convert(_:to:) method to do that. You can read about it here.
UPD:
Actually you don't need to calculate distance between frameScaleBut.center and gesture.location(in: self.view). Why? Because self.initialDistance almost always will be around 0. So let dist = distance(center, touchLocation) - initialDistance will always be positive. Instead calculate distance between gestureSuperView.center and gesture.location(in: self.view). gestureSuperView is already in view coordinate space.

I need to move and rotate view around point in the screen with pan gesture

I want to make the view rotate and move around the center of the screen.
Here is visual representation of what I want to achieve. Image
And my current solution:
#objc func circleMoved(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
let translation = gesture.translation(in: view)
circle.layer.zPosition = 200
let spread = view.width
let angle: CGFloat = 100
let angleMoveX = angle * translation.x / spread
let angleMoveY = angle * translation.y / spread
let angleMoveXRadians = angleMoveX * CGFloat.pi/180
let angleMoveYRadians = angleMoveY * CGFloat.pi/180
var move = CATransform3DIdentity
move = CATransform3DRotate(move, angleMoveXRadians, 0, 1, 0)
move = CATransform3DRotate(move, -angleMoveYRadians, 1, 0, 0)
move = CATransform3DTranslate(move, translation.x, translation.y, 0)
circle.layer.transform = CATransform3DConcat(circle.layer.transform, move)
gesture.setTranslation(.zero, in: view) } }
But the problem is that it rotates more than I wanted. (60 degrees max)

Scroll UIScrollView faster when scrolling it through another view

I have a UIScrollView with large UIImageView with image of very high resolution (about 10,000 x 10,000). In this UIImageView both zooming and scrolling is enabled. I also have a smaller UIImageView with the same image with much smaller resolution (about 100 x 100). I'm showing the visible portion of larger UIImageView on the smaller UIImageView. And the user can navigate to other places on larger UIImageView by panning on smaller UIImageView. The following images show what I'm trying to explain. My issue is the while panning on the smaller UIImageView the scrolling in larger UIScrollView really slow.
// function that handles the pan on green view
func handlePanNavigation(gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: navigationPanel)
guard let gv = gestureRecognizer.view else { return }
let point = CGPoint(x: gv.center.x + translation.x, y: gv.center.y + translation.y)
gestureRecognizer.view?.center = point
gestureRecognizer.setTranslation(.zero, in: navigationPanelView)
let transform = CGAffineTransform(scaleX: orgSize.width*tiledScrollView.zoomScale/navSize.width, y: orgSize.height*tiledScrollView.zoomScale/navSize.height)
let offset = navigationPanelView.frame.origin.applying(transform)
tiledScrollView.setContentOffset(offset, animated: true)
}
}
You should not animate the change of the content offset while applying a transformation of a user input real-time, since that can easily slow down the feedback.
Change
tiledScrollView.setContentOffset(offset, animated: true)
to
tiledScrollView.setContentOffset(offset, animated: false)
I'm not entirely certain how you want to accomplish this but if you want to slow-down or speed-up a pan gesture translation, add a multiplier.
switch gesture.state {
case .began:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
case .changed:
gesture.setTranslation(CGPoint.zero, in: gesture.view)
if someView.frame.origin.y < someThreshold {
someView.center = CGPoint(x: someView.center.x, y: someView.center.y + (translation.y * 0.25))
}
...
Here, any pan upward beyond someThreshold is 4x slower. In your case, obviously, add a multiplier greater than 1.

Place anchor point at the centre of the screen while doing gestures

I have a view with an image which responds to pinch, rotation and pan gestures. I want that pinching and rotation of the image would be done with respect to the anchor point placed in the middle of the screen, exactly as it is done using Xcode iPhone simulator by pressing the options key. How can I place the anchor point in the middle of the screen if the centre of the image might be scaled and panned to a different location?
Here's my scale and rotate gesture functions:
#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 // Save current scale for later use
}
}
#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 // Save rotation angle for later use
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
}
}
EDIT
I see that people aren't eager to get into this. Let me help by sharing some progress I've made in the past few days.
Firstly, I wrote a function centerAnchorPoint which correctly places the anchor point of an image to the centre of the screen regardless of where that anchor point was previously. However the image must not be scaled or rotated for it to work.
func setAnchorPoint(_ anchorPoint: CGPoint, forView view: UIView) {
var newPoint = CGPoint(x: view.bounds.size.width * anchorPoint.x, y: view.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.size.width * view.layer.anchorPoint.x, y: view.bounds.size.height * view.layer.anchorPoint.y)
newPoint = newPoint.applying(view.transform)
oldPoint = oldPoint.applying(view.transform)
var position = view.layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
view.layer.position = position
view.layer.anchorPoint = anchorPoint
}
func centerAnchorPoint(gestureRecognizer : UIGestureRecognizer) {
if gestureRecognizer.state == .ended {
view?.layer.anchorPoint = CGPoint(x: (photo.bounds.midX / (view?.bounds.size.width)!), y: (photo.bounds.midY / (view?.bounds.size.height)!))
}
}
func centerAnchorPoint() {
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Find new anchor point
let newAnchorPoint = CGPoint(x: (imageCenter.x+2*distanceX)/(UIScreen.main.bounds.size.width), y: (imageCenter.y+2*distanceY)/(UIScreen.main.bounds.size.height))
//photo.layer.anchorPoint = newAnchorPoint
self.setAnchorPoint(newAnchorPoint, forView: photo)
let dotPath = UIBezierPath(ovalIn: CGRect(x: photo.layer.position.x-2.5, y: photo.layer.position.y-2.5, width: 5, height: 5))
layer.path = dotPath.cgPath
}
Function setAchorPoint is used here to set anchor point to a new position without moving an image.
Then I updated panGesture function by inserting this at the end of it:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
EDIT 2
Ok, so I'll try to simply explain the code above.
What I am doing is:
Finding the distance between the center of the photo and the center of the screen
Apply this formula to find the new position of anchor point:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/screenWidth
Then do the same for y position.
Set this point as a new anchor point without moving the photo using setAnchorPoint function
As I said this works great if the image is not scaled. If it is, then the anchor point does not stay at the center.
Strangely enough distanceX or distanceY doesn't exactly depend on scale value, so something like this doesn't quite work:
newAnchorPointX = (imageCenter.x-distanceX)/screenWidth + distanceX/(scaleValue*screenWidth)
EDIT 3
I figured out the scaling. It appears that the correct scale factor has to be:
scaleFactor = photo.frame.size.width/photo.layer.bounds.size.width
I used this instead of scaleValue and it worked splendidly.
So panning and scaling are done. The only thing left is rotation, but it appears that it's the hardest.
First thing I thought is to apply rotation matrix to increments in X and Y directions, like this:
let incrementX = (distanceX)/(screenWidth)
let incrementY = (distanceY)/(screenHeight)
// Applying rotation matrix
let incX = incrementX*cos(angle)+incrementY*sin(angle)
let incY = -incrementX*sin(angle)+incrementY*cos(angle)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incX, y: 0.5+incY)
However this doesn't work.
Since the question is mostly answered in the edits, I don't want to repeat myself too much.
Broadly what I changed from the code posted in the original question:
Deleted calls to adjustAnchorPoint function in pinch and rotation gesture functions.
Placed this piece of code in pan gesture function, so that the anchor point would update its position after panning the photo:
if gestureRecognizer.state == .ended {
self.centerAnchorPoint()
}
Updated centerAnchorPoint function to work for rotation.
A fully working centerAnchorPoint function (rotation included):
func centerAnchorPoint() {
// Scale factor
photo.transform = photo.transform.rotated(by: -angle)
let curScale = photo.frame.size.width / photo.layer.bounds.size.width
photo.transform = photo.transform.rotated(by: angle)
// Position of the current anchor point
let currentPosition = photo.layer.anchorPoint
self.setAnchorPoint(CGPoint(x: 0.5, y: 0.5), forView: photo)
// Center of the image
let imageCenter = CGPoint(x: photo.center.x, y: photo.center.y)
self.setAnchorPoint(currentPosition, forView: photo)
// Center of the screen
let screenCenter = CGPoint(x: UIScreen.main.bounds.midX, y: UIScreen.main.bounds.midY)
// Distance between the centers
let distanceX = screenCenter.x - imageCenter.x
let distanceY = screenCenter.y - imageCenter.y
// Apply rotational matrix to the distances
let distX = distanceX*cos(angle)+distanceY*sin(angle)
let distY = -distanceX*sin(angle)+distanceY*cos(angle)
let incrementX = (distX)/(curScale*UIScreen.main.bounds.size.width)
let incrementY = (distY)/(curScale*UIScreen.main.bounds.size.height)
// Find new anchor point
let newAnchorPoint = CGPoint(x: 0.5+incrementX, y: 0.5+incrementY)
self.setAnchorPoint(newAnchorPoint, forView: photo)
}
The key things to notice here is that the rotation matrix has to be applied to distanceX and distanceY. The scale factor is also updated to remain the same throughout the rotation.

Stop UIView leaving container with UIPanGesture?

I have managed to stop my UIView going outside of the container with the pan gesture but what I am struggling to achieve is to stop it at its edges. Currently it goes right to the last pixel edge and not the edge of the moveable view.
I tried changing the frame value which I think is the way to go but couldn't produce the right results. Not sure how close I have got or if there is an easier way?
How would I stop it at its edges of the moveable view instead of the last pixel?
//Container is self.sliderContainer
//Green Block is sender.view which has a UIGesture applied
var location = sender.locationInView(self.view)
var newFrame = CGRectMake(0, 0, self.sliderContainer.frame.width, self.sliderContainer.frame.height)
let obstacleViewFrame = self.view.convertRect(self.sliderContainer.frame, fromView: self.sliderContainer.superview)
// Check if the touch is inside the obstacle view
if CGRectContainsPoint(obstacleViewFrame, location) {
sender.view!.center.x = sender.view!.center.x + translation.x
sender.setTranslation(CGPointZero, inView: self.view)
}
I would recommend doing it the simplest way, not limiting the gesture, only making sure the rect won't go out of bounds:
let movingView = sender.view!
let minCenterX = movingView.frame.size.width / 2
let maxCenterX = self.sliderContainer.frame.width - movingView.frame.size.width / 2
let newCenterX = movingView.center.x + translation.x
movingView.center.x = min(maxCenterX, max(minCenterX, newCenterX))
It looks like you are basing the movement of the sender view in the slide based on the coordinates of the touch and not the frame of sender.view.
let senderViewFrame = self.view.convertRect(sender.view!.frame, fromView: sender.view!.superview)
let insetSenderViewFrame = CGRectInset(senderViewFrame, 0.0, 10.0 /*or whatever's needed*/)
let obstacleViewFrame = self.view.convertRect(self.sliderContainer.frame, fromView: self.sliderContainer.superview)
// Check if the sender view is inside the obstacle view
if CGRectContainsRect(obstacleViewFrame, insetSenderViewFrame) {
sender.view!.center.x = sender.view!.center.x + translation.x
sender.setTranslation(CGPointZero, inView: self.view)
}

Resources