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

SwiftUI结合SceneKit:相机缩放与平移手势实现问题求助

解决方案:SwiftUI + SceneKit 双指平移相机实现

核心思路

放弃纯SwiftUI手势的局限,通过UIViewRepresentable封装SceneKit视图,直接在UIKit层面处理手势。SwiftUI的DragGesture在双指场景下易与单指手势冲突,而UIKit的UIPanGestureRecognizer可精准识别双指平移,同时能和已有的单指旋转、双指缩放手势做互斥配置,完全满足自定义控制需求。

具体实现步骤

1. 封装SceneKit视图到UIViewRepresentable

创建SCNViewRepresentable结构体,内部管理SCNView实例,在makeUIView方法中添加所有手势识别器:

struct SCNViewRepresentable: UIViewRepresentable {
    @Binding var scene: SCNScene?

    func makeUIView(context: Context) -> SCNView {
        let scnView = SCNView()
        scnView.scene = scene
        scnView.allowsCameraControl = false

        // 单指旋转物体手势(保留你的原有逻辑,适配UIKit手势)
        let rotateGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleRotate(_:)))
        rotateGesture.minimumNumberOfTouches = 1
        rotateGesture.maximumNumberOfTouches = 1
        scnView.addGestureRecognizer(rotateGesture)

        // 双指缩放手势(替换SwiftUI的MagnificationGesture,UIKit实现更稳定)
        let pinchGesture = UIPinchGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handlePinch(_:)))
        pinchGesture.minimumNumberOfTouches = 2
        pinchGesture.maximumNumberOfTouches = 2
        scnView.addGestureRecognizer(pinchGesture)

        // 双指平移相机XY轴手势
        let panGesture = UIPanGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleCameraPan(_:)))
        panGesture.minimumNumberOfTouches = 2
        panGesture.maximumNumberOfTouches = 2
        // 设置手势互斥,避免冲突
        rotateGesture.require(toFail: panGesture)
        pinchGesture.require(toFail: panGesture)
        scnView.addGestureRecognizer(panGesture)

        scnView.delegate = context.coordinator
        return scnView
    }

    func updateUIView(_ uiView: SCNView, context: Context) {
        uiView.scene = scene
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, SCNSceneRendererDelegate {
        var parent: SCNViewRepresentable
        private var initialCameraPos: SCNVector3?
        private var initialPanTrans: CGPoint?

        init(_ parent: SCNViewRepresentable) {
            self.parent = parent
        }

        // 单指旋转物体处理
        @objc func handleRotate(_ gesture: UIPanGestureRecognizer) {
            guard let scnView = gesture.view as? SCNView,
                  let targetNode = scnView.scene?.rootNode.childNodes.first(where: { $0.name == "YourTargetNode" }) else { return }
            
            let trans = gesture.translation(in: scnView)
            let rotation = SCNVector4Make(Float(trans.y)/100, Float(-trans.x)/100, 0, 1)
            targetNode.transform = SCNMatrix4Mult(targetNode.transform, SCNMatrix4MakeRotation(Float.pi/2, rotation.x, rotation.y, rotation.z))

            if gesture.state == .ended {
                gesture.setTranslation(.zero, in: scnView)
            }
        }

        // 双指缩放相机处理
        @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
            guard let camera = parent.scene?.rootNode.camera else { return }
            
            switch gesture.state {
            case .began, .changed:
                camera.position.z *= Float(gesture.scale)
                gesture.scale = 1.0
            default: break
            }
        }

        // 双指平移相机XY轴处理
        @objc func handleCameraPan(_ gesture: UIPanGestureRecognizer) {
            guard let scnView = gesture.view as? SCNView,
                  let camera = scnView.scene?.rootNode.camera else { return }
            
            let trans = gesture.translation(in: scnView)
            let viewSize = scnView.bounds.size

            switch gesture.state {
            case .began:
                initialCameraPos = camera.position
                initialPanTrans = trans
            case .changed:
                guard let initPos = initialCameraPos, let initTrans = initialPanTrans else { return }
                // 调整系数适配你的场景坐标系
                let xDelta = Float((trans.x - initTrans.x) * 0.01)
                let yDelta = Float((trans.y - initTrans.y) * 0.01)
                // 若场景Y轴向上,可反转yDelta符号
                camera.position = SCNVector3(initPos.x + xDelta, initPos.y - yDelta, initPos.z)
            case .ended:
                initialCameraPos = nil
                initialPanTrans = nil
            default: break
            }
        }
    }
}

2. 在SwiftUI视图中使用封装组件

struct ContentView: View {
    @State private var scene: SCNScene? = SCNScene(named: "YourScene.scn")

    var body: some View {
        SCNViewRepresentable(scene: $scene)
            .ignoresSafeArea()
    }
}

关键注意点

  • 手势互斥:通过require(toFail:)确保双指手势仅在单指手势不触发时生效,彻底避免冲突。
  • 坐标系适配:SceneKit相机坐标系与UIKit屏幕坐标系方向可能差异,需根据你的场景调整平移的正负号和系数。
  • 状态记录:手势开始时记录相机初始位置和手势初始位移,防止平移过程中出现跳变。
  • 性能优化:若场景复杂,可在手势处理中添加scnView.displayIfNeeded(),或用SCNTransaction包裹相机属性更新,保证流畅性。

内容的提问来源于stack exchange,提问作者HBE

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 14:23:13