티스토리 뷰

Swift + iOS/iOS

[ios] Handling UIKit Gestures

군옥수수수 2018. 6. 28. 15:53

Handling UIKit Gestures


Gesture recognizer를 사용하는 것은 뷰에서 발생하는 Touch나 Press 이벤트를 다룰수 있는 가장 간단한 방법입니다. 어떤 뷰든간에 한 개 혹은 복수 개의 Gesture에 대한 recognizer를 붙일 수 있습니다. Gesture recognizer는 뷰 위에서 발생하는 일련의 패턴이 존재하는 이벤트들 ( Double-Tap, Swipe, Pinch 등등 )을 처리하기 위해 Target-Action 패턴을 사용하고 이벤트가 발생하면 Target 객체에 이러한 사실을 전달하여 해당 이벤트를 처리할 수 있는 액션 메소드를 호출합니다.


Gesture Recognizer에는 두 종류가 있습니다.


  • 불연속 gesture recognizer


    • 이벤트를 인식한 후 액션 메소드를 한번만 호출
    • UITapGestureRecognizer
  • 연속 gesture recognizer


    • 최초 이벤트 인식 후 이벤트의 변화를 추적하며 액션 메소드를 변화에 맞춰 호출
    • UIPanGestureRecognizer

Configuring a gesture recognizer

Gesture recognizer를 구성하기 위해서는


  1. 스토리보드에서 Gesture recognizer를 뷰 위에 드래그하여 올려 놓습니다.

  2. 액션 메소드를 구현합니다.

  3. 액션 메소드와 Gesture recognizer를 연결합니다.

코드로 이를 구현할 때는 addGestureRecognizer(_:)를 사용합니다.

Responding to Gestures

액션 메소드를 통해 gesture를 적절히 처리해줍니다. 위에서 언급했듯이 불연속적 gesture는 버튼과 같이 한 번의 gesture에 대해 액션 메소드는 한번만 호출됩니다. 연속적 gesture에 대해서는 이벤트를 추적하고 이에 맞게 액션 메소드 역시 여러번 호출됩니다.


UIGestureRecognizer에는 state라는 프로퍼티가 있고 이를 활용하여 액션 메소드를 구성할 수 있습니다. 연속 Gesture recognzier에선 .began, .changed, .ended 그리고 .cancelled 를 사용할 수 있습니다. 예를 들어 .changed 상태에선 뷰의 속성을 임의로 변경시키고 .ended에서는 이를 확정 짓는 등의 행위를 할 수 있습니다.



UIGestureRecognizer

Gesture recognizer는 gesture를 인식하는 로직과 그에 따른 행위를 분리시켜 놓습니다. 그리고 gesture를 인식하거나 인식하고 있는 gesture의 변화가 감지되면 해당 사실을 지정된 객체(Target Object)에 전달합니다.


UIGestureRecognizer의 서브 클래스들로는 다음 클래스들이 있습니다.


  • UITapGestureRecognizer
  • UIPinchGestureRecognizer
  • UIRotationGestureRecognizer
  • UISwipeGestureRecognizer
  • UIPanGestureRecognizer
  • UIScreenEdgePanGestureRecognizer
  • UILongPressGestureRecognizer

UIGestureRecognizer 클래스는 위의 서브 클래스들에서 정의되는 공통되는 행위에 대해 정의하고 있습니다. 또한 UIGestureRecognizerDelegate 프로토콜을 준수하고 있는 객체와도 상호작용이 가능하기 때문에 이들을 활용하여 보다 섬세한 행위들을 정의해줄 수 있습니다.


또한 액션 메소드로는 두 가지 유형을 사용할 수 있습니다.

@IBAction func myActionMethod()
@IBAction func myActionMethod(_ gesture: UIGestureRecognizer)

두 번째 유형처럼 UIGestureRecognizer나 그의 서브 클래스를 인자로 받는다면 gesture의 보다 상세한 정보들을 사용할 수 있습니다. 예를들어 UIRotationGestureRecognizer에 대한 액션 메소드로 인자를 받는다면 회전 각도에 대한 정보를 알 수 있습니다.


이제 Gesture recognizer에서 중요한 부분에 대해 살펴보도록 하겠습니다. 그것은 바로 addGestureRecognizer(_:) 메소드로 뷰와 관계를 갖는 Gesture recognizer는 뷰의 Responder chain에 속하지 않는다는 것입니다.


터치 이벤트를 예를들어 설명하자면 만일 UITapGestureRecognizer의 지정 객체인 뷰 위에서 터치가 발생하면 윈도우는 터치 이벤트를 해당 뷰로 전달하기 전에 먼저 Gesture recognizer로 전달합니다.


