You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI列表顶部新增条目时保持滚动位置的实现问题

解决SwiftUI List新增顶部条目时滚动位置偏移的问题

当你在messages数组末尾添加新元素(通过reversed()在List顶部显示)时,List默认会自动调整滚动位置,导致用户正在查看的内容被顶走。以下是保留List组件样式与滑动功能的解决方案:

import SwiftUI

struct MessageList: View {
    @ObservedObject var socketManager: WebSocketManager
    @State var isUpdating: Bool = false
    @State private var isScrolledToBottom = true // 标记是否处于列表底部(对应最新消息位置)
    
    @State private var timer: Timer? = nil
    
    func simulateMessages(){
        timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { _ in
            let newItem = Message(content: "Hello", sentFlag: true)
            socketManager.messages.append(newItem)
        }
    }
    
    func stopSimulation(){
        timer?.invalidate()
        timer = nil
    }
    
    var body: some View {
        Button{
            if isUpdating{
                stopSimulation()
            } else {
                simulateMessages()
            }
            isUpdating.toggle()
        } label: {
            Text(isUpdating ? "Stop" : "Update")
        }
        
        ScrollViewReader { proxy in
            List(socketManager.messages.reversed(), id: \.uuid) { message in
                messageView(message: message)
                    .swipeActions(edge: .trailing) {
                        DeleteButton(socketManager: socketManager, message: message)
                    }
                    .swipeActions(edge: .leading){
                        SaveButton(socketManager: socketManager, message: message)
                    }
                    .contextMenu{
                        SaveButton(socketManager: socketManager, message: message)
                        DeleteButton(socketManager: socketManager, message: message)
                    }
            }
            .onAppear {
                // 初始滚动到最新消息
                if let lastMessageID = socketManager.messages.last?.uuid {
                    proxy.scrollTo(lastMessageID, anchor: .bottom)
                }
            }
            .onChange(of: socketManager.messages) { _ in
                // 仅当用户处于列表底部时,自动滚动到最新消息
                if isScrolledToBottom, let lastMessageID = socketManager.messages.last?.uuid {
                    proxy.scrollTo(lastMessageID, anchor: .bottom)
                }
            }
            .onScrollChange { scrollPhase in
                // 监听滚动状态,更新底部标记
                switch scrollPhase {
                case .bottom:
                    isScrolledToBottom = true
                case .top, .middle:
                    isScrolledToBottom = false
                @unknown default:
                    isScrolledToBottom = false
                }
            }
        }
    }
}

#Preview {
    let socketManager = {
        let manager = WebSocketManager()
        manager.messages = [Message(content: "Hello, World! \nthis is a long message \nThis \nnew line\na\nb\nc\nd", sentFlag: true),
                            Message(content: "Hello, World! \nthis is a long message \nThis \nnew line\na\nb\nc\nd", sentFlag: false)]
        return manager
    }()
    MessageList(socketManager: socketManager)
}

关键逻辑说明

  • ScrollViewReader:获取列表滚动代理,精准控制滚动位置
  • isScrolledToBottom:标记用户是否处于列表底部(对应最新消息位置),避免强制滚动干扰用户查看历史内容
  • onChange(of: socketManager.messages):消息数组更新时,仅在用户处于底部时自动滚动到最新消息,其余场景保持当前滚动位置
  • onScrollChange:SwiftUI 4.0+原生滚动状态监听API,实时更新底部标记

兼容旧版SwiftUI(低于4.0)

如果需要兼容更早版本,可替换onScrollChange为自定义滚动检测器:

struct ScrollPositionDetector: UIViewRepresentable {
    var onScrollPositionChange: (Bool) -> Void
    
    func makeUIView(context: Context) -> UIView { UIView() }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.async {
            guard let scrollView = uiView.superview?.superview as? UIScrollView else { return }
            // 预留20pt误差,避免因列表布局微小变化误判
            let isAtBottom = scrollView.contentOffset.y + scrollView.frame.size.height >= scrollView.contentSize.height - 20
            onScrollPositionChange(isAtBottom)
        }
    }
}

在List中添加该检测器替代onScrollChange:

List(...) { /* 列表内容 */ }
.overlay(ScrollPositionDetector { isAtBottom in
    isScrolledToBottom = isAtBottom
})

内容的提问来源于stack exchange,提问作者jarv

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 05:52:42