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

SwiftUI List中AsyncImage仅滚出视图才更新的问题修复

问题分析与解决方案

核心问题根源

  1. 单元格状态管理错误:每个PokemonCell用@State存储PokemonDetail,但列表单元格是复用机制,当单元格处于可视区域时,@State的生命周期和视图复用逻辑冲突,导致后续请求被中断或无法触发。
  2. 异步请求时机不当:在body中直接调用请求方法,SwiftUI的视图刷新机制会导致请求重复触发或被意外取消,尤其是非第一个单元格的请求容易被丢弃。
  3. API请求实现隐患:若使用completion handler方式处理请求,可能存在循环引用、任务未正确保留的问题,导致后续请求无法进入回调分支。

具体解决方案

1. 状态提升到父级统一管理

把PokemonDetail的状态从单元格内部移到父视图的ViewModel中,用字典按Pokemon ID存储详情,避免单元格复用导致的状态丢失。

class PokemonViewModel: ObservableObject {
    @Published var pokemonList: [Pokemon] = []
    @Published var pokemonDetails: [String: PokemonDetail] = [:] // 用ID作为key存储详情

    // 获取Pokemon列表的原有逻辑
    func fetchPokemonList() {
        // ... 你的列表请求代码
    }

    // 异步获取单条详情(用async/await替代completion handler更稳定)
    func fetchPokemonDetail(for pokemonID: String) async throws {
        guard let url = URL(string: "https://pokeapi.co/api/v2/pokemon/\(pokemonID)") else {
            throw NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的详情URL"])
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        guard (response as? HTTPURLResponse)?.statusCode == 200 else {
            throw NSError(domain: "APIError", code: -2, userInfo: [NSLocalizedDescriptionKey: "请求失败"])
        }
        
        let detail = try JSONDecoder().decode(PokemonDetail.self, from: data)
        DispatchQueue.main.async {
            self.pokemonDetails[pokemonID] = detail
        }
    }
}

2. 用.task修饰符触发异步请求

在父视图的列表项上绑定.task,它会自动在单元格进入可视区域时执行请求,离开时取消任务,完美适配列表复用逻辑。

struct ContentView: View {
    @StateObject private var viewModel = PokemonViewModel()

    var body: some View {
        List(viewModel.pokemonList) { pokemon in
            PokemonCell(pokemon: pokemon, detail: viewModel.pokemonDetails[pokemon.id])
                .task {
                    // 仅当未获取过详情时发起请求
                    if viewModel.pokemonDetails[pokemon.id] == nil {
                        try? await viewModel.fetchPokemonDetail(for: pokemon.id)
                    }
                }
        }
        .onAppear {
            viewModel.fetchPokemonList()
        }
    }
}

3. 简化PokemonCell实现

单元格只负责展示数据,不再处理状态和请求逻辑:

struct PokemonCell: View {
    let pokemon: Pokemon
    let detail: PokemonDetail?

    var body: some View {
        HStack(spacing: 12) {
            if let detail = detail {
                AsyncImage(url: URL(string: detail.sprites.frontDefault)) { phase in
                    switch phase {
                    case .empty:
                        ProgressView().frame(width: 50, height: 50)
                    case .success(let image):
                        image.resizable()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                    case .failure:
                        Image(systemName: "questionmark.circle")
                            .frame(width: 50, height: 50)
                    @unknown default:
                        Image(systemName: "questionmark.circle")
                    }
                }
            } else {
                ProgressView().frame(width: 50, height: 50)
            }
            
            Text(pokemon.name)
                .font(.headline)
        }
        .padding(.vertical, 8)
    }
}

额外排查点

  • 检查后续请求的Pokemon ID是否正确传递,避免URL拼接错误;
  • 若仍使用completion handler方式处理API请求,确保没有捕获self导致循环引用,且任务被正确保留;
  • 避免在单元格的init或body中直接调用异步方法,这类场景下SwiftUI的视图刷新会导致请求异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 18:01:26