만일 등록한 Gesture recognizer와 맞지 않은 이벤트라면 해당 이벤트는 온전히 터치가 발생한 뷰로 전달됩니다. 하지만 Gesture recognizer에 맞는 gesture라면 해당 이벤트는 recognizer에 의해 처리가 되고 뷰로는 전달되지 않습니다. 이를 테스트해보기 위해 직접 코드를 작성해보았습니다.


버튼에 더블 탭 Gesture recognizer를 등록했을 경우입니다.


버튼을 한번만 클릭하면 결과물은 다음과 같습니다.

UIRESPONDER : Button Touch Began
UIRESPONDER : Button Touch Ended

당연한 결과물이죠? 터치 이벤트는 등록된 Gesture recognizer의 패턴과 일치하지 않으므로 터치 이벤트는 온전하게 뷰로 전달이 됩니다. 하지만 더블 클릭을 하면 어떻게 될까요?

UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer
UIRESPONDER : Button Touch Cancelld

터치가 일어나면 우선 손가락이 화면에 처음 접촉한 순간에 대한 이벤트는 정상적으로 전달이 됩니다. 그렇기 때문에 touchesBegan 메소드는 정상적으로 호출이 되는 것입니다. 하지만 그 후 더블 클릭이 일어나면 (빠르게 같은 좌표에 대한 손가락의 접촉과 때어짐이 두번 발생) 이에 대한 이벤트는 doubleTapGesture로 전달이 되어 처리가 되고 UIResponder로 전달되어야 하는 이벤트는 취소 처리가 되는 것입니다.


이런 이벤트 전달의 흐름은 UIGestureRecognizer의 프로피티인 cancelsTouchesInView, delaysTouchesBegan 그리고 delaysTouchesEnded 값에 의해 결정됩니다.


cancelsTouchesInView - Bool 타입의 프로퍼티로 default 값은 true입니다. Gesture Recognizer가 gesture를 인식하면 나머지 터치 정보들을 뷰로 전달하지 않고 이전에 전달된 터치들은 취소됩니다. (touchesCancelled) 하지만 만일 이 값을 false로 할당한다면 gesture를 인신한 후에도 터치 정보를 뷰에 전달하게 됩니다. 이를 코드와 출력 결과물로 살펴보도록 하겠습니다.

UIGestureRecognizercancelsTouchesInView 프로퍼티는 기본적으로 true이기 때문에 이에 대한 출력 결과물은 다음과 같습니다.

UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer
UIRESPONDER : Button Touch Cancelld

하지만 singleTapGesture.cancelsTouchesInView = false 코드를 작성하면 결과물은 다음과 같습니다.

UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer
UIRESPONDER : Button Touch Ended

터치가 끝났다는 정보가 뷰로 전달되었다는 것을 확인할 수 있습니다.


delaysTouchesBegan - Bool 타입의 프로퍼티로 직역하자면 터치 최초 발생 이벤트 전달을 늦춘다는 의미입니다. defatult 값은 false입니다. default 값이 false이기 때문에 터치가 최초 발생한 순간 해당 gesture가 recognizer의 패턴과 하는 것과 무관하게 터치 시작 이벤트가 뷰로 전달되고 touchesBegan 메소드가 호출됩니다.

UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer
UIRESPONDER : Button Touch Cancelld

하지만 값이 true라면 recognizer가 패턴을 검사하는 동안에는 터치의 최초 발생 이벤트를 뷰로 전달하지 않고 보류합니다. 그리고 gesture가 recognizer의 패턴과 일치된다고 판단되면 해당 이벤트는 버려지고 (discard) 뷰로 전달되지 않습니다. 이렇게 애초에 최초 터치 이벤트조차 뷰로 전달되지 않으니 touchesCancelled도 호출되지 않는 것입니다.

UIGestureRecognizer : UITapGestureRecognizer 

하지만 어디까지나 delay (지연)이기 때문에 singleTapGesture가 올라가있는 뷰 위에서 손가락을 최초 터치 후 때지 않고 기다리면 터치의 시작 이벤트는 뷰로 전달되고 touchesBegan 메소드는 호출됩니다.


delaysTouchesEnded - delaysTouchesBegan과 유사한 역할의 프로퍼티로 터치가 끝난 순간의 이벤트 전달을 지연시킬 것인지를 결정하는 프로퍼티입니다. 하지만 delaysTouchesBegan과 다르게 default 값은 true입니다. 이 프로퍼티를 사용해보기 위해 저는 더블 탭을 인지하는 doubleTapGesture를 사용하였습니다. 결과는 제가 기대한 것과는 약간 달랐습니다.

