web-dev-qa-db-ja.com

UICollectionView、全幅セル、自動レイアウトの動的な高さを許可しますか?

ここに、現在のiOS開発に関するかなり基本的な質問がありますが、良い答えは見つかりませんでした。


(たとえば)垂直のUICollectionViewで、

全角セルにすることは可能ですが、自動レイアウトによって動的な高さを制御できますか?

(iOSの「動的な高さ」に慣れていない場合、セルには、たとえば長さの異なるテキストビューやサイズの異なる画像が含まれるため、最終的に各セルの高さはまったく異なります。)

もしそうならどのように?

これは、おそらく「iOSで最も重要な質問で、本当に良い答えはありません」と思われます。

59
Fattie

Swift 4.2およびiOS 12では、UICollectionViewFlowLayoutをサブクラス化し、estimatedItemSizeプロパティをUICollectionViewFlowLayoutAutomaticSizeに設定できます(これにより、システムにUICollectionViewCellsの自動サイズ変更を処理するよう指示します)。セルの幅を設定するには、layoutAttributesForElements(in:)layoutAttributesForItem(at:)をオーバーライドする必要があります。最後に、セルのpreferredLayoutAttributesFitting(_:)メソッドをオーバーライドし、圧縮されたフィッティングの高さを計算する必要があります。


次の完全なコードは、複数行のUILabelを全角のUIcollectionViewCell内に表示する方法を示しています(UICollectionViewのセーフエリアとUICollectionViewFlowLayoutのインセットによって制約されています)。

CollectionViewController.Swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let items = [
        "Lorem ipsum.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        "Lorem ipsum dolor sit amet.",
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam."
    ]
    let columnLayout = FlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.alwaysBounceVertical = true
        collectionView.collectionViewLayout = columnLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.label.text = items[indexPath.row]
        return cell
    }

}

FlowLayout.Swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    override init() {
        super.init()

        self.minimumInteritemSpacing = 10
        self.minimumLineSpacing = 10
        self.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = collectionView else { return nil }
        layoutAttributes.bounds.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        guard scrollDirection == .vertical else { return superLayoutAttributes }

        let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute in
            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
        }
        return computedAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

}

Cell.Swift

import UIKit

class Cell: UICollectionViewCell {

    let label = UILabel()

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

        label.numberOfLines = 0
        backgroundColor = .orange
        contentView.addSubview(label)

        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        layoutIfNeeded()
        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        layoutAttributes.bounds.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        return layoutAttributes
    }

}

preferredLayoutAttributesFitting(_:)の代替実装を次に示します。

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    label.preferredMaxLayoutWidth = label.bounds.size.width
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

期待されるディスプレイ:

enter image description here

42
Imanou Petit

問題

あなたは自動高さを探していて、幅もいっぱいにしたいので、UICollectionViewFlowLayoutAutomaticSizeを使用して両方を取得することはできません。 UITableVIewの代わりにUICollectionViewを使用しない理由は、heightForRowUITableViewAutomaticDimensionとして返すだけで、自動的に管理されます。とにかくUICollectionViewを使用してやりたいので、以下が解決策です。

ソリューション

Step-I:セルの予想される高さを計算する

1。UILabelCollectionViewCellのみがあり、numberOfLines=0を設定し、UIlableの予想される高さを計算した場合、3つのパラメーターすべてを渡します

func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat {
    // pass string, font, LableWidth  
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
     label.numberOfLines = 0
     label.lineBreakMode = NSLineBreakMode.byWordWrapping
     label.font = font
     label.text = text
     label.sizeToFit()

     return label.frame.height
}

2。CollectionViewCellUIImageViewのみが含まれており、UIImageの高さを取得する必要がある場合にHeightで動的であると想定される場合UIImageViewにはAspectRatio制約が必要です) )

// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale

計算された高さよりも両方の高さが含まれている場合は、それらを加算します。

STEP-IICollectionViewCellのサイズを返す

1。 viewControllerにUICollectionViewDelegateFlowLayoutデリゲートを追加します

2。デリゲートメソッドを実装する

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    // This is just for example, for the scenario Step-I -> 1 
    let yourWidthOfLable=self.view.size.width
    let font = UIFont(name: "Helvetica", size: 20.0)

    var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable)


    return CGSize(width: view.frame.width, height: expectedHeight)
}

