包含AsyncImages的SwiftUI列表仅在滚动出视图时更新



我有一个Pokemon对象列表,显示在PokemonCells中。当这些PokemonCells被创建时,它检查它是否有一个PokemonDetail对象。PokemonDetail是一个@State变量,如果它是nil,它会触发一个ProgressView和一个获取pokemondetails的调用。它将获取的结果存储到PokemonDetail @State变量中。

一旦这个@State变量被分配,它就会触发调用来检索一个AsyncImage,在它工作的同时显示一个占位符。

struct PokemonCell: View {
let pokemon: Pokemon
//This is nil when the view is first instantiated.
@State var pokemonDetail: Result<PokemonDetail, PokemonAPI.RequestError>?
var body: some View {
HStack {
//If pokemonDetail not nill, enter switch
if let pokemonDetail = pokemonDetail {
//Switch on the pokemondetail/pokemonapi.requesterror object.
switch pokemonDetail {
//If result from API is .success, get asyncimage
case .success(let newPoke):
//Create the asyncImage using the URL contained in the pokemonDetail object.
AsyncImage(
url: URL(string: newPoke.spriteImageURL),
transaction: .init(animation: .spring(response: 1.6))
) { phase in
switch phase {
case .empty:
ProgressView()
.progressViewStyle(.circular)
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Text("Failed fetching image try again.")
.foregroundColor(.red)
@unknown default:
Text("Unknown error. Please try again.")
.foregroundColor(.red)
}
}
.frame(width: 50, height: 50)
case .failure(_):
Text("ImgError")
}
}
//If it IS null, trigger the progressview and fetch the details.
else {
ProgressView().onAppear(perform: fetchPokemonDetails)
}
VStack(alignment: .leading) {
Text(pokemon.name).font(.title)
}
}
}
//Fetch details from API, store the Result<PokemonDetail, PokemonAPI.RequestError> object in the pokemonDetail @State var
func fetchPokemonDetails() {
self.pokemonDetail = nil
PokemonAPI.shared.getPokemonDetails(name: pokemon.name) { result in self.pokemonDetail = result
}
}
}

奇怪的是,这对于第一个图像工作良好,然后对于其余的图像,它继续显示占位符图像。只有当图像滚动出屏幕时,它才会触发FetchPokemonDetails,并正确更新图像。

在创建cell的Contentview中,我使用相当类似的代码来检索pokemon名称列表,并且它可以正常工作。

var body: some View {
if let pokemonsState = pokemonsState {
switch pokemonsState {
case .success(let pokemons):
NavigationView {
List(pokemonsList) { pokemon in
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
PokemonCell(pokemon: pokemon).onAppear {
if pokemon.name == pokemons.last?.name {
offset += 10
fetchPokemon()
}
}
}
}.navigationTitle("Pokemons")
}
default: Text("pholder")
}
} else {
ProgressView().onAppear(perform: fetchPokemon)
}
}

API代码

func getPokemonDetails(
name: String, completion: @escaping (Result<PokemonDetail, RequestError>) -> Void
) {
print("api called")
let url = URL(string: "https://pokeapi.co/api/v2/pokemon/(name)")!
let urlRequest = URLRequest(url: url)
print("url completed: (urlRequest)")
cancellable = URLSession.shared.dataTaskPublisher(for: urlRequest)
.map({ $0.data })
.decode(type: PokemonDetail.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { result in
switch result {
case .finished:
print("pokemondetails fetched: (result) <-- this a result in finished case")
break
case .failure(let error):
switch error {
case let urlError as URLError:
completion(.failure(.urlError(urlError)))
print("pokemondetails fetched: (result) <-- this a result in error case")
print("apierror")
case let decodingError as DecodingError:
completion(.failure(.decodingError(decodingError)))
print("apierror")
print("pokemondetails fetched: (result) <-- this a result in error case")
default:
completion(.failure(.genericError(error)))
print("pokemondetails fetched: (result) <-- this a result in error case")

}
}

}

//) { (response) in print("apiclass result: (completion(.success(response)))")}
) { (response) in print("apiclass result: (completion(.success(response)))"); completion(.success(response))   }

}

我整个晚上都在摆弄这个。我试着用Scrollview + LazyVstack替换列表。实例化AsyncImage的不同方法。

我认为这可能是因为API一次调用所有的图像,然后被图像服务器拒绝。但我没有看到失败,即使将pokemon列表中的数量减少到2或3个也会产生相同的结果。图像请求本身也没有问题,因为它们在滚动出屏幕后确实工作得很好。

在FetchPokemonDetails函数和API中放置一个print语句,表明API确实为每个pokemon被调用。getPokemonDetails被调用,URL被成功创建,但随后它就停止了。它不会碰到任何。finished或。error情况,返回nil结果,也不会碰到打印结果的print语句。

func fetchPokemonDetails() {
self.pokemonDetail = nil
print("fetching for (pokemon.name)")
PokemonAPI.shared.getPokemonDetails(name: pokemon.name) { result in pokemonDetail = result
}
print("fetched, now pokemonDetail = (pokemonDetail)")
}

对所有口袋妖怪(即使是第一个成功的口袋妖怪)的输出结果如下:

fetching for ivysaur
api called
url completed: https://pokeapi.co/api/v2/pokemon/ivysaur
fetched, now pokemonDetail = nil
fetching for bulbasaur
api called
url completed: https://pokeapi.co/api/v2/pokemon/bulbasaur
fetched, now pokemonDetail = nil
apiclass result: ()
pokemondetails fetched: finished <-- this a result in finished case

看起来getPokemonDetails API只是被下一个调用打断,完全忘记了完成它正在做的事情。我该如何解决这个问题?

看起来getPokemonDetails API只是被下一个调用中断,完全忘记完成它正在做的事情

这种行为的一个解释是你存储你的cancellable的方式:

cancellable = URLSession.shared.dataTaskPublisher.....

这里没有太多关于cancellable变量的信息,但是观察到的行为表明,每次创建新请求时都要覆盖该变量。如果未存储可取消对象,您的请求将超出作用域。

要解决这个问题,您可以尝试:

var cancellables = Set<AnyCancellable>()
然后在你的函数中:
cancelables.append(URLSession.shared.dataTaskPublisher.....

但这里可能有更多的原因。很难说。

最新更新