Подтвердить что ты не робот

Динамический размер заголовка UICollectionView на основе UILabel

Я прочитал кучу сообщений о добавлении заголовка в UICollectionView. В приложении iOS 7+ в Swift я пытаюсь добавить заголовок с UILabel, в котором высота должна корректироваться в зависимости от высоты UILabel. UILabel имеет строки = 0.

Я установил заголовок в IB с помощью AutoLayout

enter image description here

ViewController реализует UICollectionViewDelegate, UICollectionViewDataSource. Я не создал пользовательский класс для заголовка, но я использую эти две функции:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
      //description is a String variable defined in the class
    let size:CGSize = (description as NSString).boundingRectWithSize(CGSizeMake(CGRectGetWidth(collectionView.bounds) - 20.0, 180.0), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: UIFont(name: "Helvetica Neue", size: 16.0)], context: nil).size
    return CGSizeMake(CGRectGetWidth(collectionView.bounds), ceil(size.height))
}

func collectionView(collectionView: UICollectionView!, viewForSupplementaryElementOfKind kind: String!, atIndexPath indexPath: NSIndexPath!) -> UICollectionReusableView! {
    var reusableview:UICollectionReusableView = UICollectionReusableView()
    if (kind == UICollectionElementKindSectionHeader) {
                    //listCollectionView is an @IBOutlet UICollectionView defined at class level, using collectionView crashes
            reusableview = listCollectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "ListHeader", forIndexPath: indexPath) as UICollectionReusableView
            let label = reusableview.viewWithTag(200) as UILabel  //the UILabel within the header is tagged with 200
            label.text = description   //description is a String variable defined in the class
        }
    }
    return reusableview
}

Отображение текста, похоже, работает, но расчет высоты не работает (см. снимок экрана ниже). Кроме того, я не думаю, что я могу получить доступ к UILabel через функцию collectionView...referenceSizeForHeaderInSection. Любые предложения по правильному вычислению CGSize?

enter image description here

4b9b3361

Ответ 1

Вот как я это сделал:

let labels = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium. Vestibulum hendrerit arcu ut ipsum gravida, ut tincidunt justo pellentesque. Etiam lacus ligula, aliquet at lorem vel, ullamcorper commodo turpis. Nullam commodo sollicitudin mauris eu faucibus.",
"Lorem ipsum dolor",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ac lorem enim. Curabitur rhoncus efficitur quam, et pretium ipsum. Nam eu magna at velit sollicitudin fringilla nec nec nisi. Quisque nec enim et ipsum feugiat pretium."]

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

В моем подклассе UICollectionReusableView (MyHeaderCollectionReusableView) есть ящик с меткой label, который я использую для просмотра заголовка раздела, назначая его в раскадровке (установка "MyHeader" как идентификатор повторного использования для просмотра раздела). Эта метка имеет горизонтальные и вертикальные ограничения пространства для границ заголовка раздела для правильной автоопределения.

override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 3
    }

override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

        let headerView =
        collectionView.dequeueReusableSupplementaryViewOfKind(kind,
            withReuseIdentifier: "MyHeader",
            forIndexPath: indexPath)
            as MyHeaderCollectionReusableView

        headerView.label.text = labels[indexPath.section]

        return headerView

    }

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        // that -16 is because I have 8px for left and right spacing constraints for the label.
        let label:UILabel = UILabel(frame: CGRectMake(0, 0, collectionView.frame.width - 16, CGFloat.max))
        label.numberOfLines = 0
        label.lineBreakMode = NSLineBreakMode.ByWordWrapping
       //here, be sure you set the font type and size that matches the one set in the storyboard label
        label.font = UIFont(name: "Helvetica", size: 17.0)
        label.text = labels[section]
        label.sizeToFit()

// Set some extra pixels for height due to the margins of the header section.  
//This value should be the sum of the vertical spacing you set in the autolayout constraints for the label. + 16 worked for me as I have 8px for top and bottom constraints.
        return CGSize(width: collectionView.frame.width, height: label.frame.height + 16)
    }

Ответ 2

Как и вопросник, у меня был UICollectionView, содержащий заголовок с одной меткой, высота которой я хотел изменить. Я создал расширение UILabel для измерения высоты многострочной метки с известной шириной:

public extension UILabel {
    public class func size(withText text: String, forWidth width: CGFloat) -> CGSize {
        let measurementLabel = UILabel()
        measurementLabel.text = text
        measurementLabel.numberOfLines = 0
        measurementLabel.lineBreakMode = .byWordWrapping
        measurementLabel.translatesAutoresizingMaskIntoConstraints = false
        measurementLabel.widthAnchor.constraint(equalToConstant: width).isActive = true
        let size = measurementLabel.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        return size
    }
}

