Похоже, вы разрешаете пользователю вращаться только по одной или другой оси, а также, возможно, слишком усложняете вещи.
Учитывая некоторый UIView, который содержит подвид "primaryView", который мы хотим повернуть , и подвид "secondaryView", который мы хотим вращать и сдвигать вперед в трехмерном пространстве (действуя как круг в видео, которое вы связали), и учитывая некоторую двухмерную точку и расстояние "перед" этой точкой, мы можем вычислить углы Эйлера X и Y в радианах от этой точки до этой 2D-проекции:
// Note, "self" is some UIView. "primaryView" and "secondaryView"s are subviews of self in the below example:
let somePoint: CGPoint = CGPoint(...)
let distanceInfront: CGFloat = 10
// First we need to convert this point to a coordinate relative to the "center" of what we're trying to orbit around. For your case, this center would be the center of the view itself:
let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let offset = CGPoint(x: center.x - somePoint, y: center.y - somePoint)
// If we picture the problem as if we're looking at it "from the side", we're essentially trying to calculate a 2D angle between a horizontal line and some 2D point to obtain the x rotation angle. The point we're trying to calculate the angle to has an x value of our desired distance, and a y value of the calculated offset's y value:
let xP = CGPoint(x: distance, y: offset.y)
let xAngle = atan2(xP.y, xP.x)
// We can do the same to calculate the y angle, picturing the problem "from above":
let yP = CGPoint(x: distance, y: offset.x)
let yAngle = atan2(yP.y, yP.x)
// Now we can use our calculated x and y angles to compute our transform:
var primaryTransform = CATransform3DIdentity
primaryTransform.m34 = 1 / self.bounds.width
primaryTransform = CATransform3DRotate(primaryTransform, yAngle, 0, 1, 0)
primaryTransform = CATransform3DRotate(primaryTransform, -xAngle, 1, 0, 0)
primaryView.layer.transform = primaryTransform
// Our primary view is now "looking at" the 2D point we've provided, at a distance "distance" in front of our view.
// We can then take that same transform and shift it forwards by our desired distance to compute the transform of the secondaryView (the circle view):
let secondaryTransform = CATransform3DTranslate(primaryTransform, 0, 0, -distance)
secondaryView.layer.transform = secondaryTransform
Вот Swift Playground, которая объединяет все вместе, чтобы продемонстрировать:
import UIKit
import PlaygroundSupport
class OrbitView: UIView {
let primaryView = UIView()
let secondaryView = UIView()
public init(primaryRadius: CGFloat, secondaryRadius: CGFloat) {
super.init(frame: .zero)
primaryView.backgroundColor = .blue
primaryView.layer.cornerRadius = primaryRadius/2.0
primaryView.translatesAutoresizingMaskIntoConstraints = false
addSubview(primaryView)
NSLayoutConstraint.activate([
primaryView.centerXAnchor.constraint(equalTo: centerXAnchor),
primaryView.centerYAnchor.constraint(equalTo: centerYAnchor),
primaryView.widthAnchor.constraint(equalToConstant: primaryRadius),
primaryView.heightAnchor.constraint(equalTo: primaryView.widthAnchor),
])
secondaryView.backgroundColor = .red
secondaryView.layer.cornerRadius = secondaryRadius/2.0
secondaryView.translatesAutoresizingMaskIntoConstraints = false
primaryView.addSubview(secondaryView)
NSLayoutConstraint.activate([
secondaryView.centerXAnchor.constraint(equalTo: centerXAnchor),
secondaryView.centerYAnchor.constraint(equalTo: centerYAnchor),
secondaryView.widthAnchor.constraint(equalToConstant: secondaryRadius),
secondaryView.heightAnchor.constraint(equalTo: secondaryView.widthAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func lookAt(_ location: CGPoint, distanceInfront distance: CGFloat) {
// Compute how far the location is from the center of our view
let center = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let offset = CGPoint(x: center.x - location.x, y: center.y - location.y)
// Calculate the x angle to the point "infront" of us
let xP = CGPoint(x: distance, y: offset.y)
let xAngle = atan2(xP.y, xP.x)
// Calculate the y angle to the point "infront" of us
let yP = CGPoint(x: distance, y: offset.x)
let yAngle = atan2(yP.y, yP.x)
// Construct a transform that rotates our primary subview's layer to point to the location in 3D space
var primaryTransform = CATransform3DIdentity
primaryTransform.m34 = 1 / self.bounds.width
primaryTransform = CATransform3DRotate(primaryTransform, yAngle, 0, 1, 0)
primaryTransform = CATransform3DRotate(primaryTransform, -xAngle, 1, 0, 0)
// Set our primary layer's transform
primaryView.layer.transform = primaryTransform
// Now, shift this primary transform forward by the distance infront of our view we're "looking",
// and apply this transform to our secondary subview
let secondaryTransform = CATransform3DTranslate(primaryTransform, 0, 0, -distance)
secondaryView.layer.transform = secondaryTransform
}
}
class MyViewController : UIViewController {
let orbitView = OrbitView(primaryRadius: 200, secondaryRadius: 50)
override func loadView() {
let view = UIView()
view.backgroundColor = .white
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
// Construct an orbit view for demonstration purposes, and embed it in our view
orbitView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(orbitView)
NSLayoutConstraint.activate([
orbitView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
orbitView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
orbitView.widthAnchor.constraint(equalToConstant: 200),
orbitView.heightAnchor.constraint(equalTo: orbitView.widthAnchor),
])
// We'll use a pan gesture recognizer to update our orbit view
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panRecognized(_:)))
view.addGestureRecognizer(panGesture)
}
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
// Tell our orbit view to "look" at a point in 3D space relative to where we are currently touching
let location = recognizer.location(in: self.orbitView)
orbitView.lookAt(location, distanceInfront: 100)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Обратите внимание, что на приведенном выше GIF-изображении красный кружок «выскакивает» вперед при первом запуске перетаскивания. Вы, вероятно, захотите начать с того, что установите преобразование красного круга (secondaryView) так, чтобы оно было смещено вперед на желаемое расстояние, когда оно создается впервые, чтобы избежать этого.