Swift - Сложность с изображениями разных размеров в TableViewCell - PullRequest
6 голосов
/ 12 марта 2019

Я загружаю несколько удаленных изображений в Kingfisher и испытываю значительные трудности с их правильной загрузкой в ​​Tableview с ячейками динамической высоты. Моя цель - чтобы изображения всегда имели полную ширину экрана и динамическую высоту, как этого достичь?

Ранее я задавал связанный вопрос, который привел к пониманиюбазовый макет с использованием стека: SnapKit: как программно установить ограничения макета для элементов в TableViewCell

Итак, я построил что-то вроде следующего:

Hierarchy overview

Со следующим кодом (некоторые детали для краткости удалены):

// CREATE VIEWS
let containerStack = UIStackView()
let header = UIView()
let headerStack = UIStackView()
let title = UILabel()
let author = UILabel()
var previewImage = UIImageView()

...

// KINGFISHER
let url = URL(string: article.imageUrl)
previewImage.kf.indicatorType = .activity
previewImage.kf.setImage(
  with: url,
  options: [
    .transition(.fade(0.2)),
    .scaleFactor(UIScreen.main.scale),
    .cacheOriginalImage
]) { result in
  switch result {
  case .success(_):
    self.setNeedsLayout()
    UIView.performWithoutAnimation {
      self.tableView()?.beginUpdates()
      self.tableView()?.endUpdates()
    }
  case .failure(let error):
    print(error)
  }
}

...

// LAYOUT
containerStack.axis = .vertical

headerStack.axis = .vertical
headerStack.spacing = 6
headerStack.addArrangedSubview(title)
headerStack.addArrangedSubview(author)
header.addSubview(headerStack)

containerStack.addArrangedSubview(header)
containerStack.addSubview(previewImage)

addSubview(containerStack)

headerStack.snp.makeConstraints { make in
  make.edges.equalToSuperview().inset(20)
}

containerStack.snp.makeConstraints { make in
  make.edges.equalToSuperview()
}

Без ограничения на imageView изображение не появляется.

При следующем ограничении изображение также не отображается:

previewImage.snp.makeConstraints { make in
  make.leading.trailing.bottom.equalToSuperview()
  make.top.equalTo(headerView.snp.bottom).offset(20)
}

При других попытках изображение полностью перекошено или перекрывает метки / другие ячейки и изображения.

Наконец, после этого комментария: С помощью Auto Layout, как мне сделать размер UIImageView динамическим в зависимости от изображения? и суть: https://gist.github.com/marcc-orange/e309d86275e301466d1eecc8e400ad00 и с этими ограничениями make.edges.equalToSuperview() Я могуполучить имвозрасты отображаются в правильных масштабах, но они полностью покрывают метки.

В идеале это будет выглядеть примерно так:

mockup

Ответы [ 3 ]

5 голосов
/ 15 марта 2019

Относительно легко сделать то, что вы описываете: вашему представлению изображения нужно ограничение ширины, равное ширине «экрана» (как вы его выразили) и ограничение высоты, пропорциональное ограничению ширины (multiplier) в зависимости от пропорций загруженного изображения (также называемого «формат изображения»).Это значение не может быть установлено заранее;вам нужно настроить его, как только у вас есть загруженный образ, так как вы не знаете его пропорции до тех пор.Поэтому вам нужен выход для ограничения высоты, чтобы вы могли удалить его и заменить на тот, который имеет правильный multiplier, когда вы это знаете.Если ваши другие ограничения верны в отношении верхней и нижней частей изображения, все остальное будет соответствовать желаемому.

Эти снимки экрана показывают, что этот подход работает:

enter image description here

(Прокрутка далее вниз по таблице:)

enter image description here

Это не на 100% идентично вашемунужный интерфейс, но идея та же.В каждой ячейке у нас есть две метки и изображение, и изображения могут иметь разные пропорции, но эти соотношения отображаются правильно - и сами ячейки имеют различную высоту в зависимости от этого.

Это код ключа Iиспользуется:

    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
    // in real life you’d set the labels here too
    // in real life you’d be fetching the image from the network...
    // ...and probably supplying it asynchronously later
    let im = UIImage(named:self.pix[indexPath.row])!
    cell.iv.image = im
    let con = cell.heightConstraint!
    con.isActive = false
    let ratio = im.size.width/im.size.height
    let newcon = NSLayoutConstraint(item: con.firstItem, attribute: con.firstAttribute, relatedBy: con.relation, toItem: con.secondItem, attribute: con.secondAttribute, multiplier: ratio, constant: 0)
    newcon.isActive = true
    cell.heightConstraint = newcon
    return cell
4 голосов
/ 21 марта 2019

100% рабочее решение с примером кода

Мне только что удалось получить тот же макет с динамическим содержимым метки и динамическими размерами изображения.Я сделал это через ограничения и Autolayout .Взгляните на демонстрационный проект в этом репозитории GitHub


Как указал Мэтт, мы должны вычислить высоту каждой ячейки после загрузки изображения(когда мы знаем его ширину и высоту).Обратите внимание, что высота каждой ячейки вычисляется методом делегата tableView heightForRowAt IndexPath

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

