admin管理员组

文章数量:1356884

I have a simple SwiftData app. Whenever ImageLibraryView appeared, there were microhangs because images inside each ThumbnailView were being loaded synchronously.

So I solved that by loading the images asyncronously. I animated their appearence so they fade in once loaded.

This worked great, however an undesired effect is every new ThumbnailView that appears when scrolling fades in. This quickly becomes annoying to see. Even when scrolling back up, the thumbnails that had previously appeared earlier animate back in.

How do I control the animation so that only the first several ThumbnailViews (or however many that fit the screen) animate in? Then once they're done, animation is disabled for all.

Thank you!

struct ImageLibraryView: View {
    
    @Query var images: [Item] = []
    
    var body: some View {
        ScrollView {
            ForEach(images) { item in
                ThumbnailView(item: item)
            }
        }
    }
}


struct ThumbnailView: View {
    
    var item: Item
    @State var uiImage: UIImage?
    
    var body: some View {
        Rectangle().fill(.gray)
            .aspectRatio(1.0, contentMode: .fit)
            .overlay {
                if let uiImage {
                    Image(uiImage: uiImage)
                        .resizable()
                        .scaledToFill()
                }
            }
            .animation(.easeInOut, value: uiImage)
            .task {
                uiImage = await loadThumbnailFromDisk(for: item.id)
            }
    }
}

I have a simple SwiftData app. Whenever ImageLibraryView appeared, there were microhangs because images inside each ThumbnailView were being loaded synchronously.

So I solved that by loading the images asyncronously. I animated their appearence so they fade in once loaded.

This worked great, however an undesired effect is every new ThumbnailView that appears when scrolling fades in. This quickly becomes annoying to see. Even when scrolling back up, the thumbnails that had previously appeared earlier animate back in.

How do I control the animation so that only the first several ThumbnailViews (or however many that fit the screen) animate in? Then once they're done, animation is disabled for all.

Thank you!

struct ImageLibraryView: View {
    
    @Query var images: [Item] = []
    
    var body: some View {
        ScrollView {
            ForEach(images) { item in
                ThumbnailView(item: item)
            }
        }
    }
}


struct ThumbnailView: View {
    
    var item: Item
    @State var uiImage: UIImage?
    
    var body: some View {
        Rectangle().fill(.gray)
            .aspectRatio(1.0, contentMode: .fit)
            .overlay {
                if let uiImage {
                    Image(uiImage: uiImage)
                        .resizable()
                        .scaledToFill()
                }
            }
            .animation(.easeInOut, value: uiImage)
            .task {
                uiImage = await loadThumbnailFromDisk(for: item.id)
            }
    }
}
Share Improve this question asked Mar 28 at 4:21 RRRRRR 3151 gold badge3 silver badges12 bronze badges 2
  • 1 Have you tried adding a condition in your task to only load the thumbnail if uiImage is nil? – Andrei G. Commented Mar 28 at 7:42
  • Does your Item conform to Identifiable? This would need to be true and it is crucial. Also, you probably want to use a transition(.opacity) modifier applied to your Image - not an animation. @AndreiG. When the task modifier executes, @State var uiImage will always be nil. – CouchDeveloper Commented Mar 28 at 12:07
Add a comment  | 

1 Answer 1

Reset to default 0

When dealing with a list of images, you probably want to use a lazy container list view.

In your code

ScrollView {
    ForEach(images) { item in
        ThumbnailView(item: item)
    }
}

you create a static list embedded in a ScrollView, which creates all the item views a priory, and in your case, this would load and keep all images. I would guess, that a transition(.opacity) modifier would work (as loading all images at once) - but I also assume, this is not at all what you want.

You likely want to use a lazy container, for example a List:

List(items) { item in
    ItemView(item: item)
}

A List only evaluates the body of the cell if it is visible. If a cell was visible and now disappears, its @State variable also get deallocated. This is unfortunate in your case, since it then also deallocates the stored image. A quick solution is to provide a suitable "model" for the cells.

The following code shows a quick solution, which can be run in preview or the simulator:


struct Item: Identifiable {
    var id: String
    var text: String?
}


struct ItemsView: View {
    
    @State private var items: [Item] = Self.makeItems()
    
    var body: some View {
        NavigationStack {
            List($items) { item in
                ItemView(item: item)
            }
        }
        .navigationTitle(Text("Items"))
    }
    
    static func makeItems() -> [Item] {
        let items: [Item] = (0..<100).map { i in
            Item(id: "\(i)")
        }
        return items
    }
}

struct ItemView: View {
    @Binding var item: Item
    
    var body: some View {
        HStack {
            if let text = item.text {
                Text(text)
                    .transition(.opacity)
            } else {
                Text("...")
            }
        }
        .padding(20)
        .task {
            try? await Task.sleep(for: .seconds(1))
            item.text = "Item \(item.id)"
        }
    }
}

You should notice the new structure Item and how it is used as a binding and how the cell is modifying the bound value by "loading" the text value and assigning it the bound value. Notice also, that the array will keep the image (well, "text" here) - and that this might be problematic, when you have a large number of images! In this case, you should consider to use a library which can efficiently load and cache images.

So, this is not a great design, but it should demonstrate the issue.

Note also that it's using a transition modifier, not an animation modifier.

本文标签: Animating items in a SwiftUI list only at first appearanceStack Overflow