UICollectionViewDiffableDataSource заменяет данные вместо обновления

Я пытаюсь понять UICollectionViewDiffableDataSource а также NSDiffableDataSourceSnapshot.

Ниже я создал очень грубую версию. По сути, при загрузке он должен загружать фотографии.

При нажатии кнопки на панели навигации происходит переход на следующую страницу. Однако это просто замена существующих данных, я ожидал, что они добавят значения в массив.

Как мне обновить свои данные, а не заменить их?

class ViewController: UIViewController {

  lazy var collectionView = UICollectionView(frame: view.frame, collectionViewLayout: UICollectionViewFlowLayout())

  enum Section {
    case main
  }

  typealias DataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>
  typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>

  private var dataSource: DataSource!
  private var snapshot = DataSourceSnapshot()

  override func viewDidLoad() {
    super.viewDidLoad()

    let updateButton = UIBarButtonItem(title: "Add", style: .plain, target: self, action: #selector(onTapLoad))
    navigationItem.rightBarButtonItem = updateButton

    view.addSubview(collectionView)
    collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
    collectionView.delegate = self
    collectionView.backgroundColor = .systemBackground
    collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "PhotoCell")

    dataSource = DataSource(collectionView: collectionView, cellProvider: { (cv, indexPath, object) -> PhotoCell? in

      if let object = object as? Photo {
        let cell = cv.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
        cell.backgroundColor = indexPath.item % 2 == 0 ? .systemTeal : .systemPink
        cell.label.text = object.title
        return cell
      }

      return nil
    })

    load()
  }

  @objc func onTapLoad() {
    load(page: 1)
  }

  func load(page: Int = 0) {
    PhotoLoader.shared.load(page: page) { result in
      if let photos = try? result.get() {
        self.apply(photos)
      }
    }
  }

  func apply(_ photos: [Photo]) {

    snapshot = DataSourceSnapshot()
    snapshot.appendSections([Section.main])
    snapshot.appendItems(photos)

    dataSource.apply(snapshot, animatingDifferences: false)
  }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return .init(width: collectionView.frame.width - 32, height: 100)
  }
}

struct Photo: Decodable, Hashable {
  let id = UUID()
  let title: String
}

final class PhotoLoader {

  static let shared = PhotoLoader()

  func load(page: Int, completion: @escaping (Result<[Photo], Error>) -> Void) {

    URLSession.shared.dataTask(with: URL(string: "https://jsonplaceholder.typicode.com/photos?_start=\(page)&_limit=5")!, completionHandler: { data, response, error in
      if let data = data, let model = try? JSONDecoder().decode([Photo].self, from: data) {
        completion(.success(model))
      }
    }).resume()
  }
}

final class PhotoCell: UICollectionViewCell {
  lazy var label = UILabel(frame: .zero)

  override init(frame: CGRect) {
    super.init(frame: frame)

    label.translatesAutoresizingMaskIntoConstraints = false
    label.numberOfLines = 3
    addSubview(label)
    NSLayoutConstraint.activate([
      label.topAnchor.constraint(equalTo: topAnchor),
      label.leadingAnchor.constraint(equalTo: leadingAnchor),
      label.bottomAnchor.constraint(equalTo: bottomAnchor),
      label.trailingAnchor.constraint(equalTo: trailingAnchor)
    ])

  }

  required init?(coder: NSCoder) {
    return nil
  }

}

2 ответа

Решение

Без использования дополнительного массива инициализируйте снимок вviewDidLoad

override func viewDidLoad() {
    super.viewDidLoad()
    
    ...
    let snapshot = DataSourceSnapshot()
    snapshot.appendSections([Section.main])
    dataSource.apply(snapshot, animatingDifferences: false)
}

И в apply(_ photos) добавлять фотографии к текущему снимку, а не создавать новый

func apply(_ photos: [Photo]) {

    var snapshot = dataSource.snapshot()
    snapshot.appendItems(photos, toSection: .main)  
    dataSource.apply(snapshot, animatingDifferences: true)
}

Свойство не нужно

частный снимок var = DataSourceSnapshot()

Примечание:

Объявите источник данных как можно более конкретным

typealias DataSource = UICollectionViewDiffableDataSource<Section, Photo>
typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, Photo>

Тогда вы избавитесь от ненужной проверки типа в

dataSource = DataSource(collectionView: collectionView, cellProvider: { (cv, indexPath, photo) -> PhotoCell? in

    let cell = cv.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
    cell.backgroundColor = indexPath.item % 2 == 0 ? .systemTeal : .systemPink
    cell.label.text = photo.title
    return cell
 
})

Вы предоставляете свой apply(_ photos: [Photo])метод с новым набором данных каждый раз. Вам необходимо предоставить ему обновленный набор данных, чтобы логика сравнения могла применить изменения.

Прямо сейчас вы просто каждый раз даете ему новые данные.

Захватите свой Photos в массиве и вызовите apply с использованием didSet наблюдатель

  private var photos: [Photo] = [] {
    didSet { apply(photos) }
  }
.....
  func load(page: Int = 0) {
    PhotoLoader.shared.load(page: page) { result in
      if let photos = try? result.get() {
        self.photos.append(contentsOf: photos)
      }
    }
  }