Список SwiftUI, содержащий AsyncImages, обновляется только при прокрутке вне поля зрения
У меня есть список объектов Pokemon, отображаемый в PokemonCells. Когда эти PokemonCell создаются, он проверяет, есть ли у них объект PokemonDetail. PokemonDetail — это переменная @State. Если он равен нулю, он вызывает ProgressView и вызов для получения сведений о покемонах. Он сохраняет результаты этой выборки в PokemonDetail @State var.
Как только эта переменная @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 и правильно обновляет изображение.
В Contentview, где создаются ячейки, я использую довольно похожий код для получения списка имен покемонов, и он работает без проблем.
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 вызывает все изображения одновременно, а затем отказывается от сервера изображений. Но провалов я не вижу, и даже уменьшение количества покемонов в списке до 2-х или 3-х приводит к тому же. В самих запросах изображений тоже нет ничего плохого, потому что они нормально работают после прокрутки за пределы экрана.
Размещение оператора печати в функции FetchPokemonDetails и в API показывает, что API действительно вызывается для каждого покемона.getPokemonDetails вызывается, URL-адрес создается успешно, но затем он просто останавливается. Он не попадает ни в один из случаев .finished или .error, возвращает нулевой результат и не обращается к оператору печати, который печатает результат в конце.
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
Похоже, API getPokemonDetails просто прерывается следующим вызовом и совершенно забывает закончить то, что делает. Как это исправить?