HIT해

SwiftUI에서 SceneKit 노드 제어하기: UIViewRepresentable과 Coordinator 패턴 본문

Swift/Swift 개발 노트

SwiftUI에서 SceneKit 노드 제어하기: UIViewRepresentable과 Coordinator 패턴

힛해 2025. 2. 3. 05:34
728x90

개발 배경

기존에는 돌을 터치했을 때만 동작하던 애니메이션이었지만 돌 프로필 조회와 액션이 겹쳐 외부 버튼으로도 제어해야만 했습니다.

문제 상황

기존 코드에서는 3D 오브젝트의 애니메이션이 터치 이벤트에만 국한되어 있었습니다.

class Coordinator: NSObject {
        var parent: DolView
        
// 터치했을때의 액션
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer) {
            let scnView = gestureRecognizer.view as! SCNView
            let location = gestureRecognizer.location(in: scnView)
            let hitResults = scnView.hitTest(location, options: nil)
            
if let parentNode = touchedNode.parent, parentNode.name == "\(parent.selectedFace)" {
    // 애니메이션 로직...
    let rotateAction1 = SCNAction.rotate(by: -2 * .pi, around: SCNVector3(0, 0, 1), duration: 3)
    let moveAction1 = SCNAction.moveBy(x: 4, y: 0, z: 0, duration: 3)
}

 

 

해결 과정

1. Static SCNView 구현

먼저 SCNView를 static 변수로 관리하여 전역적으로 접근할 수 있게 만들었습니다.

struct DolView: UIViewRepresentable {
    private static var sharedSCNView: SCNView?

    func makeUIView(context: Context) -> SCNView {
        let scnView = SCNView()
        DolView.sharedSCNView = scnView
        ...
        return scnView
    }
}

 

2. 애니메이션 로직 분리

기존 터치 이벤트에 있던 애니메이션 로직을 별도의 함수로 분리했습니다.

func rollDol() {
    guard let scnView = DolView.sharedSCNView else { return }
    
    if let faceNode = scnView.scene?.rootNode.childNode(withName: "\(parent.selectedFace)", recursively: true),
       let accessoryNode = scnView.scene?.rootNode.childNode(withName: "\(parent.selectedAccessory) reference", recursively: true) {
        let rotateAction1 = SCNAction.rotate(by: -2 * .pi, around: SCNVector3(0, 0, 1), duration: 3)
       
    }
}

3. Delegate 패턴 구현

UIViewRepresentable의 Coordinator를 활용하여 delegate 패턴을 구현하여 어디서든 함수를 호출할 수 있게 구현했습니다.

struct DolView: UIViewRepresentable {
    func rollDol() {
        makeCoordinator().rollDol()
    }
}

결과

 

동영상왜이래
// UI 로직 (외부 호출 View)
Button("돌 굴리기") {
    dolView.rollDol()
}

 

외부에서도 함수 호출만으로 내부 동작을 실행시킬 수 있게 되었습니다.

 

SCNView를 static으로 관리하며 메모리 사용량이 늘어날줄 알았는데 메모리 사용량은 그대로다. (?)

let dolView = DolView(
                                        selectedFace: $store.selectedFace,
                                        selectedFaceShape: $store.selectedFaceShape,
                                        selectedAccessory: $store.selectedAccessory,
                                        selectedSign: $store.selectedSign,
                                        selectedMail: $store.selectedMail,
                                        selectedNest: $store.selectedNest,
                                        signText: $store.message,
                                        sign: $store.sign,
                                        profile: $store.profile,
                                        mail: $store.mail,
                                        enable: $store.enable,
                                        onImagePicked: { image in
                                            store.send(.captureDol(image))
                                        },
                                        hasRendered: $store.needCapture
                                    )
dolView

Button(action: {dolView.rollDol()})

 

이와같이 변수에 뷰를 담고 해당뷰를 선언해서 함수를 호출해야했습니다.

 

물론 해당 뷰에서만 사용하기에 문제는 없지만 코드 가독성 측면에서 아쉬움이 남습니다.

 

리팩토링 과정에서 개선을 해볼 필요가 있다 생각되네요.