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

iOS UITableView加载API数据重复请求、单元格展示异常问题

问题现象

调用接口拉取列表数据时出现两个异常:

  • 接口重复发起2次请求
  • 第一次请求回调后仅渲染1个空白单元格,第二次请求回调后才加载其余数据,单元格无法正常展示完整内容
    界面异常截图
根因分析
  • 数据源设计缺陷:将接口返回的同一份列表数据拆分为5个独立的可选数组存储,赋值过程存在时序差,极易出现数组count不一致、取值越界、首次回调时部分数组未完成赋值的问题,导致第一次刷新时cell拿不到数据渲染空白、行数计算错误。
  • 网络层逻辑缺陷:DataService中闭包声明错误(将无返回值的逃逸闭包声明为返回Void?),错误处理语法写错(print{error}为无效语法),且没有请求中状态标记,无法阻止重复请求触发,解码/请求失败时也没有正确回调,会出现静默失败、回调时序异常的问题。
  • 调用逻辑隐患:未对viewDidLoad触发的数据请求做防重保护,如果存在Presenter重复初始化、或其他生命周期方法误触发拉取逻辑的情况,就会重复发起请求。
修复方案

1. 重构数据源存储逻辑

删除ViewController中拆分的多个独立存储数组,改用统一的模型数组存储列表数据,从根源避免数组不一致问题:

// 移除以下零散属性
// var numberOfRows: Int?
// var startCityNameArray: [String]?
// var endCityNameArray: [String]?
// var startDateArray: [String]?
// var endDateArray: [String]?
// var price: [Int]?

// 替换为统一模型数组,RouteItem替换为你APIResponse中data数组对应的具体模型类型
var dataList: [RouteItem] = []

2. 修复网络层实现

修正闭包声明、错误语法,添加请求中标记防止重复请求,使用Result类型统一返回成功/失败状态:

class DataService {
    // 新增请求状态标记,防止重复发起请求
    private var isRequesting = false
    
    func getRouts(completion: @escaping (Result<APIResponse, Error>) -> Void) {
        // 若当前已有请求在执行,直接返回不重复发起
        guard !isRequesting else { return }
        isRequesting = true
        
        let urlString = "https://travel.wildberries.ru/statistics/v1/cheap"
        guard let url = URL(string: urlString) else {
            isRequesting = false
            completion(.failure(NSError(domain: "DataService", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的请求地址"])))
            return
        }
        
        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            // 无论请求成功失败,结束后都重置请求状态
            defer { self?.isRequesting = false }
            
            if let error = error {
                completion(.failure(error))
                return
            }
            guard let data = data else {
                completion(.failure(NSError(domain: "DataService", code: -2, userInfo: [NSLocalizedDescriptionKey: "无返回数据"])))
                return
            }
            do {
                let jsonResults = try JSONDecoder().decode(APIResponse.self, from: data)
                completion(.success(jsonResults))
            } catch {
                // 修复原错误的打印语法
                print("解码失败: \(error)")
                completion(.failure(error))
            }
        }
        task.resume()
    }
}

3. 调整Presenter调用逻辑

适配网络层新的Result回调,保证UI操作在主线程执行:

final class ListTicketsModulPresenter: PresenterProtocol {
    let dataService = DataService()
    weak var listTicketsViewController: ListTicketsViewController?
    
    func viewDidLoad() {
        dataService.getRouts { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let response):
                    self?.listTicketsViewController?.configure(with: response)
                case .failure(let error):
                    print("数据拉取失败: \(error.localizedDescription)")
                    // 可按需添加加载失败提示、重试逻辑
                }
            }
        }
    }
}

4. 修正ViewController的数据绑定和列表实现

修改configure方法直接给统一模型数组赋值,列表数据源方法直接从模型数组取值:

func configure(with model: APIResponse) {
    self.dataList = model.data
    self.tableView.reloadData()
}

// MARK: - UITableViewDataSource
extension ListTicketsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 直接返回模型数组count,不需要单独存储行数
        return dataList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) as! ListTicketsModulCell
        let item = dataList[indexPath.row]
        // 直接从当前行模型取数据赋值,避免多数组取值越界
        cell.configure(
            startCity: item.startCity,
            endCity: item.endCity,
            startDate: item.startDate,
            endDate: item.endDate,
            price: item.price
        )
        return cell
    }
}

5. 排查调用链路

检查ViewController和Presenter的初始化绑定逻辑,确保Presenter只被初始化一次,不要在viewWillAppear、viewDidAppear等会多次触发的生命周期方法中调用presenter.viewDidLoad()或拉取数据的方法,避免额外触发请求。

内容的提问来源于stack exchange,提问作者Mikl.hart

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.03 10:24:12