Некоторые ключевые моменты, на которые следует обратить внимание:

  • Используйте 3 типа ячеек.Один для метки, один для субтитров и один для изображения.Внутри cellForRowAt инициализируйте и верните соответствующую ячейку.Каждая ячейка имеет уникальный cellIdentifier, но класс одинаков
  • количество разделов в tableView == количество источников данных
  • количество строк в разделе == 3
    • Первыйстрока соответствует заголовку, вторая строка соответствует субтитру, а третья соответствует изображению.
  • количество строк для меток должно быть 0, поэтому высота должна рассчитываться на основе содержимого
  • Внутри cellForRowAt загрузите изображение асинхронно, сохраните его в массиве и перезагрузите этот ряд.
  • При перезагрузке строки вызывается heightForRowAt, вычисляется необходимая высота ячейки на основе размеров изображения и возвращается высота.
  • Таким образом, высота каждой ячейки рассчитывается динамически на основе размеров изображения

Взгляните на некоторый код

override func numberOfSections(in tableView: UITableView) -> Int {
  return arrayListItems.count
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  //Title, SubTitle, and Image
  return 3
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

switch indexPath.row {
case 0:
  //configure and return Title Cell. See code in Github Repo 

case 1:

  //configure and return SubTitle Cell. See code in Github Repo

case 2:

  let cellImage = tableView.dequeueReusableCell(withIdentifier: cellIdentifierImage) as! TableViewCell
  let item = arrayListItems[indexPath.section]
  //if we already have the image, just show
  if let image = arrayListItems[indexPath.section].image {
    cellImage.imageViewPicture.image = image
  }else {

    if let url = URL.init(string: item.imageUrlStr) {

      cellImage.imageViewPicture.kf.setImage(with: url) { [weak self] result in
        guard let strongSelf = self else { return } //arc
        switch result {
        case .success(let value):

          print("=====Image Size \(value.image.size)"  )
          //store image in array so that `heightForRowAt` can use image width and height to calculate cell height
          strongSelf.arrayListItems[indexPath.section].image = value.image
          DispatchQueue.main.async {
          //reload this row so that `heightForRowAt` runs again and calculates height of cell based on image height
            self?.tableView.reloadRows(at: [indexPath], with: .automatic)
          }

        case .failure(let error):
          print(error) // The error happens
        }
      }

    }

  }


  return cellImage

default:
  print("this should not be called")
}

//this should not be executed
return .init()
}


override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
//calculate the height of label cells automatically in each section
if indexPath.row == 0 || indexPath.row == 1 { return UITableView.automaticDimension }

// calculating the height of image for indexPath
else if indexPath.row == 2, let image = arrayListItems[indexPath.section].image {

  print("heightForRowAt indexPath : \(indexPath)")
  //image

  let imageWidth = image.size.width
  let imageHeight = image.size.height

  guard imageWidth > 0 && imageHeight > 0 else { return UITableView.automaticDimension }

  //images always be the full width of the screen
  let requiredWidth = tableView.frame.width

  let widthRatio = requiredWidth / imageWidth

  let requiredHeight = imageHeight * widthRatio

  print("returned height \(requiredHeight) at indexPath: \(indexPath)")
  return requiredHeight


}
else { return UITableView.automaticDimension }
}

Related.

Другой подход, которому мы можем следовать, - вернуть размеры изображенияиз запроса API.Если это можно сделать, это сильно упростит ситуацию.Взгляните на этот похожий вопрос (для collectionView).

Ячейки представления Собственного размера с размерами асинхронной загрузки изображений.

Placholder.com Используетсядля асинхронной выборки изображений

Ячейки с самоконтролем: (Хорошее чтение)

Образец

Sample

1 голос
/ 19 марта 2019

Существует прямое решение для вашей проблемы, если вы не хотите менять макет.

1 - определите вашу ячейку

2 - поместите UIImageView и другие элементы пользовательского интерфейсавам нравится внутри вашей ячейки и добавить эти ограничения для изображения: -top, ведущий, трейлинг, снизу до суперпредставления -height ограничения и добавить выход к вашему коду (например: heightConstraint)

enter image description here

3-Измените наполнение контента на: аспектное соответствие

4 - Загрузите ваши изображения через зимородок или любым другим способом, который вам нравится, после того, как вы передадите свое изображение, проверьте размер ивычислите соотношение: imageAspectRatio = высота / ширина

5-Установите высотуConstraint.constant = screenWidth * imageAspectRatio

6-вызовите layoutIfNeeded () для ячейки, и вы должны быть в порядке!

* Это решение работает с любой композицией макета пользовательского интерфейса, включая представления стека, поскольку точка имеет ограничение на изображения и позволяет представлению таблицы выяснить, как рассчитать и нарисовать ограничениеs.

class CustomTableViewCell: UITableViewCell {

    @IBOutlet weak var heightConstraint: NSLayoutConstraint!
    @IBOutlet weak var sampleImageView: UIImageView!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    func configure(image:UIImage) {
        let hRatio = image.size.height / image.size.width
        let newImageHeight = hRatio * UIScreen.main.bounds.width
        heightConstraint.constant = newImageHeight
        sampleImageView.image = image
        sampleImageView.layoutIfNeeded()
    }
}

Результат: enter image description here

...