これがあなたのお役に立てば幸いです。

22
Dhiru

この問題に対処する方法はいくつかあります。

1つの方法は、コレクションビューのフローレイアウトに推定サイズを与え、セルサイズを計算することです。

注:以下のコメントで述べたように、iOS 10では、セルへの呼び出しをトリガーするためにサイズを指定したり推定したりする必要がなくなりましたfunc preferredLayoutAttributesFitting(_ layoutAttributes:)。以前(iOS 9)では、セルのprefferedLayoutAttributesを照会する場合に推定サイズを提供します。

(ストーリーボードを使用しており、コレクションビューがIBを介して接続されていると仮定)

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size
}

通常は十分な単純なセルの場合。それでもサイズが正しくない場合は、コレクションビューセルでfunc preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributesをオーバーライドできます。これにより、セルサイズをより詳細に制御できます。 注:フローレイアウトに推定サイズを指定する必要があります

次に、func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributesをオーバーライドして、正しいサイズを返します。

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
    let autoLayoutFrame = CGRect(Origin: autoLayoutAttributes.frame.Origin, size: autoLayoutSize)
    autoLayoutAttributes.frame = autoLayoutFrame
    return autoLayoutAttributes
}

または、サイズ変更セルを使用して、UICollectionViewDelegateFlowLayoutのセルのサイズを計算することもできます。

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let width = collectionView.frame.width
    let size = CGSize(width: width, height: 0)
    // assuming your collection view cell is a nib
    // you may also instantiate a instance of our cell from a storyboard
    let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell
    sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    sizingCell.frame.size = size
    sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell
    return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
}

これらは完全な生産準備が整った例ではありませんが、正しい方向で開始できるはずです。これがベストプラクティスであるとは言えませんが、複数のラベルを含むかなり複雑なセルであっても、複数の行に折り返される場合と折り返されない場合があります。

16
Eric Murphey

私はその問題の非常に簡単な解決策を見つけました:CollectionViewCellの中に、実際には単なる背景であるUIView()がありました。全幅を取得するには、次のアンカーを設定するだけです

bgView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 30).isActive = true // 30 is my added up left and right Inset
bgView.topAnchor.constraint(equalTo: topAnchor).isActive = true
bgView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
bgView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true

「魔法」は最初の行で発生します。 widthAnchorを画面の幅に動的に設定します。また、CollectionViewのインセットを減算することも重要です。そうしないと、セルが表示されません。 そのような背景ビューが必要ない場合は、単に非表示にします。

FlowLayoutは次の設定を使用します

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

結果は、動的な高さを持つ全幅サイズのセルです。

enter image description here

10
inf1783

CollectionViewCellに幅の制約を追加する必要があります

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
3
Mecid

個人的には、AutoLayoutがサイズを決定するUICollectionViewを持つ最良の方法を見つけましたが、各Cellは異なるサイズを持つことができますが、実際のCellを使用してサイズを測定しながらUICollectionViewDelegateFlowLayout sizeForItemAtIndexPath関数を実装します。

これについては、私のブログ投稿の1つで説明しました。 https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-Swift-3/

これがあなたが望むものを達成するのに役立つことを願っています。私は100%確信していませんが、UITableViewとは異なり、AutoLayout injunctionを使用してセルの高さを完全に自動化することができます

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionViewCellには画面の幅全体を埋める必要がないため、UICollectionViewにはAutoLayoutがサイズを決定するような方法はありません。

しかし、ここにあなたのための質問があります:フルスクリーン幅のセルが必要な場合、なぜあなたは付属の古き良きUITableViewの上でUICollectionViewを使用することさえ気にしますか自動サイズ調整セル?

3
xxtesaxx

これが「本当に良い答え」としてふさわしいかどうかはわかりませんが、私はこれを達成するために使用しています。私のフローレイアウトは水平であり、自動レイアウトで幅を調整しようとしているので、あなたの状況に似ています。

extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // My height is static, but it could use the screen size if you wanted
    return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60) 
  }
}

次に、自動レイアウト制約が変更されるView Controllerで、NSNotificationを起動します。

NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)

