英文:
Have ScrollView always scrolled to bottom
问题
我有一个固定高度的ScrollView,其中包含一个Text(),它不断地从我的viewModel中更新。
如果文本内容太长,一次无法全部显示,即需要滚动才能看到文本末尾,我希望它自动滚动,以便我始终看到文本的末尾。
这可能吗?
ScrollView {
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
}
.frame(height: 200)
注:这是我问题的非常简化版本。在我的应用程序中,有时文本不会被更新,但确实需要可滚动。
我尝试过scrollViewReader… 类似于:
ScrollView {
ScrollViewReader() { proxy in
Text(vm.text)
.frame(minWidth: 20, alignment: .leading)
Text("").id(0)
}
}
.frame(height: 200)
尝试的想法是滚动到空的Text,但我无法找到如何触发动画:
withAnimation {
proxy.scrollTo(0)
}
...我看到的所有示例都使用按钮,但我需要在文本更新时触发。
英文:
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.
答案1
得分: 2
你可以使用 .onChange
来对 vm.text
的更改做出反应,然后滚动到末尾。
请注意,ScrollView
应该在 ScrollViewReader
内部!原则上应该是这样工作的:
struct ContentView: View {
@State private var vmtext = "测试文本"
// 用于测试的文本更改定时器
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Text(vmtext)
.id(0)
}
// 对文本更改作出反应,滚动到末尾
.onChange(of: vmtext) { newValue in
proxy.scrollTo(0, anchor: .bottom)
}
}
.frame(width: 100, height: 200, alignment: .leading)
.border(Color.primary)
.padding()
// 用于测试的文本更改定时器
.onReceive(timer) { _ in
vmtext += " 添加新文本"
}
}
}
我在这里遇到的问题是,它只有在手动滚动滚动视图一次之后才能正常工作。
英文:
You can use .onChange
to react to change of vm.text
and then scroll to the end.
Please note that ScrollView
should be inside the ScrollViewReader
!
In principle it would work like this:
struct ContentView: View {
@State private var vmtext = "Test Text"
// timer change of text for testing
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Text(vmtext)
.id(0)
}
// react on change of text, scroll to end
.onChange(of: vmtext) { newValue in
proxy.scrollTo(0, anchor: .bottom)
}
}
.frame(width: 100, height: 200, alignment: .leading)
.border(.primary)
.padding()
// timer change of text for testing
.onReceive(timer) { _ in
vmtext += " added new text"
}
}
}
The problem a have here is that it only works after the scrollview has been scrolled manually once.
答案2
得分: 0
我已经根据您提供的代码进行了翻译,以下是翻译好的内容:
我已经根据 [@ChrisR][1] 的代码进行了适应,并通过一个小技巧展示了 scrollTo: 如何工作。它在两个几乎相同的具有不同 ID 的视图之间切换(一个有一点点填充)。
import SwiftUI
struct ContentView: View {
@State private var vmtext = "\n\n\n\n测试文本"
@State private var number = 0
@State private var id = 0
// 用于测试文本更改的计时器
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)
}
}
// 对文本更改作出反应,滚动到底部
.onChange(of: vmtext) { newValue in
print("onChange 进入。ID:\(id)")
proxy.scrollTo(id, anchor: .bottom)
}
}
.frame(height: 90, alignment: .center)
.frame(maxWidth: .infinity)
.border(.primary)
.padding()
// 用于测试文本更改的计时器
.onReceive(timer) { _ in
if id == 0 { id = 1 } else { id = 0 }
number += 1
vmtext += " 单词\(number)"
}
}
}
注意:
如果要显示的文本视图的高度没有改变,proxy.scrollTo 会被忽略。将填充设置为相同,该小技巧就会失效。
从 vmtext 变量中移除 "\n\n\n\n" 会破坏该小技巧。它们使文本视图的初始大小大于滚动视图窗口,因此立即可滚动 - 或类似于此。如果移除它们,滚动将在您用手指进行初始滚动后开始工作。
编辑:
这是一个没有填充和 "\n\n\n\n" 小技巧的版本,它使用了 [双旋转小技巧][2]。
import SwiftUI
struct ContentView: View {
@State private var vmtext = "测试文本"
@State private var number = 0
@State private var id = 0
// 用于测试文本更改的计时器
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))
// 对文本更改作出反应,滚动到底部
.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()
// 用于测试文本更改的计时器
.onReceive(timer) { _ in
number += 1
vmtext += " 单词\(number)"
}
}
}
希望文本从视图顶部开始,并且只有在文本填满视图后才滚动,就像 SwiftUI TextEditor 一样... 而且 withAnimation 不起作用。
[1]: https://stackoverflow.com/users/17896776/chrisr
[2]: https://stackoverflow.com/a/63635180/10307262
请注意,代码部分没有翻译,仅返回翻译好的部分。
英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论