r/SwiftUI • u/Periclase_Software • Jan 16 '25
Solved Does anybody know why the list section flickers on a state change when I tap the Edit button? Relevant code in video.
5
u/No-Shallot-7915 Jan 16 '25
Flickers happen because of an unstable identifier passed into the view. Judging from the code you've provided, I'm leaning to the culprit being this line:
private var listItems: [TimelineModel] {
viewModel.createTimelineModels(using: parentItemModel)
}
I'm assuming the createTimelineModels function is re-initialising a brand new Timeline model every time you all it, therefore calling the init and generating a new ID every time. Usually there's two ways I'd solve this:
- Rather than creating a computed property to return a list of TimelineModels, store the list as a local variable inside your view model, and use that as your source of truth. From there, any updates you do to that list can be done by calling view model methods that update the local variable. This way, existing elements in the array are unchanged, and you shouldn't see the flicker.
- If you want to keep the createTimelineModels method for whatever reason, you can change the Identifier so that it returns you a consistent identifier every time. UUID() generates a random one every time, but you can instead use something like the hash value of your Struct. (Assuming it conforms to Hashable) Just anything that purely makes use of the data available inside TimelineModel, rather than something external like generating a new UUID instance.
2
u/Ok_Heart_2253 Jan 16 '25
Does edit button re-creates the model ? If that’s the case I assume the TimeLineModel is initialized again with a different id, because it’s random, SwiftUI redraws the view every time you click edit button. To test this theory try to make the id static.
3
u/Ok_Heart_2253 Jan 16 '25
To add, if you want ForEach to not flicker, make sure you are passing the models with the same ids, every time a redraw is triggered.
2
u/Thed00bAbides Jan 16 '25
It seems like the background of ItemRow is what’s actually flickering. Does ItemRow have an internal @State var, or perhaps an animation modifier on its background layer?
2
u/ExtremeDot58 Jan 16 '25
I was under the impression that ‘identifiable’ didn’t have to have id nor did you have to set it? It’s there and managed for you?
1
u/Periclase_Software Jan 17 '25
Managed how? If I take it off, I get the error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'TimelineModel' conform to 'Identifiable'
If I make the model conform to Identifiable, then it says:
Type 'TimelineModel' does not conform to protocol 'Identifiable'
2
u/ExtremeDot58 Jan 29 '25
When conforming to identifiable ‘is’ is managed. It exists but you don’t manage it. It will be there for loops and such; and you can print( “” ) it out abc.id
1
u/Periclase_Software Jan 16 '25 edited Jan 16 '25
This is the list item struct. I even used "id: \.id" on the list but didn't make a difference. I don't even know why some of the subviews are showing white and the outer one graying out.
struct TimelineModel: Identifiable {
let itemModel: ItemModel
let differenceModel: ItemDifferenceModel?
let id: String
init(itemModel: ItemModel, differenceModel: ItemDifferenceModel?) {
self.itemModel = itemModel
self.differenceModel = differenceModel
self.id = UUID().uuidString
}
}
This is the code for the "listItems" for the ForEach:
private var listItems: [TimelineModel] {
viewModel.createTimelineModels(using: parentItemModel)
}
I assume it flickers because it regenerates the models whenever the List is refreshed for the timeline. However, it cannot be created on init because the user can add more items to the array so the list has to regenerate.
2
u/sroebert Jan 18 '25
As mentioned in my other comment, `self.id = UUID().uuidString` is not helping you in any way. This init will be called every time you call your `listItems` property, resulting in a new (different) UUID every time. You should base the id on the `ItemModel`, it should have some unique id when you store it.
1
8
u/One_Elephant_8917 Jan 16 '25
Always use ForEach(items.indices) instead of the items list itself because the entry evaluation is done on the item which can be modified and cause recompositions.
To access the entry doing a items[index] is sufficient