Dear Sirs,
I'm writing an audio application that should show up to 128 horizontal peakmeters (width for each is about 150, height is 8) stacked inside a ScrollViewReader. For the actual value of the peakmeter I have a binding to a CGFloat value. The peakmeter works as expected and is refreshing correct. For testing I added a timer to my swift application that is firing every 0.05 secs, meaning I want to show 20 values per second. Inside the timer func I'm just creating random CGFloat values in range of 0...1 for the bound values. The peakmeters refresh and flicker as expected but I can see a CPU load of 40-50% in the activity monitor on my MacBook Air with Apple M2 even when compiled in release mode. I think this is quite high and I'd like to reduce this CPU load. Should this be possible? I.e. I thought about blocking the refresh until I've set all values? How could this be done and would it help? What else could I do?
Thanks and best regards, JFreyberger
This example draws only in the visible window. The load is 10% at 128 stripes. The gain will be at 2048 stripes, where the load will decrease from 45% to 35%. It seems to me that you can't squeeze more out of this - 10% is the ceiling and this is an acceptable result for SWiftUI.
- You can always reduce the number of stripes - in real life you don't need that much.
- You can slow down the timer from 1/24 to 1/12.
- You can remake it to UIKit/AppKit.
- You can make virtual scrolling - this is when nothing scrolls, and changes occur only on the image or in any separate area.
In any case, I helped both myself and you. I need exactly the same functionality and was interested in how much can be squeezed out of optimization.
import SwiftUI
@main struct app: App {
static var ITEM_HEIGHT: CGFloat = 10.0
var equalizerState = EqualizerState()
@State var canvasMinY: CGFloat = 0
@State var canvasMaxY: CGFloat = 0
var body: some Scene {
WindowGroup {
ScrollView(.vertical) {
Canvas { context, size in
for index in 0 ..< EqualizerState.MAX_ITEMS {
let h = Self.ITEM_HEIGHT
let y = h * CGFloat(index)
if (self.canvasMinY ... self.canvasMaxY).contains(y) {
let value = self.equalizerState.values[index]
let wPart = size.width / 3
let wFull = size.width * value
let x1 = wPart * 0
let x2 = wPart * 1
let x3 = wPart * 2
let w1 = wFull - (wPart * 0)
let w2 = wFull - (wPart * 1)
let w3 = wFull - (wPart * 2)
if (0.00 ... 1.00).contains(value) { context.fill(Path(CGRect(x: x1, y: y, width: w1, height: h)), with: .color(.green )) }
if (0.33 ... 1.00).contains(value) { context.fill(Path(CGRect(x: x2, y: y, width: w2, height: h)), with: .color(.yellow)) }
if (0.66 ... 1.00).contains(value) { context.fill(Path(CGRect(x: x3, y: y, width: w3, height: h)), with: .color(.red )) }
}
}
}.frame(height: Self.ITEM_HEIGHT * CGFloat(EqualizerState.MAX_ITEMS))
}.onScrollGeometryChange(for: Bool.self) { geometry in
self.canvasMinY = geometry.bounds.minY
self.canvasMaxY = geometry.bounds.maxY
return true
} action: { _, _ in }
.frame(width: 150)
.background(.gray)
.padding(.vertical, 12)
}
}
}
@Observable final class EqualizerState {
static public let MAX_ITEMS: Int = 128
@ObservationIgnored private var timer: Timer? = nil
var values: [CGFloat] = []
init() {
self.values = Array(
repeating: 0.0,
count: Self.MAX_ITEMS
)
self.timer = Timer(
timeInterval: 1 / 24,
repeats: true,
block: { _ in
for index in 0 ..< Self.MAX_ITEMS {
self.values[index] = CGFloat.random(
in: 0...1
)
}
}
)
self.timer!.tolerance = 0.0
RunLoop.current.add(
self.timer!,
forMode: .common
)
}
}