2

I've build a ScrollView which contains 0-3 images and a multiline text field in a VStack. I also added a ScrollViewReader inside the scrollview and use it to scroll to the bottom of the text field upon certain events (user starts typing, image collection changes).

The point is: sometimes it works, sometimes it doesn't. When it does not work I realized, that when I scroll a little bit by hand and then try again (e.g. typing) it works.

Not sure if this is relevant, but ImageOrPlaceholderComponent first shows a placeholder as long as the image within currentEntryImages is nil, and the image after that (both states imply a change to currentEntryImages and should thus result in scrolling to the bottom of the text field).

        NavigationStack {
            ScrollView {
                ScrollViewReader { scrollview in
                    VStack {
                        // Attached images.
                        AnyLayout(VStackLayout(spacing: 2.5)) {
                            ForEach(values: currentEntryImages) { entryImage in
                                ImageOrPlaceholderComponent(image: entryImage)
                                    .clipped()
                            }
                        }

                        // Text field for the entry with toolbar.
                        TextField("...", text: $entryDTO.text, axis: .vertical)
                            .id(entryTextFieldAnchor)
                            .multilineTextAlignment(.leading)
                            .padding()
                            .focused($mainTextFieldFocused)
                            .onAppear { mainTextFieldFocused = true }

                            // Scroll to the bottom of the text field, when the user is typing ...
                            .onChange(of: entryDTO.text) { _ in
                                withAnimation {
                                    scrollview.scrollTo(entryTextFieldAnchor, anchor: .bottom)
                                }
                            }

                            // ... or the entry images have changed.
                            .onChange(of: currentEntryImages) { _ in
                                withAnimation {
                                    scrollview.scrollTo(entryTextFieldAnchor, anchor: .bottom)
                                }
                            }
                    }
      
                }
            }
        }
Sebastian
  • 335
  • 2
  • 10

2 Answers2

1

Was facing a similar problem. While not a perfect solution I noticed that rate-limiting the scrollTo function solved the issue.

Once I updated, say, once every 5 times, it scrolled as should and didn't require manual scrolling first.

My guess is something breaks in the internal scrolling logic, and from what I could see ScrollViewReader doesn't expose anything to help us moderate the usage.

Oded Ben Dov
  • 9,936
  • 6
  • 38
  • 53
  • Thanks for your feedback - I'm currently struggling with another part of my app, but I will check this later! – Sebastian May 02 '23 at 16:18
0

Found another hack, seems more solid than the rate-limiting one (leaving the answer as it is a quick and dirty solution that sometimes work)

Assuming the behavior we are seeing is some internal state breaking in ScrollView or ScrollViewReader, I came up with a way to "help" the internal state machine.

It's super hacky, but seems to work really well for me. The trick is to have two different empty views at the bottom of the scroll content, and then intermittently scroll to the bottom of each of them.

So my scroll content ends with:

Text("")
    .frame(height: 0.0)
    .id(2)
Text("")
    .frame(height: 0.0)
    .id(3)

These hacky views were better than EmptyView()'s in my tests. Again, treading black-box territory here, so will allow it :)

Then in your scrolling code you do this based on your own counting logic:

if (newStepCount % 3 == 0) {
    DispatchQueue.main.async {
        proxy.scrollTo(newStepCount % 2 == 0 ? 2 : 3, anchor: .bottom)
    }
}
Oded Ben Dov
  • 9,936
  • 6
  • 38
  • 53