2

The problem

When I populate a LazyVStack with items of varying heights, the ScrollView around it does not behave as expected on macOS 13. This simplified example is still somewhat usable, but as you can see in the screen recording (I'm scrolling very slowly), the scroll position jumps around. In my production app this behavior gets much worse, making it sometimes impossible to scroll upwards again.

enter image description here

Code

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(1...40, id: \.self) { value in
                    Item(value: value)
                        .id(value)
                        .padding()
                }
            }
            .padding()
        }
        .frame(width: 300, height: 300)
    }
}

struct Item: View {
    let value: Int
    let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

    var body: some View {
        if value < 20 {
            Text("\(value) A long text: \(lorem)")
        }
        else {
            Text("\(value) A short text")
        }
    }
}

iOS vs. macOS

I have found this post regarding the same issue on iOS. I also ran my code on iOS and while the scroll indicator changes its height (which would indicate a change in content size), the scrolling was smooth without jumps of the scroll position.

Question

Has anyone had the same issue and found a way to make this work? The example would easily work with a regular VStack, but I need to use LazyVStack for performance reasons and the ability to add a sticky header.

viedev
  • 869
  • 1
  • 6
  • 16
  • 1
    Not sure there **could** be a way to make that work... When dragging the scroll-thumb, we are effectively saying *"scroll the content to **this percentage** of the total content height"* -- but, using a `LazyVStack`, we don't **know** the total content height until we've scrolled all the way to the bottom. – DonMag Dec 20 '22 at 20:10
  • 1
    I'm also worried that it might not be possible... Upon testing further, it seems the `LazyVStack` estimates the total content height based the heights of the currently visible subviews. Unfortunately `LazyVStack` does not "remember" its total content height after I have scrolled through the entire range, so the total height changes constantly while scrolling. – viedev Dec 21 '22 at 14:19
  • Yep, that appears to be exactly what's happening. Of course, this is only noticeable when dragging the scroll thumb (does anybody do that these days?). If you have a **compelling reason** to use `Lazy`, I'm guessing you're out of luck... may need to re-think your UI. – DonMag Dec 21 '22 at 15:12
  • @viedev any update on a fix? I've resorted to using the same height, but recently some items are much taller than others and they start overlapping the outside views – Sam Chahine Feb 01 '23 at 03:05
  • As far as I can tell, there is no fix for this unfortunately. I resorted to using a regular VStack and made my views as light-weight as possible. It's not a scalable solution, but it works for my use case with only a few dozen views. – viedev Feb 01 '23 at 09:39

1 Answers1

0

From what I was able to find, this still seems to be a bug.

I was able to put together a solution where it does not jump around, but you will probably need to adjust the content to be spaced out correct. I did a quick adjustment with negative padding, but some more work will need to be done to have it styled like you had previously.

I think the issue comes from the frame applied to your ScrollView. For some reason the LazyVStack does not like that. Try just removing the frame on the ScrollView - the jumping goes away.

It looks like you can apply a frame to your Item view and that prevents it from jumping. Example below:

struct ContentView: View {
var body: some View {
    ScrollView {
        LazyVStack {
            ForEach(1...40, id: \.self) { value in
                Item(value: value)
                    .id(value)
                    .padding(.bottom, -100)
            }
        }
    }
    .frame(width: 300, height: 300)
  }
}

struct Item: View {
    let value: Int
    let lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
    
    var body: some View {
        Group {
            if value < 20 {
                Text("\(value) A long text: \(lorem)")
            }
            else {
                Text("\(value) A short text")
            }
        }
        .frame(width: 300, height: 300)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
nickreps
  • 903
  • 8
  • 20
  • 1
    Thanks for taking the time to answer! I think the reason why the `ScrollView` in your code does scroll smoothly, is that you have added a fixed size to `Item`, so all items have the same height again. This is not what I need though. Items should be able to have any height. I also tried removing `.frame` from `ScrollView` but it does not change the behavior. – viedev Dec 19 '22 at 17:45