使用URLSession.dataTaskPublisher更新UITableView二次调用失效问题
问题分析与修复方案
核心问题排查
URL拼接错误
ShowService中的URL写法完全错误,%5C(pageNumber)是无效的插值逻辑,实际请求会把(pageNumber)当成字面量拼入URL,导致API请求地址无效,直接触发错误分支返回Empty,所以sink收不到任何数据。数据类型不匹配
TVMaze的/shows?page=xx接口返回的是剧集数组,但你的getShows方法解码类型是Show.self,如果Show不是数组类型,会直接解码失败,进入catch返回空序列,无法触发receiveValue。订阅管理与状态逻辑缺陷
- 用单个
AnyCancellable变量存储订阅,每次调用fetchShows会覆盖旧订阅,虽然不是本次无响应的直接原因,但容易引发意外问题; isLoadingList的重置逻辑只在map中处理,请求失败时不会执行,导致isLoadingList一直为true,无法触发后续加载;- 加载更多时直接覆盖
filteredShows,而非追加数据,不符合分页加载的需求。
- 用单个
修复后的完整代码
1. 修正ShowService
// 假设ShowModel是单个剧集的模型类/结构体 class ShowService: RequestProtocol { func getShows(pageNumber: String) -> AnyPublisher<[ShowModel], Error> { // 正确拼接URL guard let url = URL(string: "https://api.tvmaze.com/shows?page=\(pageNumber)") else { fatalError("无效的请求URL") } return URLSession.shared.dataTaskPublisher(for: url) .map(\.data) .decode(type: [ShowModel].self, decoder: JSONDecoder()) // 错误时打印日志方便调试,不要直接吞掉错误 .catch { error in print("剧集请求失败: \(error.localizedDescription)") return Empty<[ShowModel], Error>() } .receive(on: RunLoop.main) // 移到最后,只在主线程处理结果 .eraseToAnyPublisher() } }
2. 修正ViewController逻辑
private var filteredShows: [ShowModel] = [] // 改为数组,默认空集合 private var currentPage: Int = 1 private var isLoadingList: Bool = false private var cancellables = Set<AnyCancellable>() // 用Set管理多个订阅 override func viewDidLoad() { super.viewDidLoad() fetchShows(currentPage) } private func fetchShows(_ pageNumber: Int) { ShowService().getShows(pageNumber: String(pageNumber)) .sink(receiveCompletion: { [weak self] completion in // 无论请求成功/失败,都重置加载状态 self?.isLoadingList = false if case .failure(let error) = completion { print("加载分页数据失败: \(error.localizedDescription)") } }, receiveValue: { [weak self] newShows in // 追加新数据,而非覆盖旧数据 self?.filteredShows.append(contentsOf: newShows) self?.showsTableView.reloadData() }) .store(in: &cancellables) // 将订阅存入Set管理 } // 补充实现加载更多的方法(原代码缺失) private func loadMoreItemsForList() { currentPage += 1 fetchShows(currentPage) } extension ViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { filteredShows.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // 推荐用dequeueReusableCell复用单元格,而非直接创建 let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = filteredShows[indexPath.row].name return cell } func scrollViewDidScroll(_ scrollView: UIScrollView) { let offsetY = scrollView.contentOffset.y let contentHeight = scrollView.contentSize.height let frameHeight = scrollView.frame.size.height // 调整触发阈值,避免滚动时频繁触发加载 if offsetY + frameHeight > contentHeight - 100 && !isLoadingList { isLoadingList = true loadMoreItemsForList() } } }
关键修复点说明
- URL修正:用Swift字符串插值
\(pageNumber)正确拼接页码,确保API请求地址有效; - 类型匹配:将解码类型改为
[ShowModel].self,匹配API返回的数组格式; - 订阅管理:用
Set<AnyCancellable>存储订阅,避免覆盖旧订阅,同时自动管理内存; - 数据追加:将
filteredShows改为数组,加载更多时用append(contentsOf:)追加新数据; - 状态重置:在
sink的receiveCompletion中统一重置isLoadingList,确保请求失败后仍能恢复加载能力; - 错误调试:在错误分支打印日志,方便排查请求失败原因。
内容的提问来源于stack exchange,提问作者zacdejesus
相关产品推荐
相关产品推荐

