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

使用URLSession.dataTaskPublisher更新UITableView二次调用失效问题

问题分析与修复方案

核心问题排查

  1. URL拼接错误
    ShowService中的URL写法完全错误,%5C(pageNumber)是无效的插值逻辑,实际请求会把(pageNumber)当成字面量拼入URL,导致API请求地址无效,直接触发错误分支返回Empty,所以sink收不到任何数据。

  2. 数据类型不匹配
    TVMaze的/shows?page=xx接口返回的是剧集数组,但你的getShows方法解码类型是Show.self,如果Show不是数组类型,会直接解码失败,进入catch返回空序列,无法触发receiveValue。

  3. 订阅管理与状态逻辑缺陷

    • 用单个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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 05:30:53