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.