KeithB
KeithB

Reputation: 541

How to Calculate and Publish Arrays in SwiftUI

In SwiftUI, if I declare an array variable with the property wrapper @Published, and then calculate each element of that array within a for() loop, will the variable be published each time I compute a new element? If so, is there a way to tell the compiler to not publish the variable until every element is computed?

I have an app that computes a spectrum for each successive frame of audio data. The app then publishes that spectrum[] to several Views that render fancy graphics. I want each View to re-draw only once for each successive frame of audio data - not once for each computed element of the spectrum[] array.

Here's some simplified code to illustrate the problem:

class ArrayGenerator: ObservableObject {
    @Published var spectrum = [Float](repeating: 0.0, count: 1000) 

    DispatchQueue.main.async { [self] in
        for bin in 0 ..< 1000 {
            spectrum[bin] = (userGain + userSlope * Float(bin)) * amplitudes[bin]
        }
    }
}

Since this code changes the variable 1,000 times, I believe it publishes it 1,000 times. But I want it to be published only once - when the for() loop is completed. How can I accomplish this?

Upvotes: 0

Views: 321

Answers (2)

malhal
malhal

Reputation: 30719

Your assumption that it will publish 1000 changes is correct. objectWillChange is sent in the willSet and although SwiftUI coalesces these notifications into one render it is still something you would want to avoid. The way to fix that is to simply do your work on a copy of the array and then set the published property with the new version of the array once.

Upvotes: 0

jnpdx
jnpdx

Reputation: 52555

Your assumption that it will publish 1000 changes/renders is incorrect. All of those iterations of the for loop will be done in one iteration of the main run loop. Take this simple example:

class ArrayGenerator: ObservableObject {
    @Published var spectrum = [Float](repeating: 0.0, count: 1000)

    func run() {
        DispatchQueue.main.async { [self] in
            for bin in 0 ..< 1000 {
                spectrum[bin] = Float(bin)
            }
        }
    }
}

struct ContentView: View {
    
    @StateObject private var generator = ArrayGenerator()
    
    var body: some View {
        let _ = print("Rendering...")
        Text(generator.spectrum.map { String($0) }.joined())
            .onAppear {
                generator.run()
            }
    }
}

Which prints:

Rendering...
Rendering...

It renders once for the first appearance, then the onAppear runs, and then an additional render after the loop is done.

So, your code already does what you expect.

Upvotes: 1

Related Questions