scrollTo() leads to invalid UI in VStack in iOS 16

(This is a show stopper issue for me to use VStack or LazyVStack in my app. I posted the issue here on SO but got no reply. So I'll ask in this forum.)

What I'm trying to implement is a very basic behavior: I have some items in VStack and I'd like to scroll to some item when the VStack is shown. The issue is scrollTo() scrolls too much and leads to invalid UI. See diagram below.

Setup: there are 3 items in the VStack and I'd like to scroll to the last one.

 +--------------+   +--------------+
 |      c       |   |      a       |
 |              |   |      b       |
 |              |   |      c       |
 |              |   |              |
 |              |   |              |
 |              |   |              |
 |              |   |              |
 |              |   |              |
 +--------------+   +--------------+

  (a) What I saw    (b) What I expected

The UI in diagram a is invalid and misleading because 1) it's impossible for user to get such UI interactively, and 2) when user see it he would think there is only one item.

I have been investigating how to work around the issue but couldn't find one. I found two discussion that might be relevant, but both of them were about iOS 15. I don't find any discussion on a similar issue on iOS 16.

Below is the code to reproduce the issue. I filed FB12173661 yesterday. I wonder if anyone has a workaround? Thanks.

My environment: Xcode 14.2, iOS 16.3.1. I'm installing Xcode 14.3 and will verify it on iOS 16.4 soon.

struct Item: Identifiable {
    var id: UUID = UUID()
    var value: String
}

struct TestView: View {
    var items: [Item]
    var scrollTo: UUID
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack {
                    ForEach(items) { item in
                        Text(item.value)
                    }
                }
                .onAppear {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                        proxy.scrollTo(scrollTo, anchor: .top)
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    var items: [Item] =  [Item(value: "a"), Item(value: "b"), Item(value: "c")]
    
    var body: some View {
        TestView(items: items, scrollTo: items.last!.id)
            .frame(maxWidth: .infinity)
    }
}

It is doing what you asked for in

        TestView(items: items, scrollTo: items.last!.id)

You scroll so that last item ("c") is at the top of the scrollView. That's exactly what you get.

If you change to:

        TestView(items: items, scrollTo: items.first!.id)

You get what you expect.

Note: why do you need DispatchQueue.main.asyncAfter ?

Claude31, I'm afraid I can't agree with your opinion. Let me describe my requirements, which I believe it's very typical ones and make sense to most, if not all, people:

  1. When specifying .top option, the item to be scrolled to should show up on the top of the screen.
  2. However, if that pushes the items before it outside screen and, at the same time, there are no enough items after it to fill up the screen, SwiftUI should find a proper position to show it.

The above requirements are exactly the behavior of scrollTo() in List. I don't understand why VStack/LazyVStack/LazyVGrid don't implement the same behavior. Why would anyone like to have their current (buggy) behavior? Can you think of any use cases? In my opinion that behavior doesn't make sense at all. If SwiftUI engineers really interpret the .top value that way (I really doubt it), then I think they should introduce another option value to support List like scrolling behavior.

I can't use the code you suggest, because that's not my. purpose. I'd like to scroll to the last item. It's just that SwiftUI should handle the corner cases properly.

Regarding the async call, it's required when navigating to a view.

set the anchor to nil. The documentation for scrollTo: says

If anchor is nil, this method finds the container of the identified view, and scrolls the minimum amount to make the identified view wholly visible.

Thanks, but I don't want to set anchor to nil, because it has the same result as setting anchor to .bottom in this case, which in my opinion is bad UX (I want the item to show up on top of the screen when the LazyVStack has a lot of items).

Background: my app currently uses List and setting anchor to .top works well. I was considering to change List to LazyVStack and ran into this issue.

EDIT: or did you suggest it as workaround? Yes, it works, but unfortunately it's bad UX. I'm thinking to keep using List until the issue is fixed.

Is this issue fixed? Any update?

scrollTo() leads to invalid UI in VStack in iOS 16
 
 
Q