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

SwiftUI Combine实现API分页加载:自动请求下一页至无更多数据

问题

现有SwiftUI + Combine实现的单页API数据请求代码,服务器返回数据包含page、total_pages、hasMore等字段。需求是:当返回数据的hasMore为true时,自动将page_num自增1并发起下一页请求,直到hasMore为false为止。

现有请求代码:

var page_num = 1

guard let url = URL(string: "http://127.0.0.1/api/v4/fetch_records/?page=\(page_num)")  else { return }
        
NetworkManager.GET(url: url)
    .decode(type: GlobalData.self, decoder: JSONDecoder())
    .subscribe(on: DispatchQueue.global(qos: .background))
    .receive(on: DispatchQueue.main)
    .sink{ (completion) in

    } receiveValue: { [weak self] (returnedData) in
        if returnedData.status == "success" {
            for record in returnedData.data {
                print(record)
            }
        }
    }

服务器返回数据结构:

{
    "page": 1,
    "total_pages": 10,
    "hasMore": true,
    "status": "success",
    "data": [{
        "id": 23409,
        "record_id": "P06TYUQZ",
        "money": "1.00",
        "date": "2022-10-24"
    }]
}

解决方案

要实现自动分页请求,核心是用Combine的订阅逻辑结合递归调用,同时做好分页状态管理和内存泄漏防护。

完整实现代码

import Combine

class RecordFetcher: ObservableObject {
    private var cancellables = Set<AnyCancellable>()
    private var currentPage = 1
    // 存储所有拉取到的记录,供UI展示
    @Published var allRecords: [Record] = []
    
    // 触发全量数据拉取(重置页码)
    func startFetching() {
        currentPage = 1
        allRecords.removeAll()
        fetchPage(page: currentPage)
    }
    
    // 单页请求核心方法
    private func fetchPage(page: Int) {
        guard let url = URL(string: "http://127.0.0.1/api/v4/fetch_records/?page=\(page)") else { return }
        
        NetworkManager.GET(url: url)
            .decode(type: GlobalData.self, decoder: JSONDecoder())
            .subscribe(on: DispatchQueue.global(qos: .background))
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    print("分页请求失败:\(error.localizedDescription)")
                }
            }, receiveValue: { [weak self] returnedData in
                guard let self = self else { return }
                
                if returnedData.status == "success" {
                    // 追加当前页数据到总集合
                    self.allRecords.append(contentsOf: returnedData.data)
                    
                    // 判断是否需要继续拉取下一页
                    if returnedData.hasMore {
                        self.currentPage += 1
                        self.fetchPage(page: self.currentPage)
                    } else {
                        print("所有数据加载完成,共\(self.allRecords.count)条")
                    }
                }
            })
            .store(in: &self.cancellables)
    }
}

// 对应服务器返回的模型(需确保与JSON结构匹配)
struct GlobalData: Decodable {
    let page: Int
    let total_pages: Int
    let hasMore: Bool
    let status: String
    let data: [Record]
}

struct Record: Decodable {
    let id: Int
    let record_id: String
    let money: String
    let date: String
}

关键细节说明

  • 状态管理:用currentPage维护当前请求的页码,allRecords统一存储所有分页数据,方便后续UI绑定展示;
  • 递归续页:在receiveValue回调中判断hasMore,若为true则自增页码并再次调用fetchPage发起下一页请求;
  • 内存安全:使用[weak self]避免循环引用,通过cancellables集合管理所有订阅,对象销毁时自动取消未完成的请求;
  • 错误处理:在receiveCompletion中捕获请求失败的情况,避免异常导致的无限递归。

SwiftUI视图中使用示例

struct RecordsView: View {
    @StateObject private var fetcher = RecordFetcher()
    
    var body: some View {
        List(fetcher.allRecords, id: \.id) { record in
            VStack(alignment: .leading) {
                Text(record.record_id)
                    .font(.headline)
                Text("金额:\(record.money) | 日期:\(record.date)")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
        .onAppear {
            fetcher.startFetching()
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 10:30:53