我构建了一个
ScrollView
,其中包含 0-3 个图像和 VStack
中的多行文本字段。我还在滚动视图内添加了一个 ScrollViewReader
,并在某些事件(用户开始输入、图像集合更改)时使用它滚动到文本字段的底部。
关键是:有时有效,有时无效。当它不起作用时,我意识到,当我手动滚动一点然后重试(例如打字)时,它就起作用了。
不确定这是否相关,但是只要
ImageOrPlaceholderComponent
内的图像为零,currentEntryImages
首先显示一个占位符,之后的图像(两种状态都意味着对currentEntryImages
的更改,因此应该滚动到文本字段的底部)。
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)
}
}
}
}
}
}
面临类似的问题。虽然这不是一个完美的解决方案,但我注意到对scrollTo函数进行速率限制解决了这个问题。
一旦我更新,比如说,每 5 次更新一次,它就会按预期滚动,不需要先手动滚动。
我的猜测是内部滚动逻辑出现了问题,从我看来,ScrollViewReader 没有公开任何内容来帮助我们调节使用情况。
发现了另一种黑客,似乎比速率限制的更可靠(留下答案,因为这是一种有时有效的快速而肮脏的解决方案)
假设我们看到的行为是 ScrollView 或 ScrollViewReader 中的某些内部状态中断,我想出了一种“帮助”内部状态机的方法。
这是超级黑客,但似乎对我来说非常有效。技巧是在滚动内容的底部有两个不同的空视图,然后间歇性地滚动到每个视图的底部。
所以我的滚动内容以:
结尾Text("")
.frame(height: 0.0)
.id(2)
Text("")
.frame(height: 0.0)
.id(3)
在我的测试中,这些黑客观点比
EmptyView()
的观点更好。再次强调,这里是黑匣子领域,所以将允许它:)
然后在滚动代码中,您根据自己的计数逻辑执行此操作:
if (newStepCount % 3 == 0) {
DispatchQueue.main.async {
proxy.scrollTo(newStepCount % 2 == 0 ? 2 : 3, anchor: .bottom)
}
}
我遇到了这个问题,并通过在滚动视图顶部放置另一个标记并按顺序调用来解决它:
DispatchQueue.main.async {
reader.scrollTo(topID, anchor: .top)
reader.scrollTo(bottomID, anchor: .bottom)
}
我不知道为什么会这样。
尺寸变化时保持底部滚动的 ScrollView 示例:
struct ScrollViewPinnedBottom<Content:View>: View {
@Namespace var bottomID
@Namespace var topID
@ViewBuilder var content: () -> Content
@StateObject private var model = ScrollViewPinnedBottomModel()
var body: some View {
ScrollViewReader { reader in
ScrollView(.vertical) {
VStack {
Color.clear
.frame(height: .zero)
.id(topID)
content()
Color.clear
.frame(height: .zero)
.id(bottomID)
}
.readSize { size in
if model.shouldScroll(newSize: size) {
DispatchQueue.main.async {
reader.scrollTo(topID, anchor: .top)
reader.scrollTo(bottomID, anchor: .bottom)
}
}
}
}
}
}
}
class ScrollViewPinnedBottomModel: ObservableObject {
var lastSize: CGSize?
func shouldScroll(newSize: CGSize) -> Bool {
guard newSize.height != lastSize?.height else { return false }
lastSize = newSize
return true
}
}
此示例使用此 readSize 扩展。