How to rotate and transform together using UIPanGestureRecognizer with 180 degree like the layer moving on semi-sphere, I have tried doing something and I can transform it in all directions but the transition between directions is not smooth
Briefly I want to make it like in this video
I'm simply coded these class, it works for all directions but the result not as desired:
//
// MoveCircleToolViewController.swift
//
// Created by Coder ACJHP on 17.06.2020.
// Copyright © 2020 Coder ACJHP. All rights reserved.
//
import UIKit
class MoveCircleToolViewController: UIViewController {
var currentAngleX: CGFloat = 0
var currentOffsetX: CGFloat = 0
var currentAngleY: CGFloat = 0
var currentOffsetY: CGFloat = 0
var cardSize: CGSize = .zero
let transformLayer = CATransformLayer()
var directionsFrames = Array<CGRect>()
override func viewDidLoad() {
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
view.addGestureRecognizer(panGesture)
transformLayer.frame = view.bounds
view.layer.addSublayer(transformLayer)
/// Add simple CALayer (circle shape)
addCircleView()
/** Calculate 4 corners TR, TL, BR, BL and store them in array list
to use them inside pan gesture event */
calculateCorners()
}
private func degreeToRadians(degree: CGFloat) -> CGFloat {
return (degree * CGFloat.pi) / 180
}
private func addCircleView() {
let singleSideSize = self.view.bounds.width * 0.18
cardSize = CGSize(width: singleSideSize, height: singleSideSize)
let imageLayer = CALayer()
let origin = CGPoint(x: (view.bounds.width / 2) - (cardSize.width / 2),
y: (view.bounds.height / 2) - (cardSize.height / 2))
imageLayer.frame = CGRect(origin: origin, size: cardSize)
imageLayer.contentsGravity = .resizeAspectFill
imageLayer.borderColor = UIColor.cyan.cgColor
imageLayer.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
imageLayer.borderWidth = 3.0
imageLayer.cornerRadius = cardSize.width / 2
imageLayer.masksToBounds = true
imageLayer.isDoubleSided = true
transformLayer.addSublayer(imageLayer)
}
private func calculateCorners() {
let quarterW = self.view.bounds.width / 2
let quarterH = self.view.bounds.height / 2
let topLeftRect = CGRect(x: 0,
y: 0,
width: quarterW - cardSize.width / 2,
height: quarterH - cardSize.height / 2)
let topRightRect = CGRect(x: topLeftRect.width + cardSize.width,
y: 0,
width: quarterW - cardSize.width / 2,
height: quarterH - cardSize.height / 2)
let bottomLeftRect = CGRect(x: 0,
y: topLeftRect.height + cardSize.height,
width: quarterW - cardSize.width / 2,
height: quarterH - cardSize.height / 2)
let bottomRightRect = CGRect(x: bottomLeftRect.width + cardSize.height,
y: topRightRect.height + cardSize.height,
width: quarterW - cardSize.width / 2,
height: quarterH - cardSize.height / 2)
directionsFrames.append(topLeftRect)
directionsFrames.append(topRightRect)
directionsFrames.append(bottomLeftRect)
directionsFrames.append(bottomRightRect)
}
@objc
private func handlePan(_ gestureRecognier: UIPanGestureRecognizer) {
let translationPoint = gestureRecognier.translation(in: view)
let location = gestureRecognier.location(in: view)
/// Calculate X and Y offset for animation
let xOffset = gestureRecognier.translation(in: view).x
let yOffset = gestureRecognier.translation(in: view).y
/// Reset offsets
if gestureRecognier.state == .began {
currentOffsetX = 0
currentOffsetY = 0
}
/// Calculate angle for rotation X
let xDifference = xOffset * 0.6 - currentOffsetX
currentOffsetX += xDifference
currentAngleX += xDifference
let angleOffsetX = currentAngleX
/// Calculate angle for rotation Y
let yDifference = yOffset * 0.6 + currentOffsetY
currentOffsetY -= yDifference
currentAngleY -= yDifference
let angleOffsetY = currentAngleY
/// Create transform object
var transform = CATransform3DIdentity
transform.m34 = -1 / self.view.bounds.width
// Top Left
if directionsFrames[0].contains(location) {
transform = CATransform3DRotate(transform, degreeToRadians(degree: 30), 1, 0, 0)
transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)
// Top Right
} else if directionsFrames[1].contains(location) {
transform = CATransform3DRotate(transform, degreeToRadians(degree: 30), 1, 0, 0)
transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)
// Bottom Left
} else if directionsFrames[2].contains(location) {
transform = CATransform3DRotate(transform, degreeToRadians(degree: -30), 1, 0, 0)
transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)
// Bottom Right
} else if directionsFrames[3].contains(location) {
transform = CATransform3DRotate(transform, degreeToRadians(degree: -30), 1, 0, 0)
transform = CATransform3DTranslate(transform, translationPoint.x, translationPoint.y, 200)
} else {
if let direction = gestureRecognier.direction {
switch direction {
case .Left, .Right:
transform = CATransform3DRotate(transform, degreeToRadians(degree: angleOffsetX), 0, 1, 0)
transform = CATransform3DTranslate(transform, 0, 0, 200)
case .Up, .Down:
transform = CATransform3DRotate(transform, degreeToRadians(degree: angleOffsetY), 1, 0, 0)
transform = CATransform3DTranslate(transform, 0, 0, 200)
}
}
}
CATransaction.setAnimationDuration(0)
transformLayer.transform = transform
}
}
public extension UIPanGestureRecognizer {
enum PanDirection: Int {
case Up, Down, Left, Right
public var isVertical: Bool { return [.Up, .Down].contains(self) }
public var isHorizontal: Bool { return !isVertical }
}
var direction: PanDirection? {
let translation = self.translation(in: view)
let isVertical = abs(translation.y) > abs(translation.x)
switch (isVertical, translation.x, translation.y) {
case (true, _, let y) where y < 0: return .Up
case (true, _, let y) where y > 0: return .Down
case (false, let x, _) where x > 0: return .Right
case (false, let x, _) where x < 0: return .Left
default: return nil
}
}
}
Thanks in advance
It looks like you're only allowing the user to rotate on one axis or the other, as well as possibly overcomplicating things.
Given some UIView that contains a "primaryView" subview that we want to rotate, and a "secondaryView" subview that we want to both rotate and shift forwards in 3D space (acting as the circle in the video you linked), and given some 2D point, and a distance "in front" of that point, we can calculate the X and Y Euler angles in radians from that point to that 2D projected point:
Here is a Swift Playground that wraps it all together to demonstrate:
Note in the above GIF the red circle "pops" forward the first time we start dragging. You'll probably want to start off by setting the red circle (secondaryView)'s transform to be shifted forward by your desired distance when it is first created to avoid this.