I'm working on a scrolling algorithm that takes input from a UIPanGestureRecognizer to move a drawn object around in a custom UIView. I want the scrolling to feel like the UIScrollView, instead it feels kind of clunky. I have a problem implementing the case when the finger is lifted after a pan gesture, and the particle decelerates until it stops.
The deceleration caused by the algorithm seems to differ from time to time. Sometimes it seems like it actually accelerates a little before it decelerates.
I'm wondering if you guys have some suggestions to how fix this?
The algorithm works as follows:
When the recognizer state is "began" the current position of the particle is stored in the variable startPosition.
While the recognizer state is "changed" the particles current position is calculated relative to the movement. The current velocity is stored and the view is updated to display the particle in it's new location.
When the recognizer state is "ended" we enter a global thread using the GCD API to calculate the remaining movement (This is to prevent the UI from freezing). A loop is started and set to run at 60 fps. The particles position is updated and the velocity is decreased each iteration. We enter the main thread in order to update the view to display the particle in its new position.
func scroll(recognizer: UIPanGestureRecognizer, view: UIView) {
switch recognizer.state {
case .began:
startPosition = currentPosition
case .changed:
currentPosition = startPosition + recognizer.translation(in: view)
velocity = recognizer.velocity(in: view)
view.setNeedsDisplay()
case .ended:
DispatchQueue.global().async {
let fps: Double = 60
let delayTime: Double = 1 / fps
var frameStart = CACurrentMediaTime()
let friction: CGFloat = 0.9
let tinyValue: CGFloat = 0.001
while (self.velocity > tinyValue) {
self.currentPosition += (self.velocity / CGFloat(fps))
DispatchQueue.main.sync {
view.setNeedsDisplay()
}
self.velocity *= friction
let frameTime = CACurrentMediaTime() - frameStart
if (frameTime < delayTime) {
// calculate time to sleep in μs
usleep(UInt32((delayTime - frameTime) * 1E6))
}
frameStart = CACurrentMediaTime()
}
}
default:
return
}
}
The problem may be that the loop isn't running at a very steady frame rate. But the frame rate looks steady enough to me (I might me wrong). Here is a sample of the calculated frame rate:
"
Frame rate: 59.447833329705766
Frame rate: 57.68833849362473
Frame rate: 57.43794057083063
Frame rate: 53.11410092668673
Frame rate: 51.76492245230155
Frame rate: 52.71845062546561
Frame rate: 50.211233616282904
Frame rate: 59.86028817338459
Frame rate: 55.7360938798143
Frame rate: 47.55385819651489
Frame rate: 50.13437540167264
Frame rate: 48.93274027995551
Frame rate: 50.76905714109756
Frame rate: 57.06095686426517
Frame rate: 49.852101165412876
Frame rate: 51.49043459888154
Frame rate: 55.96442240956844
Frame rate: 53.66651780498373
Frame rate: 55.336349953967726
Frame rate: 51.4698476880566
"
Calculated by adding the following line at the end of the loop, just before updating the frameStart variable:
print("Frame rate: \(1 / (CACurrentMediaTime() - frameStart))")
Any suggestions on how to make the frame rate even more steady?
Race conditions could be the problem, but the currentPosition (which is used to position the particle) variable is protected by a semaphore. And I cannot (to my lack knowledge) se any other critical regions in the code.
var currentPosition: CGPoint {
get {
semaphore.wait()
let pos = _currentPosition
semaphore.signal()
return pos
}
set {
semaphore.wait()
_currentPosition = newValue
semaphore.signal()
}
}
I'm happy to hear any suggestions. Thanks !
CADisplayLinkrather than your own loop. Also, when it comes to frame rate, there's a big difference between 60fps and 59fps since that will have one step of the animation visible for twice as long as the rest making it feel less smooth. - Craig Siemens