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

SwiftUI聊天系统ScrollView递归加载问题:滚动焦点异常触发重复加载

SwiftUI聊天系统滚动加载更多递归触发问题修复

问题描述

开发聊天系统时,需求是滚动到接近底部时自动加载更多消息,但当前实现中滚动焦点始终停留在底部,导致加载更多函数被递归触发。尝试调整.flipped()修饰符的位置后问题仍未解决,简化后的代码如下:

import Foundation
import SwiftUI

struct TestChatUIView: View {
    @State var models: [TestChatModel2] = []
    
    init() {

    }
    var body: some View {
        ZStack(alignment: .top) {
            ScrollViewReader { scrollViewProxy in
                ScrollView {
                    LazyVStack(spacing: 0) {
                        ForEach(models.indices, id: \.self) { index in
                            VStack {
                                let message = models[index]
                                Text("\(message.id)")
                                    .id(message.id)
                                    .flipped()
                                    .frame(minHeight: 40)
                                    .onAppear {
                                        if index < (3) {
                                            
                                            for i in 0...20 {
                                                let newModel = TestChatModel2(id: "id \(models.count + 1)", date: Date())
                                                models.insert(newModel, at: 0)
                                            }
                                        }
                                    }
                            }
                        }
                        
                    }
                }
                .flipped()
                    
            }
            
            VStack {
                Spacer()
                Button {
                    for int in 0...30 {
                        models.insert(.init(id: "id \(models.count + 1)", date: Date()), at: 0)
                    }
                } label: {
                    Text("loadMore")
                        .background(Color.red)
                }

            }
        }
    }
}

struct TestChatModel2: Identifiable {
    var id: String = UUID().uuidString
    var date: Date
}
extension View {
    
    func flipped() -> some View {
        self.rotationEffect(.radians(Double.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

问题根源

  1. 视图翻转导致坐标混乱:整个ScrollView被.flipped()翻转后,视觉上的“底部”对应ScrollView实际的顶部,绑定在数组前几个元素的onAppear会在滚动到视觉底部时触发,但每次插入新元素到数组头部又会让这些元素重新出现,形成递归调用。
  2. 无加载状态控制:没有判断是否正在加载,导致加载逻辑被重复触发。

修复方案

1. 放弃全局视图翻转,改用常规布局逻辑

聊天系统的常规做法是新消息添加到数组尾部,通过布局让消息从底部开始排列,避免翻转整个ScrollView带来的坐标混乱。

2. 添加加载状态锁

用isLoading标记是否正在加载,防止递归触发加载逻辑。

3. 正确绑定加载触发点

通过监听滚动偏移判断是否到达顶部(对应视觉上的底部,需加载历史消息),替代依赖元素index的触发方式。

修改后的完整代码

import Foundation
import SwiftUI

struct TestChatUIView: View {
    @State var models: [TestChatModel2] = []
    @State private var isLoading = false // 加载状态锁
    
    init() {
        // 初始化测试数据
        for i in 0..<20 {
            models.append(TestChatModel2(id: "id \(i)", date: Date()))
        }
    }
    
    var body: some View {
        ZStack(alignment: .bottom) {
            ScrollViewReader { scrollViewProxy in
                ScrollView {
                    VStack(spacing: 0) {
                        // 用Spacer把消息顶到ScrollView底部
                        Spacer(minLength: 0)
                        
                        LazyVStack(spacing: 8) {
                            ForEach(models) { message in
                                Text("\(message.id)")
                                    .id(message.id)
                                    .frame(minHeight: 40)
                                    .padding(.horizontal, 12)
                            }
                        }
                    }
                }
                .onAppear {
                    // 初始滚动到底部
                    scrollToBottom(scrollViewProxy: scrollViewProxy)
                }
                // 监听滚动偏移,判断是否到达顶部(需要加载历史消息)
                .overlay(
                    GeometryReader { geo in
                        Color.clear
                            .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).minY)
                    }
                )
                .onPreferenceChange(ScrollOffsetKey.self) { offset in
                    // 当滚动到顶部附近时加载更多
                    if offset > 50 && !isLoading {
                        loadMoreHistory()
                    }
                }
            }
            .coordinateSpace(name: "scrollView")
            
            // 加载更多按钮
            Button {
                loadMoreNewMessages()
            } label: {
                Text("Add New Messages")
                    .padding()
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
            .padding(.bottom, 16)
        }
    }
    
    // 加载历史消息(添加到数组头部)
    private func loadMoreHistory() {
        guard !isLoading else { return }
        isLoading = true
        
        // 模拟网络请求延迟
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            let newModels = (0..<20).map {
                TestChatModel2(id: "history id \(models.count + $0)", date: Date())
            }
            models.insert(contentsOf: newModels, at: 0)
            isLoading = false
        }
    }
    
    // 添加新消息(添加到数组尾部)
    private func loadMoreNewMessages() {
        let newModels = (0..<5).map {
            TestChatModel2(id: "new id \(models.count + $0)", date: Date())
        }
        models.append(contentsOf: newModels)
        // 滚动到底部显示新消息
        DispatchQueue.main.async {
            if let lastId = models.last?.id {
                ScrollViewProxy.scrollTo(lastId)
            }
        }
    }
    
    // 滚动到底部
    private func scrollToBottom(scrollViewProxy: ScrollViewProxy) {
        DispatchQueue.main.async {
            if let lastId = models.last?.id {
                scrollViewProxy.scrollTo(lastId, anchor: .bottom)
            }
        }
    }
}

struct TestChatModel2: Identifiable {
    var id: String = UUID().uuidString
    var date: Date
}

// 自定义偏好键,用于监听滚动偏移
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

关键修改说明

  • 布局调整:用Spacer将消息容器顶到ScrollView底部,实现消息从下往上排列的效果,无需翻转视图。
  • 加载状态控制:isLoading变量确保同一时间只有一次加载请求在执行,彻底避免递归触发。
  • 滚动偏移监听:通过GeometryReader和自定义PreferenceKey监听滚动位置,触发逻辑更可靠,不受元素index变化影响。
  • 消息添加逻辑:新消息添加到数组尾部,历史消息插入到头部,完全符合聊天系统的常规交互逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 10:11:14