Why is a Class faster in SwiftUI than a Struct? (with example)

Hi all,

can someone more experienced or smarter than me please take a look at this example code?
I was struggling with performance issues using SwiftUI with an MTKView and found out that the Struct I was using to keep the bindings was the guilty one. After hours of searching and trying stuff, I found out that using a Class solved the performance issues. It introduces other issues though, like the UI not always updating everywhere, and all my sliders returning to zero when my macOS app window is minimised or has no focus.
Even though I understand the difference between value and reference types, and that copying a Struct each time takes CPU cycles, I don't understand why my Class values seem to be unreachable at times by the UI...

Here's the macOS example code:

Code Block
//
// ContentView.swift
// SlowStructFastClass
//
// Created by Michel Storms on 12/01/2021.
//
import SwiftUI
class VC {
var s1: Double = 0.5
}
struct ContentView: View {
@State var valueStruct: Double = 0.5
@State var valueClass = VC()
var body: some View {
VStack {
Text("STRUCT")
Slider(value: $valueStruct)
Text("\(self.valueStruct)")
Text("")
Text("CLASS")
Slider(value: $valueClass.s1)
Text("\(self.valueClass.s1)")
}
}
}


I think this is the simplest I could make the example, yet there is still a big speed difference between struct and class binding-based sliders...
Any clues? What am I doing wrong?
Thanks for your time!
(tested on a 2020 i5 MBP running Big Sur)

Answered by stormychel in 661814022
UPDATE:

This never really got solved, not by me and not by Apple, so I decided to go with a Class and update the view by adding a bool named "triggerViewUpdate" to my AppState class, and doing a "triggerViewUpdate.toggle()" whenever needed.

This might not be the most elegant solution, but it works well enough for my use case.

I also added an example to my test code that uses ObservedObject / ObservableObject.

This solves the responsiveness of the UI, but again makes the slide more choppy. Not as bad as the Struct version, but somewhere in between.

Code:

Code Block
//
// ContentView.swift
// SlowStructFastClass
//
// Created by Michel Storms on 12/01/2021.
//
import SwiftUI
class VC {
var s1: Double = 0.5
}
class VD: ObservableObject {
@Published var s1: Double = 0.5
}
struct ContentView: View {
@State var valueStruct: Double = 0.5
@State var valueClass = VC()
@ObservedObject var valueClassObserved = VD()
var body: some View {
VStack {
Text("STRUCT")
Slider(value: $valueStruct)
Text("\(self.valueStruct)")
Text("CLASS")
Slider(value: $valueClass.s1)
Text("\(self.valueClass.s1)")
Text("CLASS - OBSERVABLEOBJECT")
Slider(value: $valueClassObserved.s1)
Text("\(self.valueClassObserved.s1)")
}
}
}

I tested (with your OP code) in Simulator with Xcode 12.2 and did not notice any performance issue you are mentioning.
Can you explain and give figures ?

Anyway, class should be faster because it does not call all the mechanisms to update the UI when its state changes. But that is the core value of SwiftUI !
Question is : is this performance difference, if any, significant enough from a user perspective ?

Main difference is that slider with class is not updated until I move the Struct slider. And that's normal.

I tested (with your OP code) in Simulator with Xcode 12.2 and did not notice any performance issue you are mentioning.
Can you explain and give figures ?
Anyway, class should be faster because it does not call all the mechanisms to update the UI when its state changes. But that is the core value of SwiftUI !
Question is : is this performance difference, if any, significant enough from a user perspective ?
Main difference is that slider with class is not updated until I move the Struct slider. And that's normal. - Claude31

Thanks for your reply, Claude31.

I assume you tried this on an iOS simulator, right? This works much better, yes.

But my question is specific to macOS, would you mind trying that?

The performance difference is extremely significant, in my simple photo filtering app which renders to a Metal View, my frame rate drops from the requested 60 fps to a mere 6fps... it's a horrible experience.

Do you think there's a workaround for the UI update stuff so I can keep using a Class?

But my question is specific to macOS, would you mind trying that?

I tried your code on macOS, and no significant performance issue could be observed.

The performance difference is extremely significant, in my simple photo filtering app which renders to a Metal View, 

Isn't it the problem of your Metal View?


Hi OOPer, thanks for trying!

The Metal View works perfectly, when the bindings are class-based, and isn't even in this example so it's purely the difference between struct / observed / class based bindings that matters here. Sometimes it's less noticeable though, but always too slow for my use case.

I spent one of my 2 yearly code-level helps to get help from an Apple Engineer, something's definitely off or I am doing something very wrong here.

Will update here when I got news from them.

The Metal View works perfectly, when the bindings are class-based, and isn't even in this example so it's purely the difference between struct / observed / class based bindings that matters here. Sometimes it's less noticeable though, but always too slow for my use case.

It was better if the shown code could reproduce the issue.

Will update here when I got news from them.

Thanks. I'm very curious and will watch on this thread.
It does reproduce the issue though.

If you move the Struct and Observed sliders around for a bit, the slider lags behind the pointer.

The Class one sticks to the pointer.

That's with one slider... an array of multiple sliders makes it even worse.



If you move the Struct and Observed sliders around for a bit, the slider lags behind the pointer.

Not reproducible in my environment, just moving the sliders around for a bit.
Or your for a bit means hours?

an array of multiple sliders makes it even worse.

Then please show an example of multiple sliders, or it may happen only in your environment.
Ok, I made 3 videos, one with the binding from a Class, the other with the same Class as an ObservableObject, and one with a Struct

https://www.icloud.com/iclouddrive/0m2o4VTzRyD12IkKj0BJptIqg#Class_vs_Struct

I think it's pretty obvious there's something off here... it's literally the same code, only the way the binding was made has changed...

I made 3 videos

Sorry, but it is useless that you reproduce the issue in your environment.

I'm not saying the issue never happens, but saying that it happens only in your environment.

If you could clarify your testing environment, someone who can prepare the similar environment would test your code.

Anyway, I cannot reproduce the issue you have described with the code currently shown.
Test project + 8K test_image for assets AND a complete compiled test are available at:

https://www.icloud.com/iclouddrive/0m2o4VTzRyD12IkKj0BJptIqg#Class_vs_Struct/Archive.zip

(Archive.zip)

Please run this fullscreen and let me know your thoughts after running all sliders for a while.

I do have to admit that the sliders are a bit smoother outside of my project. This is with only 1 filter though, while the project might run a combination of about 30 CoreImage filters (pipelined).

Please run this fullscreen and let me know your thoughts after running all sliders for a while.

I needed to modify some parts of your project and tested it on Catalina.
But the difference was very small.
Maybe Big Sur is required to reproduce your issue. Maybe some more other things.

Expecting you resolve this issue soon and clarify what is causing the lag you experienced.
I confirm OOPer finding.
Did not see significant difference on Catalina. But I had not the image library to test the complete use case.
Thanks for the efforts, OOPer and Claude31!

I tested on Big Sur, so our results may vary. Someone tested on Catalina and noticed a difference, but he is on an older iMac and the Class slider was also less smooth than on my machine.

Apple requested a test project, so I sent them the test I posted here. I hope they can shed some light on this, and show me a workaround. I will post the results here!

Accepted Answer
UPDATE:

This never really got solved, not by me and not by Apple, so I decided to go with a Class and update the view by adding a bool named "triggerViewUpdate" to my AppState class, and doing a "triggerViewUpdate.toggle()" whenever needed.

This might not be the most elegant solution, but it works well enough for my use case.

Why is a Class faster in SwiftUI than a Struct? (with example)
 
 
Q