SwiftUI+Firebase项目分页卡顿在ProgressView问题排查求助
SwiftUI + Firebase 分页加载卡住,一直显示ProgressView
我在SwiftUI结合Firebase的项目里实现分页功能,页面一直卡在加载的ProgressView,找不到问题根源。期望用户上滑时每次加载5条数据,逻辑思路是对的,但始终解决不了问题。以下是我的ViewModel和View代码:
ViewModel 代码
import Foundation import SwiftUI import FirebaseFirestoreSwift import Firebase class SportNewsViewModel: ObservableObject { @Published var sport = [SportNews]() private let pageSize = 5 // 可调整每页加载数量 private var lastDocument: DocumentSnapshot? private var isFetching = false func fetchSportNews() { fetchSportNewsBatch() } func fetchNextPageOfSportNews(completion: @escaping (Bool) -> Void) { guard !isFetching else { completion(false) return } fetchSportNewsBatch(completion: completion) } private func fetchSportNewsBatch(completion: ((Bool) -> Void)? = nil) { isFetching = true var query = Firestore.firestore().collection("sportNews").order(by: "uid", descending: true) if let lastDocument = lastDocument { query = query.start(afterDocument: lastDocument) } query.limit(to: pageSize).getDocuments { [weak self] snapshot, error in guard let self = self else { return } if let error = error { print("获取体育新闻出错: \(error.localizedDescription)") self.isFetching = false completion?(false) return } guard let snapshot = snapshot else { print("Snapshot 为空") self.isFetching = false completion?(false) return } DispatchQueue.main.async { let newSport = snapshot.documents.compactMap { document -> SportNews? in let sportTime = document["sportNewsTime"] as? Timestamp ?? Timestamp(date: Date()) let date = sportTime.dateValue() return SportNews( id: document.documentID, uid: document["uid"] as? String ?? "", sportNewsImage: document["sportNewsImage"] as? String ?? "", sportNewsTitle: document["sportNewsTitle"] as? String ?? "", sportNewsPreview: document["sportNewsPreview"] as? String ?? "", sportNewsTopBar: document["sportNewsTopBar"] as? String ?? "", sportNewsType: document["sportNewsType"] as? String ?? "", sportNewsAuthor: document["sportNewsAuthor"] as? String ?? "", sportNewsTime: sportTime, sportNewsArticle: document["sportNewsArticle"] as? String ?? "", thumbsUp: document["thumbsUp"] as? Int ?? 0, unthumbsUp: document["unthumbsUp"] as? Int ?? 0, thumbsDown: document["thumbsDown"] as? Int ?? 0, unthumbsDown: document["unthumbsDown"] as? Int ?? 0 ) } self.sport.append(contentsOf: newSport) self.isFetching = false self.lastDocument = snapshot.documents.last completion?(true) } } } }
View 代码
import SwiftUI struct SportNewsView: View { @ObservedObject var sportViewModel = SportNewsViewModel() @State private var isEndReached = false var body: some View { ScrollView { VStack(spacing: 5) { ForEach(sportViewModel.sport) { sport in NavigationLink(destination: SportNewsDetailView(sport: sport)) { SportNewsRowView(sport: sport) } } if isEndReached { Text("已加载全部内容") .foregroundColor(.gray) .padding() } else { ProgressView() .onAppear { fetchNextPageIfNeeded() } } } .frame(maxWidth: .infinity) .padding(.bottom, 50) } .onAppear { sportViewModel.fetchSportNews() } } init() { sportViewModel.fetchSportNews() } private func fetchNextPageIfNeeded() { guard !isEndReached else { return } let lastVisibleIndex = sportViewModel.sport.count - 1 if lastVisibleIndex >= 0 && lastVisibleIndex == sportViewModel.sport.count - 1 { sportViewModel.fetchNextPageOfSportNews { success in isEndReached = !success } } } }
问题根源分析
- 重复初始化请求:
SportNewsView的init和onAppear都调用了fetchSportNews(),导致首次加载发起两次请求,干扰分页状态。 - 分页触发逻辑无效:
ProgressView的onAppear仅在视图首次出现时触发一次,不会随滚动重复触发;且fetchNextPageIfNeeded里的条件永远为真,无法正确触发后续分页。 - 边界状态错误:当最后一页数据不足
pageSize时,会错误标记isEndReached = true;若首次加载无数据,ProgressView会一直显示。
修复方案
1. 移除重复的初始化请求
删除init方法里的请求调用,仅保留onAppear的触发:
// 移除以下代码块 // init() { // sportViewModel.fetchSportNews() // }
2. 改用滚动位置监听触发分页(iOS 17+)
使用onScrollChange监听滚动位置,接近底部时触发加载:
struct SportNewsView: View { @ObservedObject var sportViewModel = SportNewsViewModel() @State private var isEndReached = false @State private var scrollPosition: CGFloat = 0 var body: some View { ScrollView { VStack(spacing: 5) { ForEach(sportViewModel.sport) { sport in NavigationLink(destination: SportNewsDetailView(sport: sport)) { SportNewsRowView(sport: sport) } } if isEndReached { Text("已加载全部内容") .foregroundColor(.gray) .padding() } else { ProgressView() .padding() } } .frame(maxWidth: .infinity) .padding(.bottom, 50) } .onAppear { sportViewModel.fetchSportNews() } .onScrollChange(for: CGFloat.self) { newPosition in scrollPosition = newPosition // 距离底部200pt时触发加载 if scrollPosition > UIScreen.main.bounds.height - 200 { fetchNextPageIfNeeded() } } } private func fetchNextPageIfNeeded() { guard !isEndReached, !sportViewModel.isFetching else { return } sportViewModel.fetchNextPageOfSportNews { success in // 仅当返回数据不足一页时,标记为已到末尾 let fetchedCount = sportViewModel.sport.count - (sportViewModel.sport.count - sportViewModel.pageSize) isEndReached = success && fetchedCount < sportViewModel.pageSize } } }
3. 兼容iOS 16及以下方案(ScrollViewReader)
struct SportNewsView: View { @ObservedObject var sportViewModel = SportNewsViewModel() @State private var isEndReached = false var body: some View { ScrollViewReader { proxy in ScrollView { VStack(spacing: 5) { ForEach(sportViewModel.sport) { sport in NavigationLink(destination: SportNewsDetailView(sport: sport)) { SportNewsRowView(sport: sport) } .id(sport.id) } if isEndReached { Text("已加载全部内容") .foregroundColor(.gray) .padding() .id("endMarker") } else { ProgressView() .padding() .id("loadingMarker") } } .frame(maxWidth: .infinity) .padding(.bottom, 50) .onAppear { proxy.scrollTo("loadingMarker", anchor: .bottom) } .onChange(of: sportViewModel.sport) { _ in if !isEndReached { proxy.scrollTo("loadingMarker", anchor: .bottom) } } } .onAppear { sportViewModel.fetchSportNews() } } } private func fetchNextPageIfNeeded() { guard !isEndReached, !sportViewModel.isFetching else { return } sportViewModel.fetchNextPageOfSportNews { success in let fetchedCount = sportViewModel.sport.count - (sportViewModel.sport.count - sportViewModel.pageSize) isEndReached = success && fetchedCount < sportViewModel.pageSize } } }
内容的提问来源于stack exchange,提问作者EMPIRE
相关产品推荐
相关产品推荐

