You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 12:54:55