ここに、現在のiOS開発に関するかなり基本的な質問がありますが、良い答えは見つかりませんでした。
(たとえば)垂直のUICollectionView
で、
全角セルにすることは可能ですが、自動レイアウトによって動的な高さを制御できますか?
(iOSの「動的な高さ」に慣れていない場合、セルには、たとえば長さの異なるテキストビューやサイズの異なる画像が含まれるため、最終的に各セルの高さはまったく異なります。)
もしそうならどのように?
これは、おそらく「iOSで最も重要な質問で、本当に良い答えはありません」と思われます。
Swift 4.2およびiOS 12では、UICollectionViewFlowLayout
をサブクラス化し、estimatedItemSize
プロパティをUICollectionViewFlowLayoutAutomaticSize
に設定できます(これにより、システムにUICollectionViewCell
sの自動サイズ変更を処理するよう指示します)。セルの幅を設定するには、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
}
期待されるディスプレイ:
問題
あなたは自動高さを探していて、幅もいっぱいにしたいので、UICollectionViewFlowLayoutAutomaticSize
を使用して両方を取得することはできません。 UITableVIew
の代わりにUICollectionView
を使用しない理由は、heightForRow
をUITableViewAutomaticDimension
として返すだけで、自動的に管理されます。とにかくUICollectionView
を使用してやりたいので、以下が解決策です。
ソリューション
Step-I:セルの予想される高さを計算する
1。UILabel
にCollectionViewCell
のみがあり、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。CollectionViewCell
にUIImageView
のみが含まれており、UIImage
の高さを取得する必要がある場合にHeightで動的であると想定される場合(UIImageView
にはAspectRatio
制約が必要です) )
// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale
。計算された高さよりも両方の高さが含まれている場合は、それらを加算します。
STEP-II:
CollectionViewCell
のサイズを返す
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)
}
これがあなたのお役に立てば幸いです。
この問題に対処する方法はいくつかあります。
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)
}
これらは完全な生産準備が整った例ではありませんが、正しい方向で開始できるはずです。これがベストプラクティスであるとは言えませんが、複数のラベルを含むかなり複雑なセルであっても、複数の行に折り返される場合と折り返されない場合があります。
私はその問題の非常に簡単な解決策を見つけました: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
結果は、動的な高さを持つ全幅サイズのセルです。
CollectionViewCellに幅の制約を追加する必要があります
class SelfSizingCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
個人的には、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を使用することさえ気にしますか自動サイズ調整セル?
これが「本当に良い答え」としてふさわしいかどうかはわかりませんが、私はこれを達成するために使用しています。私のフローレイアウトは水平であり、自動レイアウトで幅を調整しようとしているので、あなたの状況に似ています。
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
が再度呼び出されます。あなたの場合、レイアウトで利用可能な新しい制約が与えられれば更新できるはずです。
エリックの答えに対する私のコメントによると、私のソリューションは彼と非常に似ていますが、固定サイズに制約するために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ここで動作するサンプルアプリ。
フローレイアウトのestimatedItemSize
を設定します。
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
セルに幅の制約を定義し、スーパービューの幅に等しくなるように設定します。
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でテスト済み。
IOS 10から、それを行うためのフローレイアウトに関する新しいAPIが導入されました。
必要なことは、flowLayout.estimatedItemSize
を新しい定数UICollectionViewFlowLayoutAutomaticSize
に設定することだけです。
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
}
出来上がり、あなたの細胞の高さは自動成長します!
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()
}
}