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) } }
关键实现说明
ViewModel逻辑优化
- 新增
allRandomPosts缓存所有随机后的帖子,避免重复请求Firestore - 通过
initialLoadCount和loadMoreCount控制分页数量 isLoading状态防止重复触发加载请求
- 新增
UI滚动检测
- 使用
GeometryReader结合自定义PreferenceKey监听底部触发器的可见性,实现滚动到底部自动加载 - 保留原有分组布局逻辑,分页加载的帖子会自动适配现有UI布局
- 使用
性能优化
- 仅渲染当前需要展示的帖子,减少一次性渲染的视图数量
- 图片加载使用Kingfisher的缓存机制,进一步提升滚动流畅度
内容的提问来源于stack exchange,提问作者Luciano Sanchez Balbuena
相关产品推荐
相关产品推荐

