I have a fixed height ScrollView containing a Text() which is constantly being updated from my viewModel. If the text is too much to be viewed all at once, i.e. I need to scroll to see the end of the text, I’d like it to be automatically scrolled so that I always see the end of the text.
Is that possible?
ScrollView {
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
}
.frame(height: 200)
Note: this is a very simplified version of my problem. In my app there are times when the text is not being updated and it does need to be scrollable.
I have tried scrollViewReader … something like:
ScrollView {
ScrollViewReader() { proxy in
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
Text("").id(0)
}
}
.frame(height: 200)
with the idea of scrolling to the empty Text, but I couldn’t work out how to trigger
withAnimation {
proxy.scrollTo(0)
}
... all the examples I've seen use a button but I need to trigger when the text updates.
I've adapted the code by @ChrisR with a hack that at the very least, shows how i'd like scrollTo: to work. It toggles between 2 almost identical views with different ids (one has a tiny bit of padding)
import SwiftUI
struct ContentView: View {
@State private var vmtext = "\n\n\n\nTest Text"
@State private var number = 0
@State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
id == 0 ?
VStack {
Text(vmtext)
.padding(.top, 0)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.padding(.top, 1)
.font(.title2)
.id(1)
}
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
print("onChange entered. id: \(id)")
proxy.scrollTo(id, anchor: .bottom)
}
}
.frame(height: 90, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
if id == 0 { id = 1} else {id = 0}
number += 1
vmtext += " word\(number)"
}
}
}
Notes: It seems that if the height of the text view being shown hasn't changed the proxy.scrollTo is ignored. Set the paddings the same and the hack breaks.
Removing the "\n\n\n\n" from the var vmtext breaks the hack. They make the initial size of the text view bigger than the scrollview window and so immediately scrollable - or something :-). If you do remove them, scrolling will start working after you do an initial scroll with your finger.
EDIT:
Here is a version without the padding and "\n\n\n\n" hacks, which uses a double rotation hack.
import SwiftUI
struct ContentView: View {
@State private var vmtext = "Test Text"
@State private var number = 0
@State private var id = 0
// timer change of text for testing
let timer = Timer.publish(every: 0.3, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Group {
id == 0 ?
VStack {
Text(vmtext)
.font(.title2)
.id(0)
}
:
VStack {
Text(vmtext)
.font(.title2)
.id(1)
}
}
.padding()
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
withAnimation {
if id == 0 { id = 1 } else { id = 0 }
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame(height: 180, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
number += 1
vmtext += " word\(number)"
}
}
}
It would be nice if the text started at the top of the view and only scrolls when the text has filled up the view, as with the swiftui TextEditor ... and the withAnimation doesn't work.