I am also creating a music app on SwiftUI and I also encountered a speed problem.
I have rewritten your code to a modern style and here it is:
import SwiftUI
@main struct app: App {
var peakmeterState = PeakmeterState()
static var ITEM_HEIGHT: CGFloat = 10.0
var body: some Scene {
WindowGroup {
GeometryReader { geometry in
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 1) {
let _ : Bool = {
print("FRAME REDRAW \(self.peakmeterState.frameNum)")
return true
}()
ForEach(0 ..< PeakmeterState.MAX_ITEMS, id: \.self) { index in
let width = geometry.size.width
let value = self.peakmeterState.peakValues[index]
Canvas { context, size in
context.fill(Path(CGRect(x: width / 3 * 0, y: 0, width: width / 3, height: Self.ITEM_HEIGHT)), with: .color(.green))
context.fill(Path(CGRect(x: width / 3 * 1, y: 0, width: width / 3, height: Self.ITEM_HEIGHT)), with: .color(.yellow))
context.fill(Path(CGRect(x: width / 3 * 2, y: 0, width: width / 3, height: Self.ITEM_HEIGHT)), with: .color(.red))
}
.frame(width: width * value, height: Self.ITEM_HEIGHT)
.animation(.spring(duration: 0.1), value: width * value)
}
}
}
}
.frame(width: 150)
.background(.gray)
.padding(.vertical, 12)
}
}
}
@Observable final class PeakmeterState {
static public let MAX_ITEMS: Int = 128
@ObservationIgnored private var timer: Timer? = nil
@ObservationIgnored var peakValues: [CGFloat] = []
var frameNum: Int = 0
init() {
self.peakValues = Array(
repeating: 0.0,
count: Self.MAX_ITEMS
)
self.timer = Timer(
timeInterval: 1 / 5,
repeats: true,
block: { _ in
for index in 0 ..< self.peakValues.count {
self.peakValues[index] = CGFloat.random(
in: 0...1
)
}
self.frameNum += 1
}
)
self.timer!.tolerance = 0.0
RunLoop.current.add(
self.timer!,
forMode: .common
)
}
}
You can reduce the load by:
- Use final on classes to speed up method calls.
- Don't modify arrays in loops in @Observable classes because it causes unnecessary repaints (set them to @ObservationIgnored and change another variable in that class after array modification).
- Reducing the number of elements.
- Making the timer slower + removing animation.
- Rewriting the component to UIKit / AppKit.
- Maybe use SpriteKit (high-performance 2D content with smooth animations to your app, or create a game with a high-level set of 2D game-based tools).
In my project, even a simple cursor redraw eats up 15% of CPU power. Nothing helps - while the number of redraw frames was reduced to the minimum value.