"For gestures that do not easily match a specific pattern, or when you want to use a gesture recognizer to gather touch input, create a continuous gesture recognizer."
특정 패턴에 일치하기 어려운 제스쳐를 구현하려고 하거나 터치 입력을 모으기 위한 제스쳐 리코그나이저 사용을 원할 때, 연속적 제스쳐 리코그나이저를 생성하시기 바랍니다.
연속적 제스쳐 리코그나이저는 한 곳에서 이벤트 처리 로직을 캡슐화하도록 해주고, 해당 로직을 여러 뷰에서 재사용할 수 있게 해줍니다. 연속적 제스쳐 리코그나이저는 상태 머신 구현에 더 많은 노력을 요구하지만, free-form 입력 캡처와 같은, 이산적 제스쳐 리코그나이저로 구현하기 어려운 작업을 수행하도록 해줍니다.
Figure 1은 free-form 제스쳐를 나타내고 있습니다. 입력이 화면에서 경로를 따라 무언가를 그리기 위해 사용하는 제스쳐입니다. 입력을 캡처하기 위해 팬 제스쳐 리코그나이저를 사용할 수도 있지만, 이는 액션 메소드가 캡처 프로세스의 모든 페이즈를 처리해줘야 합니다. 커스텀 제스쳐 리코그나이저를 사용함으로써 서브클래스의 다양한 메소드에 로직을 분배해 코드를 단순화할 수 있습니다. 커스텀 제스쳐 리코그나이저를 사용하는 것은, 경로를 캡처하기 위한 코드 작성이 가능하다는 것과 여러 뷰에서 재사용할 수 있음을 의미합니다.
Figure 1 A free-form gesture
터치 입력을 캡처하는 커스텀 제스쳐 리코그나이저의 경우 제스쳐의 실패를 발생시키는 명시적인 조건이 없습니다. 대신 제스쳐 리코그나이저는 터치 연쇄가 끝나거나 시스템에 의해 취소될 때까지 터치 입력을 캡처합니다. 제스쳐 리코그나이저의 클라이언트는 버퍼를 보내고 적용하기 위해 액션 메소드를 사용합니다. 예를 들어 클라이언트는 스크린에 경로를 그리기 위해 해당 데이터를 사용하게 될 것입니다. 오직 터치 연쇄가 해당 목표 객체에 성공적으로 마무리될 때에만 앱의 데이터 구조에 영구적으로 데이터를 커밋합니다.
터치 이벤트를 추적하는 연속적 제스쳐 리코그나이저는 해당 정보를 저장할 방법이 필요합니다. 받게 될 UITouch
객체에 레퍼런스를 저장할 수 없습니다. 왜냐하면 UIKit
은 해당 객체를 재사용하고 모든 기존값을 덮어쓰기 때문입니다. 대신 필요한 터치 정보를 저장하려면 커스텀 데이터 구조를 정의해야 합니다.
Listing 1은 StrokeSample
구조체의 정의를 나타내고 있습니다. 이 구조체의 목적은 터치에 연관된 위치를 저장하는 것입니다. 스스로 구현하게 되는 경우 타임스태프나 터치의 압력과 같은 것을 저장하려면 이 구조체에 다른 속성을 추가해야 합니다.
Listing 1 Managing the touch data
struct StrokeSample {
let location: CGPoint
init(location: CGPoint) {
self.location = location
}
}
Listing 2는 터치 정보를 캡처하기 위해 사용되는 TouchCaptureGesture
클래스의 부분 정의를 보여줍니다. 이 클래스는 StrokeSample
구조체들의 포함할 수 있는 배열인 samples
속성에 터치 데이터를 저장합니다. 또한, 클래스는 첫 번째 손가락과 관련이 있는 UITouch
객체를 저장합니다. 그렇게 함으로써 다른 터치를 무시할 수 있습니다. init(coder:)
메소드의 구현은 samples
속성이 인터페이스 빌더 파일로부터 제스쳐를 로딩할 때 적절하게 초기화되는 것을 보장해줍니다.
Listing 2 Properties of the TouchCaptureGesture class
class TouchCaptureGesture: UIGestureRecognizer, NSCoding {
var trackedTouch: UITouch? = nil
var samples = [StrokeSample]()
required init?(coder aDecoder: NSCoder) {
super.init(target: nil, action: nil)
self.samples = [StrokeSample]()
}
func encode(with aCoder: NSCoder) { }
// Overridden methods to come...
}
Listing 3은 TouchCaptureGesture
클래스의 touchesBegan(_:with:)
메소드를 나타내고 있습니다. 제스쳐는 만약 초기 이벤트가 두 터치를 포함하는 경우 즉시 실패합니다. 만약 하나의 터치만 존재한다면, 터치 객체는 trackedTouch
속성에 저장되고, 커스텀 addSample
helper 메소드는 터치 데이터와 함께 새 StrokeSample
구조체를 생성합니다. 첫 번째 터치가 발생한 후 이벤트 연쇄에 추가된 모든 새 터치는 무시됩니다.
Listing 3 Beginning the capture of touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.count != 1 {
self.state = .failed
}
// Capture the first touch and store some information about it.
if self.trackedTouch == nil {
if let firstTouch = touches.first {
self.trackedTouch = firstTouch
self.addSample(for: firstTouch)
state = .began
}
} else {
// Ignore all but the first touch.
for touch in touches {
if touch != self.trackedTouch {
self.ignore(touch, for: event)
}
}
}
}
func addSample(for touch: UITouch) {
let newSample = StrokeSample(location: touch.location(in: self.view))
self.samples.append(newSample)
}
Listing 4에 보이는 touchesMoved(:with:)
와 touchesEnded(
:with:)
메소드는 각각의 새로운 샘플을 기록하고, 제스쳐 리코그나이저의 상태를 업데이트합니다. 상태를 UIGestureRecognizer.State.ended
로 설정하는 것은 상태가 인식되었다고 설정하는 것과 동일합니다. 그리고 이는 제스쳐 리코그나이저의 액션 메소드를 호출하게 됩니다.
Listing 4 Managing the touch input
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
self.addSample(for: touches.first!)
state = .changed
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.addSample(for: touches.first!)
state = .ended
}
항상 touchesCancelled(_:with:)
와 [reset()]
메소드를 제스쳐 리코그나이저 속에 구현해야 합니다. 그리고 이전 것을 깨끗이 하기 위해 이 메소드들을 사용해야 합니다. Listing 5는 TouchCaptureGesture
클래스에 대한 이 메소드들의 구현을 보여주고 있습니다. 메소드들은 제스쳐 리코그나이저의 속성들을 초기값으로 복구합니다.
Listing 5 Cancelling and resetting the continuous gesture
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
self.samples.removeAll()
state = .cancelled
}
override func reset() {
self.samples.removeAll()
self.trackedTouch = nil
}