HIT해

3D 오브젝트 애니메이션 제어를 위한 Delegate 패턴 구현하기 본문

Swift/UIKit 개발 노트

3D 오브젝트 애니메이션 제어를 위한 Delegate 패턴 구현하기

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

개발 배경 및 문제 상황

SwiftUI와 SceneKit을 사용하여 3D 오브젝트(돌)의 애니메이션을 구현했습니다.

 

기존에는 사용자가 화면에서 돌을 직접 터치했을 때만 애니메이션이 동작했지만, 돌 프로필 조회와 애니메이션 액션이 겹치는 문제가 발생했습니다.

 

이로 인해 외부 UI 버튼으로도 동일한 애니메이션을 제어할 필요성이 생겼습니다.

 

문제는 기존 코드에서 3D 오브젝트 애니메이션이 UIKit의 터치 이벤트에만 연결되어 있어 SwiftUI의 버튼 등 다른 UI 요소에서 같은 애니메이션을 트리거하기 어려웠다는 점입니다.

 

이를 해결하기위해 Delegate 패턴을 활용했습니다.

 

Delegate 패턴이란?

Delegate 패턴은 객체 간의 통신을 위한 디자인 패턴으로, 한 객체가 특정 작업을 다른 객체에게 위임하는 방식이다.

 

  • 관심사 분리: 객체가 자신의 주요 기능에만 집중하고, 다른 기능은 위임할 수 있다.
  • 느슨한 결합: 객체 간의 의존성을 줄이고 코드의 재사용성을 높인다
  • 확장성: 코드 변경 없이 새로운 기능을 추가할 수 있다.

 

 

해결 과정

1. Static SCNView 구현

첫 번째 단계는 SCNView에 전역적으로 접근할 수 있도록 static 변수로 관리하는 것입니다:

struct DolView: UIViewRepresentable {
    // 어디서든 접근 가능한 shared SCNView 인스턴스
    private static var sharedSCNView: SCNView?
    
    // 선택된 면과 액세서리를 저장하는 @State 변수들
    @Binding var selectedFace: String
    @Binding var selectedAccessory: String
    
    func makeUIView(context: Context) -> SCNView {
        let scnView = SCNView()
        // 생성된 SCNView를 정적 변수에 저장
        DolView.sharedSCNView = scnView
        
        // SCNView 설정 코드...
        
        return scnView
    }
    
    func updateUIView(_ uiView: SCNView, context: Context) {
        // UI 업데이트 코드...
    }
}

2. Coordinator 클래스를 활용한 Delegate 패턴 구현

Coordinator 클래스를 확장하여 애니메이션 로직을 처리하는 메서드를 추가했습니다

extension DolView {
    class Coordinator: NSObject {
        var parent: DolView
        
        init(_ parent: DolView) {
            self.parent = parent
        }
        
        // 터치 이벤트 핸들러
        @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 firstHit = hitResults.first {
                let touchedNode = firstHit.node
                
                // 돌의 특정 면이 터치되었는지 확인
                if let parentNode = touchedNode.parent, parentNode.name == "\(parent.selectedFace)" {
                    // 애니메이션 실행
                    rollDol()
                }
            }
        }
        
        // 애니메이션 로직을 별도 메서드로 분리
        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 rotateAction = SCNAction.rotate(by: -2 * .pi, around: SCNVector3(0, 0, 1), duration: 3)
                
                // 이동 애니메이션
                let moveAction = SCNAction.moveBy(x: 4, y: 0, z: 0, duration: 3)
                
                // 애니메이션 실행
                faceNode.runAction(rotateAction)
                accessoryNode.runAction(moveAction)
            }
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
}

 

3. 외부에서 호출 가능한 인터페이스 제공

extension DolView {
    // SwiftUI 외부에서 호출할 수 있는 애니메이션 메서드
    func rollDol() {
        makeCoordinator().rollDol()
    }
}

결과물

 

동영상왜이래
struct ContentView: View 
    // DolView에 대한 참조를 저장
    @State private var dolViewRef: DolView?
    
    var body: some View {
        VStack {
            // DolView 생성 및 참조 저장
            DolView()
                .onViewCreated { view in
                    self.dolViewRef = view as? DolView
                }
            
            // 외부 버튼으로 애니메이션 제어
            Button("돌 굴리기") {
                dolViewRef?.rollDol()
            }
        }
    }
}

 

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

 

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