skiptools / skip-ui

SwiftUI for Android
https://skip.tools
GNU Lesser General Public License v3.0
92 stars 9 forks source link

Possible issue with modifying @State variables within .task? #30

Open jeffc-dev opened 2 months ago

jeffc-dev commented 2 months ago

I ran across a weird issue wherein setting a @State variable from within .task based on an array of values (I tested with UUIDs and random Ints) works properly in SwiftUI but not Compose.

In a nutshell, assume that a view struct has the following variables:

let uuids = [UUID(), UUID(), UUID(), UUID(), UUID()]
@State private var firstUUID:UUID?

This initializes the view with 5 random UUIDs and a firstUUID @State variable set to nil.

Within the view's .task, I attempt to set firstUUID to the first UUID in the array.

.task {
    self.firstUUID  = self.uuids.first
}

This works properly within SwiftUI, but not Compose.

(Real-world context: I want to use UUIDs to model different pieces of model that a user can toggle between in the view...)

I do note, however, that (assuming these views are in tabs) if I click on a different tab and then return the value is set properly in Compose as well. It also seems to work consistently within the second tab. Some sort of timing issue, perhaps?

I tried this using random Ints and saw the same behavior with them, so I don't think it's specific to UUID, per se.

This has been a bit hard to explain, so I've created a test project that I believe demonstrates what I'm seeing. I will attach a screenshot of the output from Compose (left) and SwiftUI (right), respectively.

You'll see that Compose consistently identifies the first UUID in the array within view reconstruction, but comes up with a completely different UUID when it's based on the @State variable.

Screenshot 2024-05-02 at 3 06 20 AM

This might seem a bit esoteric, but I found it while having trouble getting a struct to conform to Identifiable (hence the UUID - I was using one for my struct's id). I imagine others will run across this as well.

aabewhite commented 2 months ago

Thanks for the detailed report!I believe I know why this might happen. I’ll investigate later today and give you a fix or status report.

aabewhite commented 2 months ago

Status: I don't have a fix for this, but just by looking at it I can tell why it's likely happening, and that will let you understand how to avoid it.

Both SwiftUI and Compose may re-evaluate view bodies due to state, environment, etc changes. And view bodies typically create child views. So in sum you have a lot of cases where views get recreated.

@State values are preserved across recreation. But non-@State values are not. So what I believe is happening is:

Why does it work in SwiftUI? I'm actually not sure if it would work correctly across all cases, but SwiftUI seems to be better at avoiding re-renders and also better at copying views without re-running their initializations (i.e. taking advantage of struct value semantics).

I'm not sure what your real use case is, so I hesitate to prescribe a solution. But for example a static UUID set or a @State or a value from a shared ViewModel would work. The problem is in using a value that is reset along with view construction.

Please let me know if this makes sense.