SwiftUI旋转3D立方体偏移问题修复方案咨询
问题描述
本人接触SwiftUI仅约一个月,此前一直使用React Native开发项目,对Swift的标准实践不太熟悉。
我用SwiftUI实现了一个可通过手势旋转的3D立方体,但拖动旋转时立方体出现垂直和水平方向的偏移,无法固定在原位(效果参考:
)。
以下是我的代码:
import SwiftUI import UIKit let transforms = [ // Front face CATransform3DMakeTranslation(0, 0, 50), // Right face CATransform3DRotate(CATransform3DMakeTranslation(50, 0, 0), CGFloat.pi / 2, 0, 1, 0), // Top face CATransform3DRotate(CATransform3DMakeTranslation(0, -50, 0), CGFloat.pi / 2, 1, 0, 0), // Bottom face CATransform3DRotate(CATransform3DMakeTranslation(0, 50, 0), -CGFloat.pi / 2, 1, 0, 0), // Left face CATransform3DRotate(CATransform3DMakeTranslation(-50, 0, 0), -CGFloat.pi / 2, 0, 1, 0), // Back face CATransform3DRotate(CATransform3DMakeTranslation(0, 0, -50), CGFloat.pi, 0, 1, 0) ] struct TransformableView: UIViewRepresentable { var views: [AnyView] @Binding var angleX: CGFloat @Binding var angleY: CGFloat func makeUIView(context: Context) -> UIView { let container = UIView() container.backgroundColor = .clear // Apply perspective to the container /* var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 300 container.layer.sublayerTransform = perspective */ for (index, view) in views.enumerated() { let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 100, height: 100) hostingController.view.backgroundColor = .clear hostingController.view.layer.transform = transforms[index] container.addSubview(hostingController.view) } return container } func updateUIView(_ uiView: UIView, context: Context) { let rotationX = CATransform3DMakeRotation(angleX, 1, 0, 0) let rotationY = CATransform3DMakeRotation(angleY, 0, 1, 0) let combinedRotation = CATransform3DConcat(rotationX, rotationY) // ISSUE HERE MAYBE? uiView.layer.anchorPoint = CGPoint(x: 0, y: 0) uiView.layer.sublayerTransform = combinedRotation } } struct ContentView: View { @State private var angleX: CGFloat = 0 @State private var angleY: CGFloat = 0 @State private var lastAngleX: CGFloat = 0 @State private var lastAngleY: CGFloat = 0 var body: some View { TransformableView(views: [ AnyView(Rectangle().fill(Color.red)), AnyView(Rectangle().fill(Color.green)), AnyView(Rectangle().fill(Color.purple)), AnyView(Rectangle().fill(Color.yellow)), AnyView(Rectangle().fill(Color.blue)), AnyView(Rectangle().fill(Color.white)), ], angleX: $angleX, angleY: $angleY) .frame(width: 100, height: 100) .gesture( DragGesture() .onChanged { value in // Update the angles based on the drag amount angleX = lastAngleX + value.translation.height / 300 angleY = lastAngleY + value.translation.width / 300 } .onEnded { value in // Store the last angles when the gesture ends lastAngleX = angleX lastAngleY = angleY } ) .background(.pink) } } #Preview { ContentView() }
我已经尝试过的方案:
- 使用Metal,但需要在立方体各表面放置View,不符合需求;
- 使用
rotational3D。
参考资源:
- 一篇关于用CATransformLayer渲染3D UIView的入门文章;
- Aheze的Prism代码,但该渲染器是模拟3D效果而非真正生成3D立方体。
现在需要修复立方体旋转偏移的问题,让它能锚定在中心旋转。
解决方案
问题出在锚点设置、旋转变换顺序以及容器布局对齐这几个关键点,以下是具体修复步骤:
1. 修正容器视图的锚点与子视图对齐
当前设置锚点为左上角(0,0),导致旋转围绕左上角进行,而非中心。需要将锚点设为中心,并让子视图对齐容器中心:
func makeUIView(context: Context) -> UIView { let container = UIView() container.backgroundColor = .clear // 设置容器锚点为中心 container.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) // 开启透视效果,增强3D观感 var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 300 container.layer.sublayerTransform = perspective for (index, view) in views.enumerated() { let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 100, height: 100) hostingController.view.backgroundColor = .clear hostingController.view.layer.transform = transforms[index] // 子视图中心对齐容器中心 hostingController.view.center = container.center container.addSubview(hostingController.view) } return container }
2. 调整旋转变换的应用逻辑
不需要每次更新视图时重复设置锚点,应在初始化时完成。同时要叠加透视和旋转变换,保证3D效果正确:
func updateUIView(_ uiView: UIView, context: Context) { let rotationX = CATransform3DMakeRotation(angleX, 1, 0, 0) let rotationY = CATransform3DMakeRotation(angleY, 0, 1, 0) let combinedRotation = CATransform3DConcat(rotationX, rotationY) // 保留透视效果,再叠加旋转变换 var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 300 uiView.layer.sublayerTransform = CATransform3DConcat(perspective, combinedRotation) }
3. 确保SwiftUI容器的居中对齐
在ContentView中给TransformableView的frame添加中心对齐参数:
TransformableView(views: [...], angleX: $angleX, angleY: $angleY) .frame(width: 100, height: 100, alignment: .center)
完整修复后的代码
import SwiftUI import UIKit let transforms = [ // Front face CATransform3DMakeTranslation(0, 0, 50), // Right face CATransform3DRotate(CATransform3DMakeTranslation(50, 0, 0), CGFloat.pi / 2, 0, 1, 0), // Top face CATransform3DRotate(CATransform3DMakeTranslation(0, -50, 0), CGFloat.pi / 2, 1, 0, 0), // Bottom face CATransform3DRotate(CATransform3DMakeTranslation(0, 50, 0), -CGFloat.pi / 2, 1, 0, 0), // Left face CATransform3DRotate(CATransform3DMakeTranslation(-50, 0, 0), -CGFloat.pi / 2, 0, 1, 0), // Back face CATransform3DRotate(CATransform3DMakeTranslation(0, 0, -50), CGFloat.pi, 0, 1, 0) ] struct TransformableView: UIViewRepresentable { var views: [AnyView] @Binding var angleX: CGFloat @Binding var angleY: CGFloat func makeUIView(context: Context) -> UIView { let container = UIView() container.backgroundColor = .clear container.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 300 container.layer.sublayerTransform = perspective for (index, view) in views.enumerated() { let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 100, height: 100) hostingController.view.backgroundColor = .clear hostingController.view.layer.transform = transforms[index] hostingController.view.center = container.center container.addSubview(hostingController.view) } return container } func updateUIView(_ uiView: UIView, context: Context) { let rotationX = CATransform3DMakeRotation(angleX, 1, 0, 0) let rotationY = CATransform3DMakeRotation(angleY, 0, 1, 0) let combinedRotation = CATransform3DConcat(rotationX, rotationY) var perspective = CATransform3DIdentity perspective.m34 = -1.0 / 300 uiView.layer.sublayerTransform = CATransform3DConcat(perspective, combinedRotation) } } struct ContentView: View { @State private var angleX: CGFloat = 0 @State private var angleY: CGFloat = 0 @State private var lastAngleX: CGFloat = 0 @State private var lastAngleY: CGFloat = 0 var body: some View { TransformableView(views: [ AnyView(Rectangle().fill(Color.red)), AnyView(Rectangle().fill(Color.green)), AnyView(Rectangle().fill(Color.purple)), AnyView(Rectangle().fill(Color.yellow)), AnyView(Rectangle().fill(Color.blue)), AnyView(Rectangle().fill(Color.white)), ], angleX: $angleX, angleY: $angleY) .frame(width: 100, height: 100, alignment: .center) .gesture( DragGesture() .onChanged { value in angleX = lastAngleX + value.translation.height / 300 angleY = lastAngleY + value.translation.width / 300 } .onEnded { value in lastAngleX = angleX lastAngleY = angleY } ) .background(.pink) } } #Preview { ContentView() }
原理说明
- 锚点设置:将容器锚点设为
(0.5, 0.5),让旋转围绕容器中心进行,从根源避免偏移; - 子视图对齐:每个立方体面的视图中心与容器中心重合,保证立方体几何中心和旋转中心一致;
- 变换顺序:先应用透视效果再叠加旋转,符合3D空间的渲染逻辑,避免变换顺序导致的位置偏移。
内容的提问来源于stack exchange,提问作者Renish
相关产品推荐
相关产品推荐