UICollectionViewサブクラスで、その通知をリッスンします。

// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)

レイアウトを無効にします。

func handleConstraintNotification(notification: Notification) {
    self.collectionView?.collectionViewLayout.invalidateLayout()
}

これにより、コレクションビューの新しいサイズを使用してsizeForItemAtが再度呼び出されます。あなたの場合、レイアウトで利用可能な新しい制約が与えられれば更新できるはずです。

2
Mark Suman

エリックの答えに対する私のコメントによると、私のソリューションは彼と非常に似ていますが、固定サイズに制約するためにpreferredSizeFor ...に制約を追加する必要がありました。

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize, withHorizontalFittingPriority
        horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {

        width.constant = targetSize.width

        let size = contentView.systemLayoutSizeFitting(
            CGSize(width: targetSize.width, height: 1),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: verticalFittingPriority)

        print("\(#function) \(#line) \(targetSize) -> \(size)")
        return size
    }

この質問には多くの重複があり、ここで詳細に答えました: https://stackoverflow.com/a/47424930/2171044ここで動作するサンプルアプリ。

1
Chris Conover
  1. フローレイアウトのestimatedItemSizeを設定します。

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
    
  2. セルに幅の制約を定義し、スーパービューの幅に等しくなるように設定します。

    class CollectionViewCell: UICollectionViewCell {
        private var widthConstraint: NSLayoutConstraint?
    
        ...
    
        override init(frame: CGRect) {
            ...
            // Create width constraint to set it later.
            widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0)
        }
    
        override func updateConstraints() {
            // Set width constraint to superview's width.
            widthConstraint?.constant = superview?.bounds.width ?? 0
            widthConstraint?.isActive = true
            super.updateConstraints()
        }
    
        ...
    }
    

完全な例: https://Gist.github.com/madyanov/246217ad2628ba6a870114131a27c55c

IOS 11でテスト済み。

1
Roman

IOS 10から、それを行うためのフローレイアウトに関する新しいAPIが導入されました。

必要なことは、flowLayout.estimatedItemSizeを新しい定数UICollectionViewFlowLayoutAutomaticSizeに設定することだけです。

ソース: http://asciiwwdc.com/2016/sessions/219

1
Arpit Dongre

viewDidLayoutSubviewsで、estimatedItemSizeを全幅に設定します(レイアウトはUICollectionViewFlowLayoutオブジェクトを参照します)。

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
    return CGSize(width: collectionView.bounds.size.width, height: 120)
}

セルで、制約がセルの上部と下部の両方に接触していることを確認します(次のコードはCartographyを使用して制約の設定を簡素化しますが、必要に応じてNSLayoutConstraintまたはIBで実行できます)。

constrain(self, nameLabel, valueLabel) { view, name, value in
        name.top == view.top + 10
        name.left == view.left
        name.bottom == view.bottom - 10
        value.right == view.right
        value.centerY == view.centerY
    }

出来上がり、あなたの細胞の高さは自動成長します!

1
Raphael

IPhoneの幅を調整するために動的な幅が必要なため、解決策はありませんでした。

    class CustomLayoutFlow: UICollectionViewFlowLayout {
        override init() {
            super.init()
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
        }

        override var itemSize: CGSize {
            set { }
            get {
                let width = (self.collectionView?.frame.width)!
                let height = (self.collectionView?.frame.height)!
                return CGSize(width: width, height: height)
            }
        }
    }

    class TextCollectionViewCell: UICollectionViewCell {
        @IBOutlet weak var textView: UITextView!

        override func prepareForReuse() {
            super.prepareForReuse()
        }
    }




    class IntroViewController: UIViewController, UITextViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UINavigationControllerDelegate {
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
        @IBOutlet weak var collectionView: UICollectionView!
        var collectionViewLayout: CustomLayoutFlow!

        override func viewDidLoad() {
            super.viewDidLoad()

            self.collectionViewLayout = CustomLayoutFlow()
            self.collectionView.collectionViewLayout = self.collectionViewLayout
        }

        override func viewWillLayoutSubviews() {
            self.collectionViewTopDistanceConstraint.constant = UIScreen.main.bounds.height > 736 ? 94 : 70

            self.view.layoutIfNeeded()
        }
    }
0
iOS Flow