Interactive UIView flip transition - ios

I have the front and the back of a card. I animate the transition between the two like this:
private func flipToBack() {
UIView.transition(from: frontContainer, to: backContainer, duration: 0.5, options: [.transitionFlipFromRight, .showHideTransitionViews], completion: nil)
}
private func flipToFront() {
UIView.transition(from: backContainer, to: frontContainer, duration: 0.5, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
}
This works perfectly. However, I want to make this animation interactive, so that if the user pans horizontally across the card, the flip animation will advance proportionally. Usually, I would do this kind of interactive animation with a UIViewPropertyAnimator, but I do not know what property I would animate in the animator without building up the flip animation from scratch.
Is it possible to use UIViewPropertyAnimator, or is there some other alternative to make the flip interactive?

I ended up writing it myself. The code is pretty long, so here's a link to the full program on GitHub. Here are the key parts:
Everything is encapsulated in an InteractiveFlipAnimator object that takes a front view (v1) and a back view (v2). Each view also gets a black cover that functions as a shadow to add that darkening effect when the view turns in perspective.
Here is the panning function:
/// Add a `UIPanGestureRecognizer` to the main view that contains the card and pass it onto this function.
#objc func pan(_ gesture: UIPanGestureRecognizer) {
guard let view = gesture.view else { return }
if isAnimating { return }
let translation = gesture.translation(in: view)
let x = translation.x
let angle = startAngle + CGFloat.pi * x / view.frame.width
// If the angle is outside [-pi, 0], then do not rotate the view and count it as touchesEnded. This works because the full width is the screen width.
if angle < -CGFloat.pi || angle > 0 {
if gesture.state != .began && gesture.state != .changed {
finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
}
return
}
var transform = CATransform3DIdentity
// Perspective transform
transform.m34 = 1 / -500
// y rotation transform
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
self.v1.layer.transform = transform
self.v2.layer.transform = transform
// Set the shadow
if startAngle == 0 {
self.v1Cover.alpha = 1 - abs(x / view.frame.width)
self.v2Cover.alpha = abs(x / view.frame.width)
} else {
self.v1Cover.alpha = abs(x / view.frame.width)
self.v2Cover.alpha = 1 - abs(x / view.frame.width)
}
// Set which view is on top. This flip happens when it looks like the two views make a vertical line.
if abs(angle) < CGFloat.pi / 2 {
// Flipping point
v1.layer.zPosition = 0
v2.layer.zPosition = 1
} else {
v1.layer.zPosition = 1
v2.layer.zPosition = 0
}
// Save state
if gesture.state != .began && gesture.state != .changed {
finishedPanning(angle: angle, velocity: gesture.velocity(in: view))
}
}
The code to finish panning is very similar, but it is also much longer. To see it all come together, visit the GitHub link above.

Related

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)

How to convert CATransform3DTranslate to CGAffineTransform so it can Mimic a Carousel View