이와 같이 코드를 작성하면 저는 singleTapGesture 와 마찬가지로 touchesEnded 가 정상적으로 호출될 것으로 예상하였으나 결과물은 다음과 같았습니다.

UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer

그리고 버튼은 눌려진 상태를 유지하고 있었습니다. 즉 터치 종료 이벤트가 전달되지 않았다는 것입니다. 물론 그렇다고 취소된 것도 아닌 것이죠. 하지만 delaysTouchesEnded의 공식 문서에는 다음과 같이 설명되어 있었습니다.


Set this property to false to have touch objects in the UITouch.Phase.ended delivered to the view while the gesture recognizer is analyzing the same touches.


저는 더블 탭이 문서에서 언급하는 same touches 라고 이해를 하고 delaysTouchesEnded = false 코드를 추가로 작성해주었고 그제서야 원하는 출력 결과물을 얻을 수 있었습니다.

UIRESPONDER : Button Touch Began
UIRESPONDER : Button Touch Ended
UIRESPONDER : Button Touch Began
UIGestureRecognizer : UITapGestureRecognizer
UIRESPONDER : Button Touch Ended

Subclassing Notes

UIGestureRecognizer 를 상속받아 자신만의 Gesture recognizer를 만들 수 있습니다. 기본적으로 Gesture recognizer는 상태(UIGestureRecognizer.State)에 따른 행동을 취하는데 이러한 상태는 State machine에 의해 결정됩니다. 그렇기 때문에 UIGestureRecognizer를 상속받아 무엇을 만들 때 이런 상태 변화에 대한 정의를 명확히 해주어야 합니다. State machine에 대해서는 밑에서 보다 자세히 다루어볼 것이기 때문에 간단히만 살펴보도록 하겠습니다.


일단 연속적 gesture와 비연속적 gesture의 상태 변화 과정에는 차이가 있습니다. 우선 연속적, 비연속적 gesture와 상관없이 모든 gesture는 이벤트를 처리할 준비가 되었다는 UIGestureRecognizer.State.possible 상태로부터 시작합니다.


그리고 비연속적 gesture는 gesture 인식 성공 여부(recognized)에 따라 UIGestureRecognizer.State.ended인지 UIGestureRecognizer.State.failed 나뉘고 인식에 성공하면 Target object에 액션 메시지를 보냅니다.


연속적 gesture는 조금 더 다양한 상태의 변화를 가질 수 있습니다.


  • Possible ——> Began ——> [Changed] ——> Cancelled
  • Possible ——> Began ——> [Changed] ——> Ended

[Changed] 상태는 일어나지 않을 수도 있으며 Cancelled 혹은 Ended 상태 전에 여러번 발생할 수도 있습니다. 그리고 매 상태 변화마다 Target object에 액션 메시지를 보냅니다.

 

이렇게 UIGestureRecognizer를 사용하고 이해하며 이를 상속받아 자신만의 Gesture recognizer를 만들어 사용하기 위해서는 위에서 언급했던 State machine에 대한 이해가 필요합니다.


그리고 이외에도 UIGestureRecognizer 서브 클래스에서 반드시 재정의해주어야 하는 메소드는 공식 문서에 명시되어 있습니다.


About the Gesture Recognizer State Machine

Gesture recognizer는 기본적으로 State machine에 의해 동작합니다. 이런 State machine은 몇몇 중요한 행위에 대해 결정을 하는 역할을 합니다.


  • 연속적 Gesture recognizer가 UIGestureRecognizer.State.began 상태로 들어갈 수 있는지 여부
  • 비연속적 Gesture recognizer가 UIGestureRecognizer.State.ended 상태로 들어갈 수 있는지 여부
  • Gesture recognizer와 연결된 액션 메소드를 언제 호출할 것인지

UIGestureRecognizer 를 상속받아 직접 클래스를 작성한다면 적절한 시점에 State machine을 갱신해주어야 합니다. 그리고 최종 상태인 UIGestureRecognizer.State.ended , UIGestureRecognizer.State.failed, UIGestureRecognizer.State.cancelled에 다다른 이후 UIKit은 Gesture recognizer를 초기화하고 상태를초기 상태인 UIGestureRecognizer.State.possible 상태로 되돌립니다.

Managing State Transitions for a Discrete Gesture Recognizer

