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

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
            }
        }
    }
}

问题根源分析

  1. 重复初始化请求:SportNewsView的init和onAppear都调用了fetchSportNews(),导致首次加载发起两次请求,干扰分页状态。
  2. 分页触发逻辑无效:ProgressView的onAppear仅在视图首次出现时触发一次,不会随滚动重复触发;且fetchNextPageIfNeeded里的条件永远为真,无法正确触发后续分页。
  3. 边界状态错误:当最后一页数据不足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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 11:11:00