How can I optimize SwiftUI CPU load on frequent updates

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

Replies

Although I know you'll again blame my naming conventions I decided to add a small example here: it does nothing except the peakmeter stuff and still causes a CPU load of 25% to 30% for 128 channels. This CPU load merely changes if you decrease the size of the window to only show two channels, while starting the example with only two channels (set s_NbPeakmeter = 2) shows a much lower cpu load. So invisible views also seem to cause CPU load?

import SwiftUI

class CPeakmeterManager: NSObject, ObservableObject
{
    static public let s_NbPeakmeter: Int = 128

    @Published var m_VecPeakmeterValues: [CGFloat] = []

    var m_Timer: Timer? = nil

    override init() 
    {
        super.init()
        
        m_VecPeakmeterValues = [CGFloat](repeating: 0.0, count: CPeakmeterManager.s_NbPeakmeter )
        
        m_Timer = Timer.scheduledTimer(timeInterval: 0.05, target: self, selector: #selector( OnTimer ), userInfo: nil, repeats: true)
    }
    
    @objc func OnTimer() 
    {
        for ChannelIndex in 0..<m_VecPeakmeterValues.count
        {
            m_VecPeakmeterValues[ ChannelIndex ] = CGFloat.random(in: 0...1)
        }
    }
}

struct PeakmeterView: View
{
    @Binding var b_PeakmeterValue: CGFloat

    var body: some View 
    {
        GeometryReader { geometry in
            ZStack(alignment: .trailing) {
                
                HStack(spacing: 0) {
                    Rectangle()
                        .frame( width: 0.4 * geometry.size.width, height: 10 )
                        .foregroundColor(.green)
                        
                    Rectangle()
                        .frame( width: 0.3 * geometry.size.width, height: 10 )
                        .foregroundColor(.yellow)
                            
                    Rectangle()
                        .frame( width: 0.3 * geometry.size.width, height: 10 )
                        .foregroundColor(.red)
                }

                Rectangle()
                    .frame(width: min( ( 1.0 - b_PeakmeterValue ) * geometry.size.width, geometry.size.width), height: 10)
                    .opacity(0.9)
                    .foregroundColor(.gray)
                }
        }
    }
}

@main
struct PeakmeterTestApp: App {

    @StateObject var m_PeakmeterManager = CPeakmeterManager()

    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(self.m_PeakmeterManager)
        }
    }
}

struct ContentView: View {

    @EnvironmentObject var m_PeakmeterManager: CPeakmeterManager

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                ForEach(0 ..< CPeakmeterManager.s_NbPeakmeter, id: \.self) { ChannelIndex in
                    PeakmeterView( b_PeakmeterValue: $m_PeakmeterManager.m_VecPeakmeterValues[ ChannelIndex ])
                        .frame(width: 150)
                }
            }
        }
        .padding([.top,.bottom],12)
    }
}