简体   繁体   中英

How to set a custom prefetching time for LazyVStack in SwiftUI?

TL;DR: Is there some parameter or way to set the offset at which LazyVStack initialises views?

LazyVStack initialises the views lazily, so when I scroll, the next (few?) views are initialised. I am loading an image once a view is drawn, using SDWebImage Package in swift. This takes a view milliseconds, and since I am using a LazyVStack, if one scrolls fast (even within reasonable limits), the placeholder is visible for a short moment, because the view has just been created a (too) short moment ago. If I scroll very slowly, the image loads just before the view appears, so no placeholder is visible.

If I could make the LazyVStack initialise the views just a few milliseconds earlier my problem would be gone...

Once would think this is a pretty common problem, timing this initialisation just right so as not to load too early or too late.. but nothing at all in the docs about this

this process is called as prefetching -because you're prefetching them so it will look smooth-

And sorry, but there's no way to access prefetching of LazyVStack in SwiftUI right now. Also, keep in mind that both SwiftUI's Grid And LazyH/VStack is not performant as UIKit 's UICollectionView . So what you could do here is you can use UICollectionView 's UICollectionViewDataSourcePrefetching protocol in your collection view's data.

I used SDWebImage Library to Fetch Images from inte.net (one of the most popular libraries for UIKit )

I tried to explain everything so give your attention to them, here's what it looks like: 动图

here's the code:

import SwiftUI
import SDWebImage

struct CollectionView: UIViewRepresentable {
    let items: [String]
    
    func makeUIView(context: Context) -> UICollectionView {
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
        collectionView.delegate = context.coordinator
        collectionView.dataSource = context.coordinator
        return collectionView
    }
    
    func updateUIView(_ uiView: UICollectionView, context: Context) {
        // Reload the collection view data if the items array changes
        uiView.reloadData()
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDataSourcePrefetching {
        let parent: CollectionView
        
        init(_ collectionView: CollectionView) {
            self.parent = collectionView
        }
        
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return parent.items.count
        }
        
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as! ImageCell
            let item = parent.items[indexPath.item]
            
            // Set the progress of the progress view as the image is being downloaded
            cell.progressView.progress = 0.0
            SDWebImageDownloader.shared.downloadImage(with: URL(string: item), options: .highPriority, progress: { (receivedSize, expectedSize, url) in
                DispatchQueue.main.async {
                    cell.progressView.progress = Float(receivedSize) / Float(expectedSize)
                }
            }) { (image, data, error, finished) in
                DispatchQueue.main.async {
                    cell.imageView.sd_setImage(with: URL(string: item))
                    cell.progressView.isHidden = true
                }
            }
            return cell
        }
        
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: 100, height: 100)
        }
        
        // MARK: - UICollectionViewDataSourcePrefetching
        
        func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
            // Filter the index paths to only include the ones that are within the desired range, trick relies on here
            // In our example, i'm fetching 6 items beforehand which equals 2 rows, so i'm prefetching 2 rows beforehand. you can increase that amount if you w ant to
            let prefetchIndexPaths = indexPaths.filter { $0.item < collectionView.numberOfItems(inSection: $0.section) - 6 }
            let urls = prefetchIndexPaths.compactMap { URL(string: self.parent.items[$0.item])! }
            SDWebImagePrefetcher.shared.prefetchURLs(urls)
        }
        
        func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
            // Cancel the prefetching for the given index paths, this is not required but i wanted to add it
            let urls = indexPaths.map { URL(string: self.parent.items[$0.item]) }
        }
    }
}

class ImageCell: UICollectionViewCell {
    let imageView = UIImageView()
    let progressView = UIProgressView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(imageView)
        addSubview(progressView)
        // if you're not familiar with uikit this is just a disgusting uikit code to make proper layouts :(
        progressView.translatesAutoresizingMaskIntoConstraints = false
        progressView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        progressView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        progressView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.topAnchor.constraint(equalTo: progressView.bottomAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        imageView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

and Here's how you can implement it to swiftui:


struct ContentView: View {
    var items : [String] {
        var i = 0
        var _items = [String]()
        while (i < 900) {
            _items.append("https://picsum.photos/\(Int.random(in: 300..<600))/\(Int.random(in: 300..<600))")
            i = i + 1
        }
        return _items
    }
    
    var body: some View {
        CollectionView(items: items)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I used lorem picsum which is a website for generating random images and that's why you see images reloading randomly in my sample(that white ones), in your case, this shouldn't be a problem

Since I was using SDWebImageSwiftUI before, simply calling the following already before the view starts to initialise solved my problem:

SDWebImagePrefetcher.shared.prefetchURLs(urls) { finishedCount, skippedCount in
     print("preloading complete")
}

then in my LazyVStack I use:

LazyVStack {
     ForEach(items, id: \.self) { item in
             ItemView(item: item)
                   .onAppear {
                        // calling function to prefetch next x-items by their url
                  }
            }
     }
}

In SwiftUI, we can achieve prefetching using the "LazyVStack" view, which allows us to efficiently render a large number of items in a vertical stack.

LazyVStack {
    ForEach(data, id: \.self) { item in
        Text(item.attribute)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM