Don't try to do your animation in draw(rect:)
. That puts all the work on the CPU, not the GPU, and does not take advantage of the hardware-accelerated animation in iOS.
I would suggest instead using a CAShapeLayer
and a CABasicAnimation
to animate your path.
Install the CGPath
from your UIBezierPath
into the CAShapeLayer
, and then create a CABasicAnimation
that changes the path property of the shape layer.
The trick to getting smooth shape animations is to have the same number of control points for every step in the animation. Thus you should not add more and more points to your path, but rather create a new path that contains a graph of the last n points of your waveform.
I would suggest keeping a ring buffer of the n points you want to graph, and building a GCPath/UIBezierPath out of that ring buffer. As you add more points, the older points would "age out" of the ring buffer and you'd always graph the same number of points.
Edit:
Ok, you need something simpler than a ring buffer: Let's call it a lastNElementsBuffer. It should let you add items, discarding the oldest element, and then always return the most recent elements added.
Here is a simple implementation:
public struct LastNItemsBuffer<T> {
fileprivate var array: [T?]
fileprivate var index = 0
public init(count: Int) {
array = [T?](repeating: nil, count: count)
}
public mutating func clear() {
forceToValue(value: nil)
}
public mutating func forceToValue(value: T?) {
let count = array.count
array = [T?](repeating: value, count: count)
}
public mutating func write(_ element: T) {
array[index % array.count] = element
index += 1
}
public func lastNItems() -> [T] {
var result = [T?]()
for loop in 0..<array.count {
result.append(array[(loop+index) % array.count])
}
return result.compactMap { $0 }
}
}
If you create such a buffer of CGFloat values, and populate it with all zeros, you could then start saving new waveform values to it as they are read.
Then you'd create an animation that would create a path using the buffer of values, plus a new value, and then create an animation that shifts the new path to the left, revealing the new point.
I created a demo project on Github that shows the technique. You can download it here: https://github.com/DuncanMC/LastNItemsBufferGraph.git
Here is a sample animation:

Edit #2:
It sounds like you need a slightly different style of animation that what I did in my sample app. You should modify the method buildPath(plusValue:)
in GraphView.swift
to draw the style of graph you desire from the array of sample values (plus an optional new value). The rest of the sample app should work as written.
I updated the app to also offer a bar graph style similar to Apple's Voice memo app:

Edit #3:
In another thread you said you wanted to be able to allow the user to scroll back and forth through the graph, and proposed using a scroll view to manage that process.
The problem there is that your audio sample could be over a long time interval, and so the image of the whole waveform graph could be too large to hold in memory. (Imagine a graph of a 3 minute recording with a data-point for every 1/20th of a second, and each data-point is graphed 10 points wide by 200 points tall. Thats 3 * 60 * 20 = 3600 data points. If you use 10 points horizontally, on a 3X Retina display, that's 30 pixels wide per data point or 108,000 pixels wide, • 200 points • 3X = 600 pixels tall, or 64.8 MILLION pixels. At 3 bytes/pixel, (8 bits/color with no alpha) that's 194.4 million bytes of data, just for a 3 minute recording. Now let's say it's a 2 hour long recording. Time to run out of memory and crash.)
Instead I would say you should save a buffer of data points for your entire recording, scaled down to the smallest data type that would give you one point precision. You could probably use a single byte per data point. Save those points in a struct that also includes the samples/second.
Write a function that takes the graph data struct and time offset as input, and generates a CGPath for that time offset, plus or minus enough data points to make the graph wider than your display window on either side. Then you could animate the graph in either direction for forward or reverse playback. You could implement a tap gesture recognizer for letting the user drag the graph back and forth, or a slider, or whatever you needed. When you get to the end of the current graph, you'd just call your function to generate a new portion of the graph and display that new portion offset to the right screen location.