2

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

esp
  • 7,314
  • 6
  • 49
  • 79
  • You do not want to scroll to first element of array ? – Ptit Xav Aug 10 '22 at 18:53
  • no, because it doesn't solve the problem of infinite scroll to the top - without flipping the view the position in the scroll is changing when adding items to the beginning of the list. – esp Aug 14 '22 at 10:40
  • 2
    I would consider custom solution, like this one https://stackoverflow.com/a/58708206/12299030. – Asperi Aug 18 '22 at 11:54
  • yes, custom solution seems the way to go... I've managed to solve custom context menu problem, but it prevents partial item updates, and makes the whole thing more brittle... Might you be interested to develop a functional open-source component for reverse lazy scroll if we sponsored it? – esp Aug 20 '22 at 15:58
  • @Asperi - maybe you would like to post everything you know about making a reverse lazy scroll view as an answer and I would award the bounty before it expires in 18 hours? :) – esp Aug 21 '22 at 17:21

3 Answers3

0

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.

DialFrost
  • 1,610
  • 1
  • 8
  • 28
  • This is not what is needed, sorry, it does not solve the problem. I want the view to open already scrolled to the bottom, not to the top, without scrolling it in front of the user. I also want its position to remain when the items are added to the top - clearing and repopulating does not achieve it. – esp Aug 20 '22 at 15:56
  • Hmm ok @esp. So you want the view to auto go to the bottom? – DialFrost Aug 21 '22 at 09:37
  • yes, and also stay in the same location when the items are added to the top. Both are achieved when the view is flipped... but it breaks context menus. – esp Aug 21 '22 at 17:02
0

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.

esp
  • 7,314
  • 6
  • 49
  • 79
-2

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

AJX Tech
  • 1
  • 4
  • Link only questions aren’t allowed in SO, links breaks over time. If there is relevant code in the link please share that code. – lorem ipsum Aug 21 '22 at 11:58
  • struct StargazersViewInfiniteScroll: View { @ObservedObject var viewModel:StargazersViewModel is also part of the code but it didnt come in a proper manner – AJX Tech Aug 21 '22 at 12:38
  • the main problem is that the solution is for the infinite scroll downwards, when the items are added to the bottom of the view - this is a simple problem. I need upwards scroll, when the items are added to the top. – esp Aug 21 '22 at 17:04