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)
}
}
}
}
}
}
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)
}
}
I found a solution from your examples and Apple's example.
No hacks needed.
The correct combination of that elements is required: ScrollViewReader + ScrollView + onGeometryChange + scrollTo + Dummy.
onGeometryChange:struct ContentView: View {
@Namespace var scrollToRight
@State var items: [Int] = []
var body: some View {
ScrollViewReader { scrollProxy in
Button("Add New Item and Scroll to right") {
self.items.append(self.items.count + 1)
}
ScrollView(.horizontal) {
HStack(spacing: 0) {
HStack(spacing: 10) {
ForEach(self.items.sorted(), id: \.self) { value in
Text("\(value)")
}
}
.onGeometryChange(for: CGSize.self) { geoProxy in geoProxy.size } action: { _ in
scrollProxy.scrollTo(scrollToRight) // scroll is here
}
Color.clear
.frame(width: .zero, height: .zero)
.id(scrollToRight)
}
.padding(20)
.background(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.border(.black)
}.frame(width: 300, height: 200)
}
}
button click:struct ContentView: View {
@Namespace var scrollToRight
@State var items: [Int] = []
var body: some View {
ScrollViewReader { scrollProxy in
Button("Add New Item and Scroll to right") {
self.items.append(self.items.count + 1)
scrollProxy.scrollTo(scrollToRight) // scroll is here
}
ScrollView(.horizontal) {
HStack(spacing: 0) {
HStack(spacing: 10) {
ForEach(self.items.sorted(), id: \.self) { value in
Text("\(value)")
}
}
Color.clear
.frame(width: .zero, height: .zero)
.id(scrollToRight)
}
.padding(20)
.background(.white)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.border(.black)
}.frame(width: 300, height: 200)
}
}
Apple docs: developer.apple.com/documentation/swiftui/scrollviewreader
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With