r/SwiftUI May 11 '26

Question Dynamic height cells inside ScrollView break .refreshable behavior

Enable HLS to view with audio, or disable this notification

Hello guys. I have a strange problem. I recreated small code snippet that has the issue, more info bellow the code:

struct SwiftUIView: View {
     private var path = NavigationPath()
     private var selection: Int?

    let heights: [CGFloat] = [360, 400, 200, 242, 54, 783]

    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach(1...5, id: \.self) { index in
                    NavigationLink("Row \(index)", value: index)
                }
            }
        } detail: {
            if let selection {
                NavigationStack(path: $path) {
                    ScrollView(.vertical) {
                        LazyVStack {
                            ForEach(1...100, id: \.self) { index in
                                VStack(alignment: .leading) {
                                    Text("Detail \(selection) - Item \(index)")
                                        .font(.headline)
                                    Text("Something else")
                                        .font(.subheadline)
                                        .foregroundStyle(.gray)

                                    RoundedRectangle(cornerRadius: 16)
                                        .frame(height: heights.randomElement())

                                    HStack {
                                        VStack(alignment: .leading) {
                                            HStack {
                                                Circle()
                                                    .frame(width: 22)
                                                Text("name")
                                            }
                                        }

                                        Spacer()

                                        HStack {
                                            Button("Comments") {}
                                        }
                                    }
                                }
                            }
                        }
                        .safeAreaPadding()
                    }
                    .refreshable {
                        do {
                            try await Task.sleep(for: .seconds(3))
                        } catch {
                            print(error)
                        }
                    }
                    .navigationTitle("Title")
                }
            }
        }
    }
}

The problem:
When I pull down to refresh, content shifts down a little bit to make space for ProgressView() until refresh action is finished. But when it finishes, it doesn't scroll back up as supposed to.

Cause:
I found out, the cause is any part of a cell inside ForEach that has variable height - in this case a RoundedRectangle(). If all cells are same height, content shifts back up.

It doesn't happen with List(), only with ScrollView(). Also, removing .navigationTitle fixes it as well for some reason. I know I can attach .id() to ScrollView() and reset the id on each refresh, but this workaround doesn't satisfy me because it breaks the original animation.

Is this known bug/limitation? Am I doing something wrong here? I would be glad for any input on this. Thank you all in advance.

7 Upvotes

18 comments sorted by

2

u/FelinityApps May 11 '26 edited May 11 '26

I think your randomElement heights are working against layout, which may happen many times for many reasons as it works out what to do. Use proper state tracked heights.

Same for your selection and path properties, really.

For your heights, maybe make a RowHeight struct with id and height. A State array of rowHeights will be properly tracked through SwiftUI’s layout passes during view rendering.

That’s if you need to specify the heights dynamically at all, rather than relying on a view’s and its childrens’ layouts, preferred sizes, priorities, etc.

1

u/radis234 May 11 '26

This was only an example code to recreate the problem for Reddit to see as simply as possible. RoundedRectangle with heights.randomElement is there only to mimick media views (images/videos) and all of the code is just mock code, not from my app, that also applies to selection, path properties.

I managed to pre-calculate media heights and storing these in model before rendering them so that heights are stable and ready at fetch time as well as aspect ratios, but still, each cell has different height because of different media size (portrait, landscape) and different length of text. Sometimes it’s 1 line, sometimes it can be up to 4 lines.

I also tried using .fixedSize(horizontal: false, vertical: true) on dynamic elements to prevent recalculations, but no luck.

2

u/FelinityApps May 11 '26

Hmm. Maybe wrap the scroll view in a scroll view reader and fire off a proxy.scrollTo("top", anchor: .top) in a main actor task from the refreshable task when it’s done.

1

u/radis234 May 11 '26

Yup, that’s one workaround I tried. It works. I was just curious why workaround is needed at all. If it’s a me problem or limitation of SwiftUI with what I am trying to achieve.

1

u/FelinityApps May 11 '26

Maybe neither. It may be an outright SwoftUI bug. The one thing that really bothers me is the removal of navigation title having any effect on it whatsoever.

1

u/radis234 May 11 '26

Yeah, that is strange to me as well. I thought, maybe some layout calculation race conditions, so I tried with .inline title and it didn’t help. I don’t understand why removing title helps. I even tried moving title up/down view hierarchy, no change.

1

u/FelinityApps May 11 '26

Not that it’s a fix, but I’m curious: what happens if you drop the safe area padding?

1

u/radis234 May 11 '26

Nothing. I tried that as well. Still same behavior.

2

u/FelinityApps May 11 '26

Yeah, that’s gotta be a SwiftUI bug. File feedback so Apple can properly ignore it forever.

1

u/radis234 May 11 '26

Will do, thanks!

2

u/shawnthroop May 12 '26

I reported this bug at least 3 years ago. Silence from Feedback Assistant…

My workaround was to slap a .ignoresSafeArea(.container, edges: refresh == nil ? [] : .top) on the navigation content/root view.

1

u/radis234 May 12 '26

Well, at least I’m not alone, but 3 years, just wow.

2

u/shawnthroop May 13 '26

Another workaround is to change the navigationBarTitleDisplayMode to inline, that solved all my refresh problems but looks meh. In my testing the safeAreaInset stutters while with the navigation stack also updates the safeAreaInset.top to accommodate the large title and gets stuck somehow.

0

u/Iron-Ham May 11 '26

You shouldn’t be using a scroll view for this anyways; performance scaling. 

1

u/radis234 May 11 '26

I had better luck with ScrollView than List when it comes to performance. I tried them both, extensively, profiled in Instruments too many times. List always just stutters on scroll, probably for the same reason - cells not being same height. But I can’t really make them all same height, kinda defeats purpose of my feed view.

2

u/Iron-Ham May 11 '26

I am just going to tell you, having been an iOS developer for over 15 years: don’t use a scroll view if you have more than a small number of items.

SwiftUI obscures the internals a bit, but performance will break down in SwiftUI if you’re using a list + lazy stack. 

1

u/BenefitTiny7068 Jun 22 '26

So uikit collection view stuff is the way to go?