들어가기 앞서.. 이번 챕터에 많이 활용할 convert 메서드에 대해 알아봅시다

UIKit에서 각 View는 고유한 좌표계를 가지고 있습니다. 이를 테면, 작은 View에도 scrollView가 추가될 수 있고, scrollView는 View보다 더 큰 영역일 수 있습니다. 이러한 경우, 각 뷰의 좌표계가 서로 다르기 때문에 특정 좌표를 처리하거나 비교하려면 좌표 변환이 필요합니다.

UIView는 좌표 변환을 위해 convert 메서드를 제공합니다. convert 메서드는 크게 두가지 종류가 있습니다.

각 convert 메서드는 CGPoint를 받아 변환된 CGPoint를 반환하거나, CGRect를 받아 변환된 CGRect를 반환하는 메서드가 존재합니다.

스크롤 뷰에서 오브젝트 선택하기

에어플레인의 화이트보드 뷰 계층은 아래와 같이 이루어져 있습니다.

(ToolBar의 경우 scrollView보다 위에 존재하지만, ViewController의 view에 추가되어 있습니다!)

image.png

viewDidLoad 시점에 configureScrollView() 를 통해 scrollView에 TapGesture를 추가했습니다. target을 ViewController로 설정하여, ScrollView에서 제스쳐가 인식되었을 경우, ViewController가 handleScrollViewTapGesture를 실행하도록 했습니다.

private func configureScrollView() {
    let scrollViewTapGestureRecognizer = UITapGestureRecognizer(
        target: self,
        action: #selector(handleScrollViewTapGesture))
    scrollViewTapGestureRecognizer.numberOfTapsRequired = 1
    scrollViewTapGestureRecognizer.isEnabled = true
    scrollView.addGestureRecognizer(scrollViewTapGestureRecognizer)
}

@objc private func handleScrollViewTapGesture(gesture: UITapGestureRecognizer) {
    let location = gesture.location(in: canvasView)

    let touchedView = canvasView.hitTest(location, with: nil)

    if let touchedView = touchedView as? WhiteboardObjectView {
        viewModel.action(input: .selectObject(objectID: touchedView.objectID))
    } else {
        viewModel.action(input: .deselectObject)
    }
}

handleScrollViewTapGesture 내부에서는, hitTest를 통해 오브젝트 뷰를 반환받으면 오브젝트 뷰에 대한 선택 작업을, 반대의 경우에는 선택 해제 작업을 진행하도록 구현했습니다.

오브젝트를 ControlView로 조작하기

각 오브젝트 뷰는 아래와 같이 구성되어 있습니다. 오브젝트 뷰를 중심으로 오브젝트뷰의 bounds 외부에 선택한 사람을 나타내는 ProfileIconVIew와, 오브젝트를 조작할 수 있는 ControlView가 있습니다.

image.png

                                         **ProfileIconView(좌상단), ControlView(우하단)**

ControlView에는 panning을 통해 오브젝트의 크기와 각도를 수정할 수 있는 panGesture가 추가되어 있습니다. 한편, ControlView의 center는 ObjectView의 (maxX, maxY) 점에 위치합니다. 따라서 ControlView의

영역 중 1/4 만 오브젝트 View의 bounds에 위치합니다.

image.png