My question pertains to how to mimic this Carousel view Youtube video only using a UIView not it's layer or a CALayer, which means actually transforming the UIViews its self.
I found a stack overflow question that actually is able to convert a
CATransform3D into a CGAffineTransform. That was written by some genius here as an answer, but my problem is a little unique.
The animation you see below is using CALayer to create. I need to create this same animation but transforming the UIView instead of its layer.
What it's Supposed to look like:
Code (Creates animation using Layers):
This takes an image card which is a CALayer() with a image attached to it and transforms which places it in the Carousel of images.
Note: turnCarousel() is also called when the user pans which moves / animates the Carousel.
let transformLayer = CATransformLayer()
func turnCarousel() {
guard let transformSubLayers = transformLayer.sublayers else {return}
let segmentForImageCard = CGFloat(360 / transformSubLayers.count)
var angleOffset = currentAngle
for layer in transformSubLayers {
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, degreeToRadians(deg: angleOffset), 0, 1, 0)
transform = CATransform3DTranslate(transform, 0, 0, 175)
CATransaction.setAnimationDuration(0)
layer.transform = transform
angleOffset += segmentForImageCard
}
}
What It Currently Looks Like:
So basically it's close, but it seems as though there is a scaling issue with the cards that are supposed to be seen as in the front and the cards that are in the back of the carousel.
Fo this what I did is use a UIImageView as the base view for the carousel and then added more UIImageViews as cards to it. So now we are trying do a transformation on a UIImageView/UIView
Code:
var carouselTestView = UIImageView()
func turnCarouselTestCarousel() {
let segmentForImageCard = CGFloat(360 / carouselTestView.subviews.count)
var angleOffset = currentAngleTestView
for subview in carouselTestView.subviews {
var transform2 = CATransform3DIdentity
transform2.m34 = -1 / 500
transform2 = CATransform3DRotate(transform2, degreeToRadians(deg: angleOffset), 0, 1, 0)
transform2 = CATransform3DTranslate(transform2, 0, 0, 175)
CATransaction.setAnimationDuration(0)
// m13, m23, m33, m43 are not important since the destination is a flat XY plane.
// m31, m32 are not important since they would multiply with z = 0.
// m34 is zeroed here, so that neutralizes foreshortening. We can't avoid that.
// m44 is implicitly 1 as CGAffineTransform's m33.
let fullTransform: CATransform3D = transform2
let affine = CGAffineTransform(a: fullTransform.m11, b: fullTransform.m12, c: fullTransform.m21, d: fullTransform.m22, tx: fullTransform.m41, ty: fullTransform.m42)
subview.transform = affine
angleOffset += segmentForImageCard
}
}
the sub image that actually make up the Carousel are add with this function which simply goes through a for loop of image named 1...6 in my assets folder.
Code:
func CreateCarousel() {
carouselTestView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height / 2.9)
carouselTestView.center = CGPoint(self.view.frame.width * 0.5, self.view.frame.height * 0.5)
carouselTestView.alpha = 1.0
carouselTestView.backgroundColor = UIColor.clear
carouselTestView.isUserInteractionEnabled = true
self.view.insertSubview(carouselTestView, at: 0)
for i in 1 ... 6 {
addImageCardTestCarousel(name: "\(i)")
}
// Set the carousel for the first time. So that now we can see it like an actual carousel animation
turnCarouselTestCarousel()
let panGestureRecognizerTestCarousel = UIPanGestureRecognizer(target: self, action: #selector(self.performPanActionTestCarousel(recognizer:)))
panGestureRecognizerTestCarousel.delegate = self
carouselTestView.addGestureRecognizer(panGestureRecognizerTestCarousel)
}
The addImageCardTestCarousel function is here:
Code:
func addImageCardTestCarousel(name: String) {
let imageCardSize = CGSize(width: carouselTestView.frame.width / 2, height: carouselTestView.frame.height)
let cardPanel = UIImageView()
cardPanel.frame.size = CGSize(width: imageCardSize.width, height: imageCardSize.height)
cardPanel.frame.origin = CGPoint(carouselTestView.frame.size.width / 2 - imageCardSize.width / 2 , carouselTestView.frame.size.height / 2 - imageCardSize.height / 2)
guard let imageCardImage = UIImage(named: name) else {return}
cardPanel.image = imageCardImage
cardPanel.contentMode = .scaleAspectFill
cardPanel.layer.masksToBounds = true
cardPanel.layer.borderColor = UIColor.white.cgColor
cardPanel.layer.borderWidth = 1
cardPanel.layer.cornerRadius = cardPanel.frame.height / 50
carouselTestView.addSubview(cardPanel)
}
Purpose:
The purpose of this is that I want to build a UI that can take UIViews on the rotating cards you see, and a CALayer cannot add a UIView as a subview. It can only add the UIView's layer to its own Layer. So to solve this problem I need to actually achieve this animation with UIViews not CALayers.
I solved it the view that Appears to be behind the front most view are actually grabbing all the touch even if you touch a card right in the front the back card will prevent touches for the front card. So i made a function that can calculate. Which views are in the front. Than disable and enable touches for then. Its like when 2 cards get stacked on top of each other the card to the back will stop the card from the front from taking/userInteraction.
Code:
func DetermineFrontViews(view subview: UIView, angle angleOffset: CGFloat) {
let looped = Int(angleOffset / 360) // must round down to Int()
let loopSubtractingReset = CGFloat(360 * looped) // multiply 360 how ever many times we have looped
let finalangle = angleOffset - loopSubtractingReset
if (finalangle >= -70 && finalangle <= 70) || (finalangle >= 290) || (finalangle <= -260) {
print("In front of view")
if subview.isUserInteractionEnabled == false {
subview.isUserInteractionEnabled = true
}
} else {
print("Back of view")
if subview.isUserInteractionEnabled == true {
subview.isUserInteractionEnabled = false
}
}
}
I added this to the turn function to see if it could keep track of the first card being either in the back of the carousel or the front.
if subview.layer.name == "1" {
DetermineFrontViews(view: subview, angle: angleOffset)
}

