For some reason I am genuinely incapable of understanding how ForEach works for some reason...
So first of all I think SwiftUI automatically redraws views on it's own when it sees that it's necessary right? But in this example say I have some array ["a","a","a"]
and I wanna loop over the elements with ForEach
VStack
{
ForEach(array, id:\.self) { item in
Text(item)
}
But I just don't understand where id's come in to be useful and I guess this stems in from me maybe not understanding how all this gets redrawn but let's say I decide to change this array into an array like ["b","b","b"]
what causes this to be redrawn? Does ForEach just recognize that the array changed and it decides to redraw it or idk what exactly triggers the redraw?
So I don't really know what makes it get redrawn whether its ForEach realizing the array changed or what but this gets redrawn right? So it goes over the loop again and creates a view for each element again so why is the id neccessary? If I change the array to ["a","b","c"]
SwiftUI also recognize this and redraws it and then I can swap a[0] and a[1] but SwiftUI also recognizes this and redraws it again so why are ID's necessary??
CodePudding user response:
it goes over the loop again and creates a view for each element again
Don't think of a view like a UIKit "UIView". A SwiftUI view is just a description. The foreach is creating a value struct that is compared against the previous value struct. The diffs, if any, are used by the SwiftUI runtime to work out how to mutate the on screen drawing objects, including, for example, animating the changes. SwiftUI automatically works out how it will make changes to what was drawn previously in order to get to what should be being drawn now.
The id is needed so that you can work out from the diff how exactly something changed, for example if you only know:
["A", "B", "C"]
->
["B", "C", "C"]
That could be:
(1) A changing to B, the B to C
or
(2) the A being removed, and then a C being added to the end.
By adding in an ID we can tell which thing is which:
[("A", 1), ("B", 2), ("C", 3)]
->
[("B", 1), ("C", 2), ("C", 3)]
= change A to B, change B to C
= animate cells 1 and 2
or
[("A", 1), ("B", 2), ("C", 3)]
->
[("B", 2), ("C", 3), ("C", 4)]
= delete 1, append 4 = "C"
= animate deletion of 1, shift cells left, animate new cell in from right
The magic of how SwiftUI view notices it needs to recompute a View body and work out if anything/what has changed, and potentially update its drawing state is based around the property wrappers of State
, ObservedObject
, StateObject
etc (not ForEach). These propertyWrappers wrap the variables like your "array" such that when they are set (when their setter function is called), additional work is performed to call your view's body function to give a new value for the view's description and then the diffing, possible updating etc all happens.
CodePudding user response:
In your case where the array is ["a", "a", "a"]
you're setting the id
to \.self
which is "a"
(i.e self). Similarly, if you change that array to ["b", "b", "b"]
the id
changes to "b"
causing a redraw.
Identifiable
We use the id
in order to make the array
act like the Identifiable
protocol. Try this:
ForEach(0..<someArray.count) { index in
...
}.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() 10) {
self.someArray.append(someElement)
}
}
When you run the above snippet, the ForEach
doesn't get redrawn. That behavior is the result of not using Identifiable
or assigning a uniques identifier, ForEach
can't identify each unique element, hence it will give you a runtime warning
(purple colored).
ForEach
Only use
ForEach(0..<array.count) { index in
when your data isstatic
.If it is
dynamic
, use theIdentifiable
protocol:ForEach(someIdentifiableArray) { element in
Or using id:
ForEach(someIdentifiableArray, id: \.uniqueProperty) { element in
Note: When an array
contains duplicates, make sure to have an id
property used for identification like UUID
.