skip to Main Content

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

3

Answers


  1. Chosen as BEST ANSWER

    Quick answer to the question: no


    That being said, in this case there is still a solution: 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
                      }
                }
         }
    }
    

  2. 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 internet (one of the most popular libraries for UIKit)

    I tried to explain everything as comments in the code so give your attention to them, here’s what it looks like:
    gif

    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

    Login or Signup to reply.
  3. You can try adding extra space to the ScrollView and removing it using .padding:

    private enum Constant {
        static let topInset: CGFloat = UIScreen.main.bounds.height * 0.4
        static let bottomOffset: CGFloat = UIScreen.main.bounds.height * 0.4
    }
    
    struct PrefetchedScrollView<Content: View>: View {
        let axes: Axis.Set
        let showsIndicators: Bool
        let content: () -> Content
    
       var body: some View {
            ScrollView(axes, showsIndicators: showsIndicators) {
                Spacer(minLength: Constant.topInset)
                content()
                Spacer(minLength: Constant.bottomOffset)
            }
            .padding(top: -Constant.topInset, bottom: -Constant.bottomOffset)
        }
    }
    

    PS: This method adds a bug with pull to refresh, but this bug is easy to fix, using a custom pull to refresh implementation 😊

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search