Zoom with one finger on mapbox map also seems to change the center location?

Below is my method for zooming. I am trying to implement it, so that when the user uses one finger to zoom (move finger up or down in specified region), it should zoom straight down or up just like normal (see snapchat as an example).
The problem is the code I have written changes latitude at the same time, which is unintended.
Why is this happening and how can I change it?
#objc func panGesture(_ sender: UIPanGestureRecognizer) {
print("recognized?")
// note that 'view' here is the overall video preview
let velocity = sender.velocity(in: mapView) //view
//print(sender.translation(in: view), "<-- waht is val?")
// print(sender.setTranslation(CGPoint(x: 16, y: 495), in: view), "<-- mmmmm")
if velocity.y >= 2 || velocity.y <= 2 {
let minimumZoomFactor: CGFloat = 1.0
let maximumZoomFactor: CGFloat = 17.0 // artificially set a max useable zoom of 14x (maybe shuld be icncrease?)
// clamp a zoom factor between minimumZoom and maximumZoom
func clampZoomFactor(_ factor: CGFloat) -> CGFloat {
return min(max(factor, minimumZoomFactor), maximumZoomFactor)
}
func update(scale factor: CGFloat) {
mapView.zoomLevel = Double(exactly: factor)! //maaybe setZoomLevel(... and animate it bit by bit?
}
var lastVal = 0
//BELOW IS SENDER.STATE THINGS!!!
switch sender.state {
case .began:
originalZoomLevel = mapView.zoomLevel//device.videoZoomFactor
//print(originalZoomLevel, "<-- what is initialZoom11111111???")
case .changed:
// distance in points for the full zoom range (e.g. min to max), could be view.frame.height
let fullRangeDistancePoints: CGFloat = 300.0 //dont know fi this is right??
// extract current distance travelled, from gesture start
let currentYTranslation: CGFloat = sender.translation(in: view).y
// calculate a normalized zoom factor between [-1,1], where up is positive (ie zooming in)
let normalizedZoomFactor = -1 * max(-1,min(1,currentYTranslation / fullRangeDistancePoints))
// calculate effective zoom scale to use
let newZoomFactor = clampZoomFactor(CGFloat(originalZoomLevel) + normalizedZoomFactor /** (maximumZoomFactor - minimumZoomFactor)*/)
print(originalZoomLevel, "<-- what is initialZoom???")
print(newZoomFactor, "<-- what is newZoomFactor???")
// update device's zoom factor'
update(scale: newZoomFactor)
print(lastVal - Int(mapView.centerCoordinate.latitude), " : The change is here")
lastVal = Int(mapView.centerCoordinate.latitude)
print(mapView.centerCoordinate, " : Cenetr courdenate in .changed")
case .ended, .cancelled:
print(originalZoomLevel, "<-- what is this???")
break
default:
break
}
}
}
The problem in your code is in your func update(scale ...)
Replace this code:
mapView.zoomLevel = Double(exactly: factor)!
With this:
mapView.setCenter(centerCoordOrig, zoomLevel: Double(exactly: factor)!, animated: false)
By the way, create a new variable outside for the centerCoordOrig.

Drag object on XZ plane

