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
相关产品推荐
相关产品推荐

