"If your gesture involves a specific pattern of events, consider implementing a discrete gesture recognizer for it."
만약 제스쳐가 특정 패턴과 관련이 있다면, 이를 위해 이산적 제스쳐 리코그나이저 구현을 고려해보시기 바랍니다.
제스쳐 리코그나이저는 이벤트가 이벤트 성공 혹은 실패를 나타내기 전까지 UIGestureRecognizer.State.possible
상태로 남아있습니다. 이 시점에서 상태가 변경될 것입니다. 이산적 제스쳐 리코그나이저의 장점은 적은 상태 전환만을 요구하기 때문에 구현이 쉽다는 것입니다. 단점은 이벤트 연쇄 후에 상태 변경이 발생하기 때문에, 같은 뷰에 존재하는 연속적 제스쳐에 의해 인식이 선점될 수 있다는 것입니다.
Figure 1은 손가락이 오른쪽 아래로 내려갔다가 오른쪽 위로 올라가는 것을 추적하는 체크마크 체스쳐를 나타냅니다. 제스쳐는 특정 경로를 따라가기 때문에 이산적 제스쳐 리코그나이저를 사용하는 좋습니다.
Figure 1 A custom checkmark gesture
제스쳐 리코그나이저 코드를 구현하기 전에 어떤 인식이 발생되어야 하는지에 대한 조건을 정의해야 합니다. 체크마크 제스쳐에 일치하는 조건은 아래와 같습니다.
정의된 조건과 함께 필요한 정보를 추적하기 위해서 제스쳐 리코크나이저에 속성들을 추가해야 합니다. 체크마크 제스쳐의 경우 제스쳐 리코그나이저는 제스쳐의 시작점을 알 필요가 있습니다. 그렇게 함으로써 마지막 지점과 시작 지점을 비교할 수 있습니다. 사용자의 손가락이 아래 혹은 위 방향으로 움직이는지에 대해서 알아야 할 필요도 있습니다.
Listing 1은 커스텀 CheckmarkGestureRecognizer
클래스 정의의 첫 번째 부분을 나타내고 있습니다. 이 클래스는 초기 터치 지점과 제스쳐의 현재 페이즈를 저장합니다. 또한, 클래스는 첫 번째 손가락과 관련이 있는 UITouch
객체를 저장합니다. 그렇게 함으로써 다른 터치는 무시할 수 있게 됩니다.
Listing 1 Beginning of the CheckmarkGestureRecognizer class
enum CheckmarkPhases {
case notStarted
case initialPoint
case downStroke
case upStroke
}
class CheckmarkGestureRecognizer : UIGestureRecognizer {
var strokePhase : CheckmarkPhases = .notStarted
var initialTouchPoint : CGPoint = CGPoint.zero
var trackedTouch : UITouch? = nil
// Overridden methods to come...
Listing 2는 제스쳐를 인식하기 위한 초기 조건을 설정하는 touchesBegan(_:with:)
메소드를 나타내고 있습니다. 제스쳐는 초기 이벤트가 두 손가락을 포함하는 경우 즉시 실패합니다. 만약 하나의 터치만 있으면, 터치 객체는 trackedTouch
속성에 저장됩니다. UIKit
은 UITouch
를 재사용하고, 이 객체의 객체에 덮어씁니다. 그리고 이 메소드는 initialTouchPoint
속성에 터치의 위치를 저장하기도 합니다. 첫 번재 터치가 발생항 후 이벤트 연쇄에 추가된 모든 새 터치는 무시됩니다.
Listing 2 Getting the first touch
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if touches.count != 1 {
self.state = .failed
}
// Capture the first touch and store some information about it.
if self.trackedTouch == nil {
self.trackedTouch = touches.first
self.strokePhase = .initialPoint
self.initialTouchPoint = (self.trackedTouch?.location(in: self.view))!
} else {
// Ignore all but the first touch.
for touch in touches {
if touch != self.trackedTouch {
self.ignore(touch, for: event)
}
}
}
}
터치 정보가 변경되면 UIKit
은 touchesMoved(_:with:)
메소드를 호출합니다. Listing 3은 체크마크 제스쳐를 위한 이 메소드의 구현을 보여줍니다. 이 메소드는 첫 번째 터치가 정확히 하나인지 검증합니다. 이 터치는 정확히 하나여야 합니다. 왜냐하면 모든 후속 터치가 무시되었기 때문입니다. 초기 움직임이 오른쪽 아래를 향하면, 이 메소드는 strokePhase
속성을 downStroke
로 설정합니다. 움직임이 방향을 변경시키고 위를 향하길 시작하게 될 때, 메소드는 스트로크 페이즈를 upStroke
로 변경시킵니다. 만약 제스쳐가 이와 같은 움직임과 다른 패턴을 보인다면, 메소드는 제스쳐의 상태를 실패한 것으로 설정합니다.
Listing 3 Tracking the touch movement
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
let newTouch = touches.first
// There should be only the first touch.
guard newTouch == self.trackedTouch else {
self.state = .failed
return
}
let newPoint = (newTouch?.location(in: self.view))!
let previousPoint = (newTouch?.previousLocation(in: self.view))!
if self.strokePhase == .initialPoint {
// Make sure the initial movement is down and to the right.
if newPoint.x >= initialTouchPoint.x && newPoint.y >= initialTouchPoint.y {
self.strokePhase = .downStroke
} else { self.state = .failed
}
} else if self.strokePhase == .downStroke {
// Always keep moving left to right.
if newPoint.x >= previousPoint.x {
// If the y direction changes, the gesture is moving up again.
// Otherwise, the down stroke continues.
if newPoint.y < previousPoint.y {
self.strokePhase = .upStroke
}
} else {
// If the new x value is to the left, the gesture fails.
self.state = .failed
}
} else if self.strokePhase == .upStroke {
// If the new x value is to the left, or the new y value
// changed directions again, the gesture fails.]
if newPoint.x < previousPoint.x || newPoint.y > previousPoint.y {
self.state = .failed
}
}
}
터치 연쇄의 마지막 시점에 UIKit
은 touchesEnded(_:with:)
메소드를 호출합니다. Listing 4는 체크마크 제스쳐를 위한 이 메소드의 구현을 보여줍니다. 만약 제스쳐가 이미 실패했다면, 이 메소드는 제스쳐가 끝날 때 위로 움직이고 있었는지를 확인하고, 초기 지점보다 마지막 지점이 더 높은지 확인합니다. 만약 두 조건이 true
이면, 메소드는 인식된 것으로 상태를 설정합니다. 그렇지 않으면 제스쳐는 실패합니다.
Listing 4 Determining whether the gesture succeeded
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
let newTouch = touches.first
let newPoint = (newTouch?.location(in: self.view))!
// There should be only the first touch.
guard newTouch == self.trackedTouch else {
self.state = .failed
return
}
// If the stroke was moving up and the final point is
// above the initial point, the gesture succeeds.
if self.state == .possible &&
self.strokePhase == .upStroke &&
newPoint.y < initialTouchPoint.y {
self.state = .recognized
} else {
self.state = .failed
}
}
터치를 추적하는 것과 더불어 CheckmarkGestureRecognizer
클래스는 touchesCancelled(_:with:)
와 reset()
메소드를 구현합니다. 클래스는 제스쳐 리코그나이저의 지역 속성을 적합한 값으로 재설정하기 위해 이 메소드를 사용합니다. Listing 5는 이 메소드들의 구현을 나타내고 있습니다.
Listing 5 Cancelling and resetting the discrete gesture
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.initialTouchPoint = CGPoint.zero
self.strokePhase = .notStarted
self.trackedTouch = nil
self.state = .cancelled
}
override func reset() {
super.reset()
self.initialTouchPoint = CGPoint.zero
self.strokePhase = .notStarted
self.trackedTouch = nil
}