"Learn how to create a simple app that incorporates predicted touches into its drawing code."
예측된 터치를 드로잉 코드에 통합시키는 간단한 앱 생성 방법을 알아봅니다.
샘플 앱인 Speed Sketch
(Leveraging Touch Input for Drawing Apps를 보시기 바랍니다)는 드로잉할 때 지연을 최소화하고자 예측된 터치를 사용합니다. 애플 펜슬 혹은 손가락을 사용할 때 모두를 포함합니다. 터치를 수집하는 핵심 클래스는 StrokeGestureRecognizer
클래스입니다. 각 터치 이벤트들의 새로운 연쇄는 앱의 드로잉 캔버스에 Stroke
객체의 생성을 초래합니다. Stroke
객체는 선 드로잉에서 스타일이 적용되는 데 필요한 터치 데이터를 저장하고, 해당 데이터를 캘리그라피 펜, regular 펜, 각각의 구분되는 터치 이벤트에 대한 선 세그먼트들을 그리는 특수한 디버그 모드를 사용해 렌더링할 수 있습니다.
Figure 1 Speed Sketch drawing modes
StrokeGestureRecognizer
클래스는 드로잉 관련 터치 입력을 수집하고, 이를 사용해 렌더를 위한 경로를 나타내는 Stroke
객체를 생성합니다. 실제로 발생한 터치와 더불어 이 클래스는 모든 예측된 터치들도 수집합니다. Listing 1은 제스쳐 리코그나이저의 예측된 터치들을 수집하는 데 책임을 지닌 append 메소드 일부분을 보여주고 있습니다. 이 코드에 의해 호출된 컬렉터 블록은 각각의 터치 이벤트를 처리합니다. 해당 블록의 파라미터는 터치가 실제 터치인지 예측된 터치인지를 나타냅니다.
Listing 1 Collecting predicted touches in Speed Sketch
// Collect predicted touches only while the gesture is ongoing.
if (usesPredictedSamples && stroke.state == .active) {
if let predictedTouches = event?.predictedTouches(for: touchToAppend) {
for touch in predictedTouches {
collector(stroke, touch, view, false, true)
}
}
}
터치 입력의 컬렉션은 StrokeSample
객체를 생성하고, 이 객체는 현재 Stroke
객체에 추가됩니다. Stroke
객체는 다른 터치로부터 분리해서 예측된 터치를 저장합니다. 분리하는 것은 이후에 이들을 삭제하는 것을 더 쉽게 해주고, 실제 터치 입력과 혼동되지 않도록 해줍니다. 앱이 실제 터치의 집합을 추가할 때마다, 예측된 샘플의 집합은 버립니다.
Listing 2는 Stroke
클래스의 일부분을 보여줍니다. 이 부분은 그려진 하나의 선과 관련이 있는 터치들을 나타냅니다. 터치의 각 집합의 경우 클래스는 샘플의 주요 리스트에 실제 터치를 추가합니다. 모든 예측된 터치는 predictedSamples
속성에 저장됩니다. StrokeGestureRecognizer
가 Stroke
메소드 추가를 호출할 때마다 메소드는 예측된 터치의 마지막 집합을 previousPredictedSamples
로 이동시키고, 궁극적으로는 버려집니다. 그러므로 Stroke
는 예측된 터치의 마지막 집합만 유지합니다.
Listing 2 Managing predicted samples in the Stroke
class
class Stroke {
static let calligraphyFallbackAzimuthUnitVector = CGVector(dx: 1.0, dy:1.0).normalize!
var samples: [StrokeSample] = []
var predictedSamples: [StrokeSample] = []
var previousPredictedSamples: [StrokeSample]?
var state: StrokeState = .active
var sampleIndicesExpectingUpdates = Set<Int>()
var expectsAltitudeAzimuthBackfill = false
var hasUpdatesFromStartTo: Int?
var hasUpdatesAtEndFrom: Int?
var receivedAllNeededUpdatesBlock: (() -> ())?
func add(sample: StrokeSample) -> Int {
let resultIndex = samples.count
if hasUpdatesAtEndFrom == nil {
hasUpdatesAtEndFrom = resultIndex
}
samples.append(sample)
if previousPredictedSamples == nil {
previousPredictedSamples = predictedSamples
}
if sample.estimatedPropertiesExpectingUpdates != [] {
sampleIndicesExpectingUpdates.insert(resultIndex)
}
predictedSamples.removeAll()
return resultIndex
}
func addPredicted(sample: StrokeSample) {
predictedSamples.append(sample)
}
func clearUpdateInfo() {
hasUpdatesFromStartTo = nil
hasUpdatesAtEndFrom = nil
previousPredictedSamples = nil
}
// Other methods...
}
렌더링이 진행되는 동안 앱은 예측된 터치를 실제 터치처럼 다룹니다. 이는 각 Stroke
객체의 컨텐츠를 하나 혹은 하나 이상의 StrokeSegment
객체로 나눕니다. StrokeSegment
객체는 드로잉 코드가 StrokeSegmentIterator
객체를 사용해 가져오는 객체입니다. Listing 3은 이 클래스의 구현을 보여주고 있습니다. 드로잉 코드가 stroke
샘플을 반복할 때, sampleAt
메소드는 실제 터치에 대한 샘플을 반환합니다. 메소드가 실제 터치를 반환한 이후에만 이터레이터가 모든 예측된 터치를 반환합니다. 그러므로 예측된 터치는 항상 stroked 선의 끝에 위치해야 합니다.
Listing 3 Fetching predicted touches during drawing
class StrokeSegmentIterator: IteratorProtocol {
private let stroke: Stroke
private var nextIndex: Int
private let sampleCount: Int
private let predictedSampleCount: Int
private var segment: StrokeSegment!
init(stroke: Stroke) {
self.stroke = stroke
nextIndex = 1
sampleCount = stroke.samples.count
predictedSampleCount = stroke.predictedSamples.count
if (predictedSampleCount + sampleCount > 1) {
segment = StrokeSegment(sample: sampleAt(0)!)
segment.advanceWithSample(incomingSample: sampleAt(1))
}
}
func sampleAt(_ index: Int) -> StrokeSample? {
if (index < sampleCount) {
return stroke.samples[index]
}
let predictedIndex = index - sampleCount
if predictedIndex < predictedSampleCount {
return stroke.predictedSamples[predictedIndex]
} else {
return nil
}
}
func next() -> StrokeSegment? {
nextIndex += 1
if let segment = self.segment {
if segment.advanceWithSample(incomingSample: sampleAt(nextIndex)) {
return segment
}
}
return nil
}
}