I’m experiencing a confusing issue in SwiftUI where Images on a simple VStack card flash during interaction. See the video below:
Short video demonstrating the problem
I’ve split this out from the app I’m working on into a simpler sample project. Essentially, there’s a simple button that loads the PhotoPicker and adds photos to state. Each photo card uses an ObservableObject
to track whether it is selected–this gets toggled on tap to ensure only a single card is selected at a time.
class EntriesState : ObservableObject {
@Published var selectedIndex : Int? = nil
}
struct PhotoView : View {
var image : Data;
var index : Int;
@ObservedObject var entriesState: EntriesState
var body: some View {
VStack {
if let uiImage = UIImage(data: image) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 24.0))
.onAppear() {
print("Image (index) appeared")
}
}
if entriesState.selectedIndex == index {
Text("This is the description for the image")
}
}
.padding(20)
.background(Color.blue)
.clipShape(RoundedRectangle(cornerRadius: 24.0))
.onTapGesture {
// Removing `withAnimation` fixes the flashing,
// but removes the animation.
// Is there a way to have the nice animation
// as the card expands, without the image flashing?
withAnimation {
if entriesState.selectedIndex == index {
entriesState.selectedIndex = nil
} else {
entriesState.selectedIndex = index
}
}
}
}
}
struct ContentView: View {
@State private var selectedItems = [PhotosPickerItem]()
@State private var selectedImages = [Data]()
@StateObject var entriesState = EntriesState()
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200))]) {
ForEach(0..<selectedImages.count, id: .self) { i in
PhotoView(image: selectedImages[i], index: i, entriesState: entriesState)
.padding(20)
}
}
}
.toolbar {
PhotosPicker("Select images", selection: $selectedItems, matching: .images)
}
.onChange(of: selectedItems) {
Task {
selectedImages.removeAll()
for item in selectedItems {
if let data = try? await item.loadTransferable(type: Data.self) {
selectedImages.append(data)
}
}
}
}
}
}
}
I’ve tried:
- Using @Binding instead of @ObservableObject to track the selected index.
- Adding
.id(index)
to the image - Tracking
.onAppear
calls for the image–I’m only seeing that called once, not multiple times like I would expect if the image were being rerendered. - Removing the animation. This fixes the problem, but I’d like to be able to animate the growing/shrinking of the card
- Using
@State var selected
in thePhotoView
. This prevents the flashing on ALL images, but the image still flashes on the selected card. See here - Searching StackOverflow and Google for solutions. Notably, I’ve found people with similar problems here and here but nothing suggested has worked 🙁
Please let me know if you have any suggestions or questions! Thanks!
2
Answers
I found a solution! Here is a video of what it looks like now.
The solution is pretty simple--instead of storing the image data in the view, store the UIImage directly. My guess is the flash comes from converting the Data -> UIImage during the animation as for a frame or two there might be nothing to show.
I see that you already tried
.onAppear()
method but, my best guess is that the flashing happens because of the.resizable()
modifier being applied to theImage
view. As we know, this modifier recalculates the size of the image based on the new layout constraints during the animation. That may be the reason you’re facing this issue. In order to handle this,.onAppear()
can be used. Among other things.I’ve looked through the links you provided and came up with solution, hope that works out for you.
We can introduce a separate state variable to track the selected state of each
PhotoView
and we can avoid modifying theentriesState.selectedIndex
directly during the animation. This separation should prevent the recalculation of theImage
view’s size and as a result, prevents the flash effect.Now let’s modify the
.onAppear()
in theImage
view to set the initial value ofselected
based on theselectedIndex
.After that, we can update the
onTapGesture
to toggle theselected
state instead of directly modifyingentriesState.selectedIndex
.Finally, update the if condition in the
VStack
to use theselected
state.With these changes, the image should animate smoothly without any flashes when tapped. Hope that solutions works for you.