Примечание: приведенное выше в синтаксисе Swift 3.

Затем я реализую метод размера заголовка UICollectionViewDelegateFlowLayout как:

extension MyCollectionViewController : UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        let text = textForHeader(inSection: section)
        var size =  UILabel.size(withAttributedText: text, forWidth: collectionView.frame.size.width)
        size.height = size.height + 16
        return size
    }
}

Работа по вычислению размера заголовка делегируется вышеуказанному расширению UILabel. +16 представляет собой экспериментально полученное фиксированное смещение (8 + 8), основанное на марже и может быть получено программным путем.

Все, что необходимо в обратном вызове заголовка, это просто указать текст:

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == UICollectionElementKindSectionHeader, let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as?  MyCollectionHeader {
        let text = textForHeader(inSection: section)
        headerView.label.text = text
        return headerView
    }
    return UICollectionReusableView()
}

Ответ 3

Идея состоит в том, чтобы иметь экземпляр заголовка шаблона в памяти для вычисления желаемой высоты перед созданием представления заголовка результата. Вы должны переместить представление заголовка раздела в отдельный .nib файл, установить все ограничения автоопределения и создать экземпляр шаблона в методе viewDidLoad следующим образом:

class MyViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

  @IBOutlet var collectionView : UICollectionView?
  private var _templateHeader : MyHeaderView

  override func viewDidLoad() {
    super.viewDidLoad()

    let nib = UINib(nibName: "HeaderView", bundle:nil)
    self.collectionView?.registerNib(nib, forCellWithReuseIdentifier: "header_view_id")

    _templateHeader = nib.instantiateWithOwner(nil, options:nil)[0] as! MyHeaderView
  }

}

Затем вы сможете вычислить размер заголовка (высота в моем примере) в методе делегата раскладки:

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {

    _templateHeader.lblTitle.text = "some title here"
    _templateHeader.lblDescription.text = "some long description"

    _templateHeader.setNeedsUpdateConstraints();
    _templateHeader.updateConstraintsIfNeeded()

    _templateHeader.setNeedsLayout();
    _templateHeader.layoutIfNeeded();

    let computedSize = _templateHeader.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

    return CGSizeMake(collectionView.bounds.size.width, computedSize.height);
}

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

func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

    switch kind {

    case UICollectionElementKindSectionHeader:

        let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "header_view_id", forIndexPath: indexPath) as! MyHeaderView
        headerView.lblTitle.text = "some title here"
        headerView.lblDescription.text = "some long description"

        headerView.setNeedsUpdateConstraints()
        headerView.updateConstraintsIfNeeded()

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        return headerView
    default:
        assert(false, "Unexpected kind")
    }

}

Забыл сказать об одном важном моменте - в вашем представлении заголовка должно быть ограничение автоопределения на его contentView, чтобы оно соответствовало ширине просмотра коллекции (плюс или минус желаемые поля).

Ответ 4

Мне повезло с использованием метода Владимира, но мне пришлось установить рамку представления шаблона равной ширине в моем представлении коллекции.

    templateHeader.bounds = CGRectMake(templateHeader.bounds.minX, templateHeader.bounds.minY, self.collectionView.bounds.width, templateHeader.bounds.height)

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

Ответ 5

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

Код для вашего UICollectionViewDelegate:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    // We get the actual header view
    let header = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section)) as! MyHeaderView
    // We ask the label what size it takes (eventually accounting for horizontal margins)
    var size = header.myLabel.sizeThatFits(CGSize(width: collectionView.frame.width - horizontalMargins, height: .greatestFiniteMagnitude))
    // We eventually account for vertical margins
    size.height += verticalMargins
    return size
}

Работает для iOS 11+.

Ответ 6

В вашей ячейке добавьте следующее:

fileprivate static let font = UIFont(name: FontName, size: 16)
fileprivate static let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)

Настройте шрифты и вставки соответствующим образом.

Затем добавьте в свою ячейку следующее

   static func textHeight(_ text: String, width: CGFloat) -> CGFloat {
    let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)
    let attributes = [ NSAttributedStringKey.font: font ]
    let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin]
    let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes as [NSAttributedStringKey : Any], context: nil)
    return ceil(bounds.height) + insets.top + insets.bottom
}

Теперь вы можете использовать эту функцию для расчета автоматической высоты

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    let height = ManuallySelfSizingCell.textHeight("Your text", width: view.frame.width)
    return CGSize(width: view.frame.width, height: height + 16)
}

Ответ 7

Вы должны реализовать метод UICollectionViewDelegate referenceSizeForHeaderInSection.

Там вам нужно рассчитать высоту без использования метки, вызвав boundingRectWithSize:options:context: в строке с соответствующими атрибутами.