skip to Main Content

I am showing the conversation in the view, initially only the end of the conversation is loaded. To simplify it’s something like this:

ScrollViewReader { proxy in
  ScrollView {
    LazyVStack {
      ForEach(items) { item in
        itemView(item)
        .onAppear { prependItems(item) }
      }
      .onAppear {
        if let id = items.last?.id {
          proxy.scrollTo(id, anchor: .bottom)
        }
      }
    }
  }
}

func prependItems(item: Item) {
  // return if already loading
  // or if the item that fired onAppear
  // is not close to the beginning of the list
  // ...
  let moreItems = loadPreviousItems(items)
  items.insert(contentsOf: moreItems, at: 0)
}

The problem is that when the items are prepended to the list, the list view position remains the same relative to the new start of the list, and trying to programmatically scroll back to the item that fired loading previous items does not work if scrollbar is moving at the time…

A possible solution I can think of would be to flip the whole list view upside down, reverse the list (so that the new items are appended rather than prepended), then flip each item upside down, but firstly it is some terrible hack, and, more importantly, the scrollbar would be on the left…

Is there a better solution for backwards infinite scroll in SwiftUI?

EDIT: it is possible to avoid left scrollbar by using scaleEffect(CGSize(width: 1, height: -1)) instead of rotationEffect(.degrees(180)), but in either case item contextMenu is broken one way or another, so it is not a viable option, unfortunately, as otherwise scaleEffect works reasonably well…

EDIT2: The answer that helps fixing broken context menu, e.g. with a custom context menu in UIKit or in some other way, can also be acceptable, and I posted it to freelancer in case somebody is interested to help with that: https://www.freelancer.com/projects/swift/Custom-UIKit-context-menu-SwiftUI/details

3

Answers


  1. Chosen as BEST ANSWER

    So far the only solution I could find was to flip the view and each item:

    ScrollViewReader { proxy in
      ScrollView {
        LazyVStack {
          ForEach(items.reversed()) { item in
            itemView(item)
            .onAppear { prependItems(item) }
            .scaleEffect(x: 1, y -1)
          }
          .onAppear {
            if let id = items.last?.id {
              proxy.scrollTo(id, anchor: .bottom)
            }
          }
        }
      }
      .scaleEffect(x: 1, y -1)
    }
    
    func prependItems(item: Item) {
      // return if already loading
      // or if the item that fired onAppear
      // is not close to the beginning of the list
      // ...
      let moreItems = loadPreviousItems(items)
      items.insert(contentsOf: moreItems, at: 0)
    }
    

    I ended up maintaining reversed model, instead of reversing on the fly as in the above sample, as on long lists the updates become very slow.

    This approach breaks swiftUI context menu, as I wrote in the question, so UIKit context menu should be used. The only solution that worked for me, with dynamically sized items and allowing interactions with the item of the list was this one.

    What it is doing, effectively, is putting a transparent overlay view with an attached context menu on top of SwiftUI list item, and then putting a copy of the original SwiftUI item on top - not doing this last step makes an item that does not allow tap interactions, so if it were acceptable - it would be better. The linked answer allows to briefly see the edges of the original SwiftUI view during the interaction; it can be avoided by making the original view hidden.

    The full code we have is here.

    The downside of this approach is that copying the original item prevents its partial updates in the copy, so for every update its view ID must change, and it is more visible to the user when the full update happens... So I believe making a fully custom reverse lazy scroll would be a better (but more complex) solution.

    We would like to sponsor the development of reverse lazy scroll as an open-source component/library - we would use it in SimpleX Chat and it would be available for any other messaging applications. Please approach me if you are able and interested to do it.


  2. Have you tried this?

    self.data = []
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01)
    {
      self.data = self.idx == 0 ? ContentView.data1 : ContentView.data2
    }
    

    Basically what this does is first empty the array and then set it again to something, but with a little delay. That way the ScrollView empties out, resets the scroll position (as it’s empty) and repopulates with the new data and scrolled to the top.

    Login or Signup to reply.
  3. I found a link related to your question: https://dev.to/gualtierofr/infinite-scrolling-in-swiftui-1p3l

    it worked for me so you can implement parts of that in ur code

    struct StargazersViewInfiniteScroll: View {
    @ObservedObject var viewModel:StargazersViewModel

    var body: some View {
        List(viewModel.stargazers) { stargazer in
            StargazerView(stargazer: stargazer)
                .onAppear {
                    self.elementOnAppear(stargazer)
            }
        }
    }
    
    private func elementOnAppear(_ stargazer:User) {
        if self.viewModel.isLastStargazer(stargazer) {
            self.viewModel.getNextStargazers { success in
                print("next data received")
            }
        }
    }
    

    you can take what you need from here

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