web-dev-qa-db-ja.com

UILabelに基づく動的UICollectionViewヘッダーサイズ

UICollectionViewへのヘッダーの追加に関する多くの投稿を読みました。 SwiftのiOS 7以降のアプリでは、UILabelの高さに基づいて高さを調整するUILabelを含むヘッダーを追加しようとしています。 UILabelにはlines = 0があります。

AutoLayoutを使用してIBにヘッダーを設定しました

enter image description here

ViewControllerはUICollectionViewDelegate, UICollectionViewDataSourceを実装します。ヘッダーにカスタムクラスを設定しませんでしたが、次の2つの関数を使用しています。

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
}

テキストの表示は機能しているようですが、高さの計算は機能していないようです(下のスクリーンショットを参照)。また、collectionView...referenceSizeForHeaderInSection関数を介してUILabelにアクセスできるとは思いません。 CGSizeを正しく計算する方法に関する提案はありますか?

enter image description here

26
Dean

これは私がそれをやった方法です:

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メソッドでヘッダーに必要なサイズを設定するために使用されます。

labelサブクラス(UICollectionReusableView)にMyHeaderCollectionReusableViewというラベルアウトレットがあり、これをストーリーボードで割り当てることでセクションヘッダービューに使用します(「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)
    }
24
Alvaro

質問者のように、1つのラベルを持つヘッダーを含む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()
}
5
Nick Ager

アイデアは、結果ヘッダービューを作成する前に、目的の高さを計算するためのテンプレートヘッダーインスタンスをメモリに保持することです。セクションヘッダービューを別の.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")
    }

}

1つの重要な瞬間について忘れていました-ヘッダービューのcontentViewには、コレクションビューの幅(必要なマージンをプラスまたはマイナス)に合わせるためのautolayout制約が必要です。

3

複数の回答に示されているようにコードから新しいラベルを再作成する代わりに、既存のラベルを使用してフィッティングサイズを計算できます。

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以降で動作します。

2
Cœur

私はウラジミールの方法を使って運が良かったのですが、テンプレートビューのフレームをコレクションビューと同じ幅に設定する必要がありました。

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

さらに、私のビューにはサイズ変更可能なコンポーネントがいくつかあり、テンプレートビューを使用すると、変更に対処するのに十分な堅牢性があります。まだもっと簡単な方法があるはずだと感じています。

0
Jacob Ruth

セルに次を追加します。

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)
}
0
Mohamed Ali