我目前在应用程序的 ChatView 上遇到困难。这是我想要实现的示例代码。它具有以下功能:当用户即将到达顶部时,它会获取较旧的消息;当用户到达底部时,它会获取较新的消息。
问题是当用户获取更新的消息时,滚动视图不断滚动到底部,并且这种情况会递归发生。获取旧消息时出现同样的问题(它连续滚动到顶部),但我使用 Flipped() 修饰符修复了它。但底部问题仍然存在。
struct ChatMessage: View {
let text: String
var body: some View {
HStack {
Text(text)
.foregroundStyle(.white)
.padding()
.background(.blue)
.clipShape(
RoundedRectangle(cornerRadius: 16)
)
.overlay(alignment: .bottomLeading) {
Image(systemName: "arrowtriangle.down.fill")
.font(.title)
.rotationEffect(.degrees(45))
.offset(x: -10, y: 10)
.foregroundStyle(.blue)
}
Spacer()
}
.padding(.horizontal)
}
}
struct Message : Identifiable,Equatable {
var id: Int
var text: String
}
struct GoodChatView: View {
@State var messages: [Message] = []
@State private var scrollViewProxy: ScrollViewProxy? // Store the proxy
@State var messageId: Int?
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(messages, id: \.id) { message in
ChatMessage(text: "\(message.text)")
.flippedUpsideDown()
.onAppear {
if message == messages.last {
print("old data")
loadMoreData()
}
if message == messages.first {
print("new data")
loadNewData()
}
}
}
}
.scrollTargetLayout()
}
.flippedUpsideDown()
.scrollPosition(id: $messageId)
.onAppear {
for i in 1...20 {
let message = Message(id: i, text: "\(i)")
messages.append(message)
}
messageId = messages.first?.id
}
}
}
func loadMoreData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let count = messages.count
var newMessages: [Message] = []
for i in count+1...count+20 {
let message = Message(id: i, text: "\(i)")
newMessages.append(message)
}
messages.append(contentsOf: newMessages)
}
}
func loadNewData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let count = messages.count
var newMessages: [Message] = []
for i in count+1...count+20 {
let message = Message(id: i, text: "\(i)")
newMessages.append(message)
}
newMessages = newMessages.reversed()
messages.insert(contentsOf: newMessages, at: 0)
}
}
}
struct FlippedUpsideDown: ViewModifier {
func body(content: Content) -> some View {
content
.rotationEffect(.radians(Double.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
extension View {
func flippedUpsideDown() -> some View {
modifier(FlippedUpsideDown())
}
}
任何帮助表示赞赏。如果还有其他方法可以实现这一目标,请告诉我🙂
您的示例代码是通过使用
.scrollPosition
上的 ScrollView
以及 .scrollTargetLayout
上的 LazyVStack
来设置滚动位置。问题是,.scrollPosition
只能有一个锚点。如果锚点是.top
,那么很难将位置设置到底部的最后一个条目,当锚点是.bottom
时,顶部的第一个条目也是如此。所以:
A
ScrollViewReader
也许是设置滚动位置的更好方法。这允许向 ScrollViewProxy.scrollTo
提供不同的锚点。您的示例中已经有 ScrollViewReader
,但尚未使用它。
可以删除
.scrollTargetLayout
和.scrollPosition
的使用。
其他注意事项:
我认为你可以删除所有的翻转。我认为没有必要,它只会让一切变得复杂。请改用
ForEach
中的反转数组。
为了防止数据加载立即触发另一个加载,我建议使用一个简单的标志
isLoading
。这应该在加载之前设置。加载完成后,可以在短暂延迟后重置。这可以确保在滚动调整发生时加载保持阻塞。
添加“新”消息时,它们会出现在
ScrollView
的底部。添加新条目后无需执行 scrollTo
,因为 ScrollView
无论如何都会保留其位置。
将“旧”消息添加到顶部是更困难的部分。如果
ScrollView
正在主动滚动(即,如果用户的手指处于活动状态),则 scrollTo
将被忽略并显示最高条目。作为解决方法,可以在每个条目后面放置一个 GeometryReader
来检测相对滚动偏移。仅当偏移量(几乎)恰好为 0 时才应触发加载。当用户手动滚动时,这种情况仍然可能发生,但可能性要小得多。通常,仅当 ScrollView
静止时才会触发负载。
我尝试更新代码以使用
Task
和 async
函数而不是 DispatchQueue
。然而,我不得不承认,我在这方面是一个初学者,任何有更多经验的人可能一眼就能发现。如果有任何建议,很乐意纠正。特别是我不确定是否有必要指定这么多@MainActor
。
在初始加载时,需要将滚动位置设置为底部在添加了第一次加载的数据之后。我尝试使用 Task.yield()
来进行更新,但这并不总是可靠的。所以现在它休眠了 10 毫秒。我希望也可能有更好的方法来做到这一点。
struct GoodChatView: View {
@State private var messages: [Message] = []
@State private var isLoading = true
var body: some View {
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(messages.reversed(), id: \.id) { message in
ChatMessage(text: "\(message.text)")
.background {
GeometryReader { proxy in
let minY = proxy.frame(in: .scrollView).minY
let isReadyForLoad = abs(minY) <= 0.01 && message == messages.last
Color.clear
.onChange(of: isReadyForLoad) { oldVal, newVal in
if newVal && !isLoading {
isLoading = true
Task { @MainActor in
await loadMoreData()
await Task.yield()
scrollView.scrollTo(message.id, anchor: .top)
await resetLoadingState()
}
}
}
}
}
.onAppear {
if !isLoading && message == messages.first {
isLoading = true
Task {
await loadNewData()
// When new data is appended, the scroll position is
// retained - no need to set it again
await resetLoadingState()
}
}
}
}
}
}
.task { @MainActor in
await loadNewData()
if let firstMessageId = messages.first?.id {
try? await Task.sleep(for: .milliseconds(10))
scrollView.scrollTo(firstMessageId, anchor: .bottom)
}
await resetLoadingState()
}
}
}
@MainActor
func loadMoreData() async {
let lastId = messages.last?.id ?? 0
print("old data > \(lastId)")
var oldMessages: [Message] = []
for i in lastId+1...lastId+20 {
let message = Message(id: i, text: "\(i)")
oldMessages.append(message)
}
messages += oldMessages
}
@MainActor
func loadNewData() async {
let firstId = messages.first?.id ?? 21
print("new data < \(firstId)")
var newMessages: [Message] = []
for i in firstId-20...firstId-1 {
let message = Message(id: i, text: "\(i)")
newMessages.append(message)
}
messages.insert(contentsOf: newMessages, at: 0)
}
@MainActor
private func resetLoadingState() async {
try? await Task.sleep(for: .milliseconds(500))
isLoading = false
}
}