添加新项目时保留 ScrollView 中的滚动位置

问题描述 投票:0回答:1

我目前在应用程序的 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())
    }
}

任何帮助表示赞赏。如果还有其他方法可以实现这一目标,请告诉我🙂

swiftui chat swiftui-scrollview
1个回答
0
投票

您的示例代码是通过使用

.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 } }
    
© www.soinside.com 2019 - 2024. All rights reserved.