비연속적 Gesture recognizer를 구현한다면 state 프로퍼티의 값의 선택지는 UIGestureRecognizer.State.endedUIGestureRecognizer.State.failed 두 가지가 됩니다. 만일 직접 만든 recognizer의 gesture와 들어온 gesture가 일치한다면 .ended로 그렇지 않다면 .failed로 변경시켜주어야 합니다.


인식에 성공하여 .ended 상태로 변경한다면 UIKit은 연결된 객체의 액션 메소드를 호출합니다. .failed라면 어느 것도 호출하지 않습니다.


비연속적 gesture recognizer를 구현해보는 공식 문서입니다.

Implementing a Discrete Gesture Recognizer

Managing State Transitions for a Continuous Gesture Recognizer

연속적 gesture recognizer에서 상태의 변화에는 크게 세 가지 종류로 나눠볼 수 있습니다.


  1. 최초 이벤트에 따라 UIGestureRecognizer.State.began 이나 UIGestureRecognizer.State.failed로의 상태 변화
  2. 그 이후 이벤트에 따라 UIGestureRecognizer.State.began 에서 UIGestureRecognizer.State.changedUIGestureRecognizer.State.failed로의 상태 변화
  3. 마지막 이벤트에 따라 UIGestureRecognizer.State.ended로의 상태 변화

흐름대로 따라가면서 살펴보도록 하겠습니다.


먼저 .possible 상태에서 gesture가 들어오고 recognizer의 패턴과 일치하는지 검사를 합니다. 일치한다면 .began 으로 상태를 갱신합니다. 만일 패턴과 일치하지 않는다면 즉시 .failed 상태로 변경해야합니다. UIKit은 한번에 하나의 Gesture recognizer만 메시지를 보내도록 허용하기 때문에 .failed 상태로 보내야 다른 Gesture recognizer들에게 그들의 gesture를 처리할 수 있는 기회가 돌아갑니다.


이렇게 초기 패턴이 일치하여 .began 상태로 들어가면 이후의 이벤트들은 .changed로 상태를 갱신합니다. 이미 .changed 상태여도 상태의 변화를 같은 값으로 갱신시키는데 이는 .changed 상태와 연관된 액션 메소드를 지속적으로 호출시키기 위함입니다.


그리고 정상적으로 gesture의 일련의 과정이 정상적으로 종료되면 .ended 상태로 갱신되고 그렇지 않으면 .cancelled 상태로 갱신됩니다.


연속적 gesture recognizer를 구현해보는 공식 문서입니다.

Implementing a Continuous Gesture Recognizer

Handling Cancellation

gesture의 취소 행위는 전화가 걸려오는 상황과 같은 시스템 이벤트에 의해 방해되었을 때 자동으로 발생합니다. 또한 이러한 취소 행위를 직접 코드로 구현할 수도 있습니다. 이러한 취소 행위는 recognizer를 만든 사람이 의도하지 않은 작업들이 발생하는 것을 막아주는 역할을 합니다.


시스템이 gesture를 취소하고나면 UIKit은 touchesCancelledpressesCancelled 메소드를 호출합니다. 이러한 gesture 취소 행위가 일어나면 위에서 언급했듯이 UIGestureRecognizer.State.cancelled로 즉시 상태를 갱신시켜야 하며 이렇게 상태를 변경시켜야 UIKit이 액션 메소드를 호출할 수 있습니다.

Resetting the Gesture Recognizer State Machine

reset() 메소드를 구현하고 사용함으로써 구현한 Gesture recognizer의 상태를 초기 상태로 되돌릴 수 있습니다. 새로운 제스처가 발생하고 해당 이벤트를 전달하기 전이나 State machine의 상태가 .cancelled, .failed, ended 일 경우 UIKit은 reset() 를 호출하여 Gesture recognizer의 상태를 초기화할 뿐만 아니라 State machine을 .possible 상태로 되돌리기도 합니다. 이렇게 .possible 상태로 되돌려 놔야 새로운 이벤트를 받을 수 있기 때문입니다.


마무리

지난 시간 Responder Chain and Touch Event 포스팅에 이어 오늘은 이벤트를 다루는 다른 방법 중 하나인 UIGestureRecognizer에 대해 전반적으로 공부해보았습니다. 가끔 UIGestureRecognizer의 서브 클래스들을 사용하면서 원하는대로 동작을 안하는 경우가 있었는데 그 이유를 알게되는 계기가 되었습니다.


추후에는 UIGestureRecognizer 를 직접 구현해보는 공식 문서를 공부해보고 이를 기록해보도록 하겠습니다. 감사합니다.


참고자료


  1. Apple Developer Documentation

 


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함