Почему требуются 2 сетевых вызова с использованием одной и той же строки поиска, прежде чем UICollectionView решит, что есть данные, которые необходимо обновить?

#ios #swift

#iOS #быстрый

Вопрос:

У меня странная проблема в UIViewController, который пользователь использует для поиска GIF.

По сути, есть 2 проблемы:

  1. Пользователь должен дважды ввести один и тот же поисковый запрос, прежде чем UICollectionView запустит метод источника данных cellForRowAt после выполнения вызова reloadData() .
  2. После того, как вы введете поисковый запрос в первый раз, heightChanged() он вызывается, но self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height возвращается как 0, хотя я подтвердил, что данные поступают обратно с сервера. При втором вводе поискового запроса высота будет ненулевым значением, а в представлении коллекции будут показаны ячейки.

Вот пример того, как я должен получить данные, чтобы они отображались:

  1. Запустите приложение, перейдите к этому UIViewController
  2. Введите поисковый запрос (т. е. «бейсбол»)
  3. Ничего не отображается (даже несмотря reloadData() на то, что был вызван, и новые данные находятся в модели представления).
  4. Удалите символ из поискового запроса (т. е. «бейсбол»)
  5. Введите отсутствующий символ (т. е. «бейсбол»)
  6. UICollectionView обновляется с помощью вызова reloadData() , а затем вызывает cellForRowAt: .

