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

SwiftUI中ScrollView实现帖子分页加载的问题求助

SwiftUI Firestore帖子分页加载实现方案

问题背景

当前项目中SearchViewModel的fetchRandomPosts()会一次性从Firestore获取所有随机帖子并展示,导致UI性能下降。需要实现初始加载18条,用户滚动到底部时追加加载6条的分页逻辑。


修改后的SearchViewModel代码

import FirebaseFirestore
import FirebaseFirestoreSwift

class SearchViewModel: ObservableObject {
    @Published var posts: [PostModel] = []
    @Published var isLoading = false
    
    private let db = Firestore.firestore()
    private var allRandomPosts: [PostModel] = [] // 缓存所有随机后的帖子
    private let initialLoadCount = 18
    private let loadMoreCount = 6
    
    // 初始获取所有随机帖子并加载第一批数据
    func fetchRandomPosts() {
        db.collectionGroup("user_posts")
            .getDocuments { querySnapshot, error in
                guard let documents = querySnapshot?.documents else {
                    print("获取文档错误: \(error?.localizedDescription ?? "未知错误")")
                    return
                }
                
                let posts = documents.compactMap { document -> PostModel? in
                    do {
                        return try document.data(as: PostModel.self)
                    } catch {
                        print("解码帖子错误: \(error.localizedDescription)")
                        return nil
                    }
                }
                
                self.allRandomPosts = posts.shuffled()
                // 加载初始18条
                self.posts = Array(self.allRandomPosts.prefix(self.initialLoadCount))
            }
    }
    
    // 加载更多帖子
    func loadMorePosts() {
        guard !isLoading, posts.count < allRandomPosts.count else { return }
        isLoading = true
        
        // 模拟网络延迟(实际使用可移除)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            let startIndex = self.posts.count
            let endIndex = min(startIndex + self.loadMoreCount, self.allRandomPosts.count)
            let newPosts = Array(self.allRandomPosts[startIndex..<endIndex])
            self.posts.append(contentsOf: newPosts)
            self.isLoading = false
        }
    }
    
    // 保持原有的分组布局逻辑
    func dividePostsIntoGroups() -> [[PostModel]] {
        var groups: [[PostModel]] = []
        var remainingPosts = posts
        
        while !remainingPosts.isEmpty {
            if remainingPosts.count >= 3 {
                groups.append(Array(remainingPosts.prefix(3)))
                remainingPosts = Array(remainingPosts.dropFirst(3))
            } else if remainingPosts.count == 2 {
                groups.append(Array(remainingPosts.prefix(2)))
                remainingPosts = []
            } else if remainingPosts.count == 1 {
                groups.append([remainingPosts.first!])
                remainingPosts = []
            }
        }
        
        return groups
    }
}

修改后的SearchView代码

import SwiftUI
import Kingfisher

struct SearchView: View {
    @StateObject var searchViewModel = SearchViewModel()
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView(.vertical, showsIndicators: false) {
                VStack(spacing: 7) {
                    let groups = searchViewModel.dividePostsIntoGroups()
                    
                    ForEach(groups.indices, id: \.self) { index in
                        let caseIndex = index % 5
                        
                        switch caseIndex {
                        case 0:
                            let post = groups[index].first!
                            let remainingPosts = Array(groups[index].dropFirst())
                            
                            HStack(spacing: 7) {
                                KFImage(URL(string: post.postImage))
                                        .resizable()
                                        .modifier(ImagePostLargeModifier(post: post))
                                
                                VStack(spacing: 7) {
                                    ForEach(remainingPosts) { post in
                                        KFImage(URL(string: post.postImage))
                                            .modifier(ImagePostMinModifier(post: post))
                                    }
                                }
                            }
                            
                        case 1, 2, 4:
                            let posts = groups[index]
                            
                            HStack(spacing: 7) {
                                ForEach(posts) { post in
                                    KFImage(URL(string: post.postImage))
                                        .modifier(ImagePostMinModifier(post: post))
                                }
                            }
                            
                        case 3:
                            let post = groups[index].last!
                            let remainingPosts = Array(groups[index].dropLast())
                            
                            HStack(spacing: 7) {
                                VStack(spacing: 7) {
                                    ForEach(remainingPosts) { post in
                                        KFImage(URL(string: post.postImage))
                                            .modifier(ImagePostMinModifier(post: post))
                                    }
                                }
                                
                                KFImage(URL(string: post.postImage))
                                    .modifier(ImagePostLargeModifier(post: post))
                            }
                            
                        default:
                            EmptyView()
                        }
                    }
                    
                    // 加载状态指示器
                    if searchViewModel.isLoading {
                        ProgressView()
                            .padding(.vertical, 20)
                    } else if searchViewModel.posts.count < searchViewModel.allRandomPosts.count {
                        // 检测滚动到底部的触发器
                        GeometryReader { geo in
                            Color.clear
                                .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .global).minY)
                        }
                        .frame(height: 10)
                    }
                }
                .onPreferenceChange(ScrollOffsetKey.self) { offset in
                    let screenHeight = UIScreen.main.bounds.height
                    // 当触发器进入屏幕可视范围时,触发加载更多
                    if offset < screenHeight {
                        searchViewModel.loadMorePosts()
                    }
                }
                .onAppear {
                    if searchViewModel.posts.isEmpty {
                        searchViewModel.fetchRandomPosts()
                    }
                }
            }
        }
    }
}

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

// 保留原有的图片修饰器示例
struct ImagePostLargeModifier: ViewModifier {
    let post: PostModel
    func body(content: Content) -> some View {
        content
            .frame(height: 200)
            .cornerRadius(12)
    }
}

struct ImagePostMinModifier: ViewModifier {
    let post: PostModel
    func body(content: Content) -> some View {
        content
            .frame(height: 96)
            .cornerRadius(12)
    }
}

关键实现说明

  1. ViewModel逻辑优化

    • 新增allRandomPosts缓存所有随机后的帖子,避免重复请求Firestore
    • 通过initialLoadCount和loadMoreCount控制分页数量
    • isLoading状态防止重复触发加载请求
  2. UI滚动检测

    • 使用GeometryReader结合自定义PreferenceKey监听底部触发器的可见性,实现滚动到底部自动加载
    • 保留原有分组布局逻辑,分页加载的帖子会自动适配现有UI布局
  3. 性能优化

    • 仅渲染当前需要展示的帖子,减少一次性渲染的视图数量
    • 图片加载使用Kingfisher的缓存机制,进一步提升滚动流畅度

内容的提问来源于stack exchange,提问作者Luciano Sanchez Balbuena

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 05:20:56