iOS

[iOS] UIButton의 TouchDrag영역이 이상하다?

스츠흐 2024. 5. 26. 16:59

안녕하세요!

요즘, 대학생 때부터 진행해온 사이드프로젝트 회고를 작성하고 있습니다.

개발자의 입장에서 도입해본 UX 개선을 주제로 글을 적고 있는데, 이전에 작업하면서 '신기하네..'하고 넘어갔던 부분에 대해서 정리해보고자 합니다.

 


 

사용자가 실수를 범할 위험을 줄이기 위해서 액션에 따른 명확한 (시각적)피드백을 주고자 하였습니다.

저희 앱의 기능 중에 양자택일의 간단한 테스트를 진행하면, 함께 여행을 가는 친구들과의 여행 스타일을 비교해주는 기능이 있었습니다.

단순한 테스트이기 때문에, 뒤로가기 버튼 없이 화면이 구성되었습니다.

가벼운 느낌을 주기 위한 의도된 디자인이긴 하지만, 이로 인해서 유저가 실수로 잘못 눌렀을 때 테스트를 처음부터 다시 시작해야 하는 플로우였습니다.

이를 개선하기 위해서 버튼 State 상태에 따라서 선택지 UI가 하이라이팅되도록 구현했습니다.

작업 결과물 (*사이드 프로젝트 회고 중 일부)

 

 

위 gif처럼 선택지를 눌렀다가 드래그해서 안팎으로 이동했을 때, 선택 및 하이라이팅이 취소되도록 개발했습니다.

 

그런데 위 동작을 구현하던 중, UIButton에서 기본으로 제공하는 UIControl Event에서 의아한 점을 발견하였습니다.

아무리 테스트를 해보아도 touchDragExittouchDragEnter이 트리거되는 타이밍이 이상하게 느껴진 것이었습니다.!

💡 touchDragExit / touchDragEnter 란?

 

버튼을 누른 상태로 드래그하여서 버튼 영역을 드나들 때 호출되는 이벤트들입니다.

- touchDragExit은 드래그해서 버튼 영역을 나갔을 때,

- touchDragEnter은 그 상태에서 다시 버튼 영역으로 들어왔을 때 호출됩니다.

Apple 문서에서 정의한 touchDragEnter / touchDragExit

 

 

 

타이밍이 이상하게 느껴진 이유는, 버튼 드래그 영역이 생각보다 훨씬 넓게 잡혀있었기 때문이었습니다.

샘플 프로젝트를 생성해서 10 간격으로 격자를 추가하고, 드래그 Enter와 Exit 영역을 확인해봤습니다.

이 샘플 프로젝트를 여기에서 확인하실 수 있습니다.

 

두 메소드가 트리거되는 지점을 찾아보니 버튼에서 상하좌우로 70까지를 이벤트 영역으로 잡고 있었습니다.

빨간색으로 표시한 선을 드나들 때, touchDragExit과 touchDragEnter가 불렸습니다.

(격자를 세어보면 70에 해당했습니다)

 

 

샘플앱을 고려했을 때, Apple 문서에서 'bounds of the control'이라고 표현한 영역이 버튼의 영역보다 넓은 듯 보였습니다.

그런데 공식문서에서 관련된 구체적인 설명을 찾지는 못했습니다..

스택오버플로우 글에서 손가락 터치이기 때문에 드래그 영역을 넓게 잡은 것 같다는 글만 찾을 수 있었습니다.

(혹시 공식 문서에서 해당 내용을 찾으시면 댓글로 알려주세요 ㅠ.ㅠ)

 

 


 

 

애플에서 의도한 드래그 이벤트 영역이라고 하더라도, 제가 구현하는 화면에는 적합하지 않다고 생각했습니다.

저희 화면에는 두가지 선택지가 크게 위치해있었기 때문에, 그보다 더 넓은 트리거 영역이 상당히 어색하게 느껴졌기 때문입니다..ㅠㅠ

그래서 드래그 이벤트를 커스텀하기로 하였습니다!

 

touchDragExit과 touchDragEnter을 쓰는 대신, touchDragInsidetouchDragOutside를 사용했습니다.

이 메소드들은 drag가 될 때 계속해서 호출되는 메소드입니다.

이 메소드가 불렸을 때, 버튼의 위치와 드래그 위치를 비교하였습니다.

(참고적으로, DragInside↔DragOutside의 경계는 DragExit↔DragEnter의 경계와 동일했습니다.)

// 드래그 이벤트가 필요한 버튼이 많아서 클래스로 생성
class DragDetectButton: UIButton {
    // DragInside<->DragOutside의 경계가 DragExit<->DragEnter의 경계와 동일
    // 두 메소드 모두에서 터치 포지션과 버튼 위치를 비교
    func setDragDetector(label: UILabel) {
        self.addTarget(self, action: #selector(buttonDragged(_:forEvent:)), for: .touchDragInside)
        self.addTarget(self, action: #selector(buttonDragged(_:forEvent:)), for: .touchDragOutside)
    }
    
    @objc func buttonDragged(_ sender: UIButton, forEvent event: UIEvent) {
        guard let touch = event.allTouches?.first else {
            return
        }
        let touchInside = CGRectContainsPoint(sender.bounds, touch.location(in: sender))
        if (touchInside) {
            // inside: select 표시, 필요 시 UI 업데이트
            self.isSelected = true
        } else {
            // outside: deselect 표시, 필요 시 UI 업데이트
            self.isSelected = false
        }
    }
}

 

완성!