Вот весь контроллер представления:

 import UIKit  protocol POGIFSelectViewControllerDelegate: AnyObject {  func collectionViewHeightDidChange(_ height: CGFloat)  func didSelectGIF(_ selectedGIFURL: POGIFURLs) }  class POGIFSelectViewController: UIViewController {    //MARK: - Constants  private enum Constants {  static let POGIFCollectionViewCellIdentifier: String = "POGIFCollectionViewCell"  static let verticalPadding: CGFloat = 16  static let searchBarHeight: CGFloat = 40  static let searchLabelHeight: CGFloat = 24  static let activityIndicatorTopSpacing: CGFloat = 10  static let gifLoadDuration: Double = 0.2  static let gifStandardFPS: Double = 1/30  static let gifMaxDuration: Double = 5.0  }    //MARK: - Localized Strings  let localizedSearchGIFs = PALocalizedStringFromTable("RECOGNITION_IMAGE_SELECTION_GIF_BODY_TITLE", table: "Recognition-Osiris", comment: "Search GIFs") as String    //MARK: - Properties  var viewModel: POGIFSearchViewModel?  var activityIndicator = MDCActivityIndicator()  var gifLayout = PAGiphyCellLayout()  var selectedGIF: POGIFURLs?    //MARK: - IBOutlet  @IBOutlet weak var GIFCollectionView: UICollectionView!  @IBOutlet weak var searchGIFLabel: UILabel! {  didSet {  self.searchGIFLabel.text = self.localizedSearchGIFs  }  }    @IBOutlet weak var searchField: POSearchField! {  didSet {  self.searchField.delegate = self  }  }    @IBOutlet weak var activityIndicatorContainer: UIView!    //MARK: - Delegate  weak var delegate: POGIFSelectViewControllerDelegate?    //MARK: - View Lifecycle Methods  override func viewDidLoad() {  super.viewDidLoad()    self.setupActivityIndicator(activityIndicator: self.activityIndicator, activityIndicatorContainer: self.activityIndicatorContainer)  self.viewModel = POGIFSearchViewModel(data: PAData.sharedInstance())  if let viewModel = self.viewModel {  viewModel.viewDelegate = self  viewModel.viewDidBeginLoading()  }    self.gifLayout.delegate = self  self.gifLayout.isAXPGifLayout = true;  self.GIFCollectionView.collectionViewLayout = self.gifLayout  self.GIFCollectionView.backgroundColor = .orange  }    override func viewDidAppear(_ animated: Bool) {  super.viewDidAppear(animated)  // This Patch is to fix a bug where GIF contentSize was not calculated correctly on first load.  DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()   .seconds(1)) {  self.viewModel?.viewDidBeginLoading()  }  }    override func viewDidLayoutSubviews() {  super.viewDidLayoutSubviews()  self.heightChanged()  }    //MARK: - Helper Methods  func heightChanged() {  guard let delegate = self.delegate else { return }    let height = self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height   Constants.verticalPadding * 3   Constants.searchLabelHeight   Constants.searchBarHeight   activityIndicatorContainer.frame.size.height   Constants.activityIndicatorTopSpacing  print("**** Items in Collection View -gt; self.viewModel?.gifModel.items.count: (self.viewModel?.gifModel.items.count)")  print("**** self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height: (self.GIFCollectionView.collectionViewLayout.collectionViewContentSize.height); height: (height)")  delegate.collectionViewHeightDidChange(height)  }    func reloadCollectionView() {  self.GIFCollectionView.collectionViewLayout.invalidateLayout()  self.GIFCollectionView.reloadData()  self.GIFCollectionView.layoutIfNeeded()  self.heightChanged()  }    func imageAtIndexPath(_ indexPath: IndexPath) -gt; UIImage? {  guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else { return nil }    var loadedImage: UIImage? = nil  let imageManager = SDWebImageManager.shared()  imageManager.loadImage(with: previewURL, options: .lowPriority, progress: nil) { (image: UIImage?, data: Data?, error: Error?, cacheType: SDImageCacheType, finished: Bool, imageURL: URL?) in  loadedImage = image  }  return loadedImage  }    func scrollViewDidScrollToBottom() {  guard let viewModel = self.viewModel else { return }    if viewModel.viewDidSearchMoreGIFs() {  self.activityIndicator.startAnimating()  } else {  self.activityIndicator.stopAnimating()  }  } }  extension POGIFSelectViewController: POSearchFieldDelegate {  func searchFieldTextChanged(text: String?) {  guard let viewModel = self.viewModel else { return }  viewModel.viewDidSearchGIFs(withSearchTerm: text)  } }  extension POGIFSelectViewController: UICollectionViewDataSource {  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -gt; UICollectionViewCell {  print("**** CELL FOR ROW AT -gt; self.viewModel?.gifModel.items.count: (self.viewModel?.gifModel.items.count)")  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.POGIFCollectionViewCellIdentifier, for: indexPath) as! POGIFCollectionViewCell  guard let previewURL = self.viewModel?.gifModel.items[indexPath.row].previewGIFURL else {  return cell  }    var cellState: POGIFCollectionViewCell.CellState = .dimmedState  if self.selectedGIF == nil {  cellState = .defaultState  } else if (self.selectedGIF?.previewGIFURL?.absoluteString == previewURL.absoluteString) {  cellState = .selectedState  }    cell.setupUI(withState: cellState, URL: previewURL) { [weak self] () in  UIView.animate(withDuration: Constants.gifLoadDuration) {  guard let weakSelf = self else { return }  weakSelf.GIFCollectionView.collectionViewLayout.invalidateLayout()  }  }    if cell.GIFPreviewImageView.animationDuration gt; Constants.gifMaxDuration {  cell.GIFPreviewImageView.animationDuration = Constants.gifMaxDuration  }    cell.backgroundColor = .green  return cell  }    func numberOfSections(in collectionView: UICollectionView) -gt; Int {  return 1  }    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -gt; Int {  guard let viewModel = self.viewModel else { return 0 }  return viewModel.gifModel.items.count  } }  extension POGIFSelectViewController: UICollectionViewDelegate {  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {  guard let selectedGIF = self.viewModel?.gifModel.items[indexPath.row],  let delegate = self.delegate else {  return  }  self.selectedGIF = selectedGIF  delegate.didSelectGIF(selectedGIF)  self.reloadCollectionView()  } }  extension POGIFSelectViewController: POGIFSearchViewModelToViewProtocol {  func didFetchGIFsWithSuccess() {  self.activityIndicator.stopAnimating()  print("**** didFetchGIFsWithSuccess() -gt; about to reload collection view")  self.reloadCollectionView()  }    func didFetchGIFsWithError(_ error: Error!, request: PARequest!) {  self.activityIndicator.stopAnimating()  } }  extension POGIFSelectViewController: PAGiphyLayoutCellDelegate {  func heightForCell(givenWidth cellWidth: CGFloat, at indexPath: IndexPath!) -gt; CGFloat {  guard let image = self.imageAtIndexPath(indexPath) else {  return 0  }  if (image.size.height lt; 1 || image.size.width lt; 1 || self.activityIndicator.isAnimating) {  return cellWidth  }    let scaleFactor = image.size.height / image.size.width  let imageViewToHighlightedViewSpacing: CGFloat = 4 // this number comes from 2 * highlightedViewBorderWidth from POGIFCollectionViewCell  return cellWidth * scaleFactor   imageViewToHighlightedViewSpacing  }    func heightForHeaderView() -gt; CGFloat {  return 0  } }  

Вы увидите, что heightChanged() метод вызывает метод делегата. Этот метод находится в другом UIViewController:

 func collectionViewHeightDidChange(_ height: CGFloat) {  self.collectionViewHeightConstraint.constant = height  }  

Итак, я не могу понять, почему мне нужно либо удалить символ из поискового запроса, либо повторно добавить его, чтобы данные обновились, даже если самый первый вызов заполнил модель представления новыми данными.

Это странно. Пожалуйста, помогите.

Комментарии:

1. Первый комментарий заключается в том, что в этом коде многое происходит, из-за чего трудно определить, в чем проблема. Разобрать кое-что из этого может помочь. Вызов функции reloadData() не означает, что данные будут загружаться. Хотя в коде это не очевидно, я бы ожидал, что загрузка данных будет асинхронной, и, вероятно, поэтому ее нет в первый раз. Загрузка данных должна управляться обработчиком завершения при сетевом вызове, а не VC.

2. Спасибо @flanker, именно это делает метод делегата didFetchGIFsWithSuccess (). Он вызывается внутри другого объекта, когда сетевой запрос успешно извлек GIF-файлы. Итак, это уже делается.

3. Непонятно: где именно обновляется viewModel и когда вызывается reloadData() . Действительно ли они последовательны в одной и той же теме? Поскольку это связано с пользовательским интерфейсом, reloadData() его необходимо вызывать в основном потоке, но также необходимо обновить модель (обработку следующей модели можно выполнить в фоновом потоке, но не устанавливать ее, так как методы делегирования пользовательского интерфейса будут полагаться на нее.