I am working on an augmented reality app and I would like to be able to drag an object in the space. The problem with the solutions I find here in SO, the ones that suggest using projectPoint/unprojectPoint, is that they produce movement along the XY plane.
I was trying to use the fingers movement on the screen as an offset for x and z coordinates of the node. The problem is that there is a lot of stuff to take in consideration (camera's position, node's position, node's rotation, etc..)
Is there a simpler way of doing this?
I have updated #Alok answer as in my case it is drgging in x plane only from above solution. So i have added y coordinates, working for me.
var PCoordx: Float = 0.0
var PCoordy: Float = 0.0
var PCoordz: Float = 0.0
#objc func handleDragGesture(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
let hitNode = self.sceneView.hitTest(sender.location(in: self.sceneView),
options: nil)
self.PCoordx = (hitNode.first?.worldCoordinates.x)!
self.PCoordy = (hitNode.first?.worldCoordinates.y)!
self.PCoordz = (hitNode.first?.worldCoordinates.z)!
case .changed:
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
let hitNode = sceneView.hitTest(sender.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x,
let coordy = hitNode.first?.worldCoordinates.y,
let coordz = hitNode.first?.worldCoordinates.z {
let action = SCNAction.moveBy(x: CGFloat(coordx - PCoordx),
y: CGFloat(coordy - PCoordy),
z: CGFloat(coordz - PCoordz),
duration: 0.0)
self.photoNode.runAction(action)
self.PCoordx = coordx
self.PCoordy = coordy
self.PCoordz = coordz
}
sender.setTranslation(CGPoint.zero, in: self.sceneView)
case .ended:
self.PCoordx = 0.0
self.PCoordy = 0.0
self.PCoordz = 0.0
default:
break
}
}
first you need to create floor or very large plane few meters (i have 10) below origin. This makes sure your hittest always returns value. Then using pan gesture :
//store previous coordinates from hittest to compare with current ones
var PCoordx: Float = 0.0
var PCoordz: Float = 0.0
#objc func move(_ gestureRecognizer: UIPanGestureRecognizer){
if gestureRecognizer.state == .began{
let hitNode = sceneView.hitTest(gestureRecognizer.location(in: sceneView), options: nil)
PCoordx = (hitNode.first?.worldCoordinates.x)!
PCoordz = (hitNode.first?.worldCoordinates.z)!
}
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
if gestureRecognizer.state == .changed {
let hitNode = sceneView.hitTest(gestureRecognizer.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x{
if let coordz = hitNode.first?.worldCoordinates.z{
let action = SCNAction.moveBy(x: CGFloat(coordx-PCoordx), y: 0, z: CGFloat(coordz-PCoordz), duration: 0.1)
node.runAction(action)
PCoordx = coordx
PCoordz = coordz
}
}
gestureRecognizer.setTranslation(CGPoint.zero, in: sceneView)
}
if gestureRecognizer.state == .ended{
PCoordx = 0
PCoordz = 0
}
}
In my case there is only one node so i have not checked if required node is taped or not. You can always check for it if you have many nodes.
If I understand you correctly, I do this using a UIPanGestureRecognizer added to the ARSCNView.
In my case I want to check if pan was started on a given virtual object and keep track of which it was because I can have multiple, but if you have just one object you may not need the targetNode variable.
The 700 constant I use to divide I got it by trial and error to make the translation smoother, you may need to change it for your case.
Moving finger up, moves the object further away from camera and moving it down moves it nearer. Horizontal movement of fingers moves object left/right.
#objc func onTranslate(_ sender: UIPanGestureRecognizer) {
let position = sender.location(in: scnView)
let state = sender.state
if (state == .failed || state == .cancelled) {
return
}
if (state == .began) {
// Check pan began on a virtual object
if let objectNode = virtualObject(at: position).node {
targetNode = objectNode
latestTranslatePos = position
}
}
else if let _ = targetNode {
// Translate virtual object
let deltaX = Float(position.x - latestTranslatePos!.x)/700
let deltaY = Float(position.y - latestTranslatePos!.y)/700
targetNode!.localTranslate(by: SCNVector3Make(deltaX, 0.0, deltaY))
latestTranslatePos = position
if (state == .ended) {
targetNode = nil
}
}

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.

Resources