UICollectionViewでセルを左揃え


96

私のプロジェクトではUICollectionViewを使用しています。この場合、線上に幅の異なる複数のセルがあります。による: https //developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

等間隔でセルを線全体に広げます。これは期待どおりに発生しますが、左揃えにし、パディング幅をハードコードします。

UICollectionViewFlowLayoutをサブクラス化する必要があると思いますが、チュートリアルなどをオンラインで読んだ後、これがどのように機能するのかわかりません。


回答:


169

このスレッドの他のソリューションは、ラインが1アイテムのみで構成されている場合、または複雑すぎる場合、正しく機能しません。

Ryanの例に基づいて、新しい要素のY位置を検査して新しい行を検出するようにコードを変更しました。非常にシンプルでパフォーマンスが速い。

迅速:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.y >= maxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

補足ビューのサイズを維持したい場合は、forEach呼び出しのクロージャの上部に次を追加します。

guard layoutAttribute.representedElementCategory == .cell else {
    return
}

Objective-C:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    CGFloat maxY = -1.0f;

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        if (attribute.frame.origin.y >= maxY) {
            leftMargin = self.sectionInset.left;
        }

        attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height);

        leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
        maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
    }

    return attributes;
}

4
ありがとう!私は、配列内の属性のコピーを作成することにより固定されている警告を、なっていた let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
私が行ったように中央揃えのバージョンを探してこれに出くわした可能性がある人のために、私はここにエンジェルの回答の修正バージョンを投稿しました:stackoverflow.com/a/38254368/2845237
Alex Koshy

この解決策を試しましたが、コレクションビューの右側にあるセルの一部がコレクションビューの境界を超えています。他の誰かがこれに遭遇しましたか?
Happiehappie

@Happiehappieはい、私もその振る舞いをしています。何か修正?
John Baum

このソリューションを試してみましたが、レイアウトをInterface Builder UtiliesパネルのcollectionViewに関連付けることができませんでした。結局、コードでそれを行うことになりました。なぜ何か手掛かり?
acrespo 2017年

63

この質問への回答には、多くの素晴らしいアイデアが含まれています。ただし、それらのほとんどにはいくつかの欠点があります。

  • セルのy値をチェックしないソリューションは、単一行のレイアウトでのみ機能します。複数行のコレクションビューレイアウトでは失敗します。
  • ソリューションチェックyのような値エンジェル・ガルシアOlloquiの答え すべてのセルが同じ高さを持っている場合にのみ作業を。高さが変化するセルでは失敗します。
  • ほとんどのソリューションはlayoutAttributesForElements(in rect: CGRect)関数をオーバーライドするだけです。それらは上書きしませんlayoutAttributesForItem(at indexPath: IndexPath)コレクションビューは定期的に後者の関数を呼び出して特定のインデックスパスのレイアウト属性を取得するため、これは問題です。その関数から適切な属性を返さないと、セルの挿入や削除のアニメーション中、またはコレクションビューのレイアウトを設定してセルのサイズを変更するときに、あらゆる種類の視覚的なバグが発生する可能性がありますestimatedItemSizeアップルのドキュメントの状態:

    すべてのカスタムレイアウトオブジェクトは、layoutAttributesForItemAtIndexPath:メソッドを実装する必要があります。

  • 多くのソリューションrectでは、layoutAttributesForElements(in rect: CGRect)関数に渡されるパラメーターについても想定しています。たとえば、多くの場合、rect常に新しい行の先頭から始まるという前提に基づいていますが、これは必ずしもそうとは限りません。

つまり、言い換えれば:

このページで提案されているソリューションのほとんどは特定のアプリケーションで機能しますが、すべての状況で期待どおりに機能するわけではありません。


AlignedCollectionViewFlowLayout

これらの問題に対処するためにUICollectionViewFlowLayoutマットクリスワグナーが同様の質問への回答で提案したのと同様のアイデアに従うサブクラスを作成しました。セルを整列させることもできます

⬅︎

左揃えのレイアウト

または ➡︎

右揃えのレイアウト

さらに、それぞれの行のセルを垂直方向に揃えるオプションを提供します(高さが異なる場合)。

ここからダウンロードするだけです。

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

使い方は簡単で、READMEファイルで説明されています。基本的にのインスタンスを作成しAlignedCollectionViewFlowLayout、必要な配置を指定して、それをコレクションビューのcollectionViewLayoutプロパティに割り当てます。

 let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, 
                                                         verticalAlignment: .top)

 yourCollectionView.collectionViewLayout = alignedFlowLayout

Cocoapodsでも利用できます。)


仕組み(左揃えのセルの場合):

ここでのコンセプトはlayoutAttributesForItem(at indexPath: IndexPath)機能のみに依存することです。では、layoutAttributesForElements(in rect: CGRect)内部のすべてのセルのrectインデックスパスを取得し、すべてのインデックスパスの最初の関数を呼び出して正しいフレームを取得します。

override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

    // We may not change the original layout attributes 
    // or UICollectionViewFlowLayout might complain.
    let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))

    layoutAttributesObjects?.forEach({ (layoutAttributes) in
        if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
            let indexPath = layoutAttributes.indexPath
            // Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
            if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
                layoutAttributes.frame = newFrame
            }
        }
    })

    return layoutAttributesObjects
}

(このcopy()関数は、単に配列内のすべてのレイアウト属性のディープコピーを作成します。その実装のソースコードを調べることができます。)

したがって、今しなければならないことは、layoutAttributesForItem(at indexPath: IndexPath)関数を適切に実装することだけです。スーパークラスはUICollectionViewFlowLayoutすでに各行に正しい数のセルを配置しているため、それらをそれぞれの行内で左にシフトするだけで済みます。難しいのは、各セルを左にシフトするために必要なスペースの量を計算することです。

セル間の間隔を固定したいので、中心となる考え方は、前のセル(現在レイアウトされているセルの左側のセル)が既に適切に配置されていると仮定することです。次にmaxX、前のセルのフレームの値にセル間隔を追加するだけでよく、それはorigin.xが現在のセルのフレームの値になります。

行の先頭に達したことを知るだけでよいので、前の行のセルの隣にセルを配置しません。(これにより、レイアウトが不正確になるだけでなく、非常に遅延します。)したがって、再帰アンカーが必要です。その再帰アンカーを見つけるために私が使用するアプローチは次のとおりです。

インデックスiのセルがインデックスi-1のセルと同じ行にあるかどうかを調べるには...

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

...現在のセルの周りに長方形を「描画」して、コレクションビュー全体の幅に引き伸ばします。UICollectionViewFlowLayoutすべてのセルを垂直方向の中央に配置するため、同じ行のすべてのセルがこの長方形と交差する必要があります。

したがって、私は単にインデックスi-1のセルがインデックスiのセルから作成されたこの線の長方形と交差するかどうかを確認します。

  • 交差する場合、インデックスiのセルは行の左端のセルではありません。
    →前のセルのフレーム(インデックスi-1を持つ)を取得し、現在のセルをその隣に移動します。

  • 交差しない場合、インデックスiのセルが行の左端のセルになります。
    →セルをコレクションビューの左端に移​​動します(垂直位置を変更せずに)。

layoutAttributesForItem(at indexPath: IndexPath)関数の実際の実装はここに掲載しません。最も重要な部分はアイデアを理解することであり、ソースコードでいつでも実装を確認できるためです。(ここでは、.right配置やさまざまな垂直方向の配置オプションも許可しているため、ここで説明するよりも少し複雑ですが、同じ考え方に従います。)


うわー、これはStackoverflowでこれまで書いた中で最も長い答えだと思います。これがお役に立てば幸いです。😉


このシナリオで適用します。stackoverflow.com/ questions / 56349052 / …が機能し ません。どうすればこれを達成できるか教えてください
Nazmul Hasan

これでも(本当に素晴らしいプロジェクトです)、アプリの垂直スクロールコレクションビューでフラッシュが発生します。20個のセルがあり、最初のスクロールでは、最初のセルは空白で、すべてのセルが1ポジション右に移動しています。上下に1回スクロールすると、物事は正しく位置合わせされます。
Parth Tamane

素晴らしいプロジェクト!Carthageで使用できるようになっているとよいでしょう。
pjuzeliunas

30

Swift 4.1とiOS 11では、必要に応じて、問題を解決するために、2つの完全な実装のいずれかを選択できます。


#1。自動サイズ変更UICollectionViewCellの左揃え

使用する方法を示して以下の実装UICollectionViewLayoutのをlayoutAttributesForElements(in:)UICollectionViewFlowLayout'S estimatedItemSizeUILabelpreferredMaxLayoutWidth中の細胞を自動サイズ変更左側整列させるためにUICollectionView

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]

    let columnLayout = FlowLayout(
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

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

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

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

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

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

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let label = UILabel()

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

        contentView.backgroundColor = .orange
        label.preferredMaxLayoutWidth = 120
        label.numberOfLines = 0

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

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

}

期待される結果:

ここに画像の説明を入力してください


#2。左ALIGN UICollectionViewCell固定サイズを有するS

使用する方法を示して以下の実装UICollectionViewLayoutのをlayoutAttributesForElements(in:)UICollectionViewFlowLayoutitemSizeAで事前に定義されたサイズの左整列セルに順にUICollectionView

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 140, height: 140),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

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

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

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

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

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

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

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

        contentView.backgroundColor = .cyan
    }

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

}

期待される結果:

ここに画像の説明を入力してください


セル属性を行でグループ化する場合10、任意の数値を使用していますか?
amariduran

25

2019年のシンプルなソリューション

これは、何年にもわたって状況が大きく変化した憂鬱な質問の1つです。簡単になりました。

基本的にはこれを行うだけです:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, obviously start fresh again each row

今必要なのは定型コードだけです。

override func layoutAttributesForElements(
                  in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
    
    guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0
    
    for a in att {
        if a.representedElementCategory != .cell { continue }
        
        if a.frame.origin.y >= y { x = sectionInset.left }
        
        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing
        
        y = a.frame.maxY
    }
    return att
}

単にそれをコピーして、 UICollectionViewFlowLayout完了です。

コピーして貼り付ける完全に機能するソリューション:

これがすべてです:

class TagsLayout: UICollectionViewFlowLayout {
    
    required override init() {super.init(); common()}
    required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
    
    private func common() {
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }
    
    override func layoutAttributesForElements(
                    in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        guard let att = super.layoutAttributesForElements(in:rect) else {return []}
        var x: CGFloat = sectionInset.left
        var y: CGFloat = -1.0
        
        for a in att {
            if a.representedElementCategory != .cell { continue }
            
            if a.frame.origin.y >= y { x = sectionInset.left }
            a.frame.origin.x = x
            x += a.frame.width + minimumInteritemSpacing
            y = a.frame.maxY
        }
        return att
    }
}

ここに画像の説明を入力してください

そして最後に...

最初にこれを明らかにした上記の@AlexShubinに感謝します!


ねえ。私は少し混乱しています。最初と最後のすべての文字列にスペースを追加しても、追加したスペースなしでアイテムセルに私の単語が表示されます。それを行う方法について何か考えはありますか?
Mohamed Lee

コレクションビューにセクションヘッダーがある場合、これによりそれらが削除されるようです。
タイラージェームス

22

質問はかなり上がっていますが、答えはなく、良い質問です。答えは、UICollectionViewFlowLayoutサブクラスの1つのメソッドをオーバーライドすることです。

@implementation MYFlowLayoutSubclass

//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this. 
#define ITEM_SPACING 10.0f

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.

    //this loop assumes attributes are in IndexPath order
    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left; //will add outside loop
        } else {
            CGRect newLeftAlignedFrame = attributes.frame;
            newLeftAlignedFrame.origin.x = leftMargin;
            attributes.frame = newLeftAlignedFrame;
        }

        leftMargin += attributes.frame.size.width + ITEM_SPACING;
        [newAttributesForElementsInRect addObject:attributes];
    }   

    return newAttributesForElementsInRect;
}

@end

Appleが推奨するように、スーパーからレイアウト属性を取得し、それらを反復します。それが行の最初の場合(origin.xが左マージンにあることで定義される)、そのままにしてxをゼロにリセットします。次に、最初のセルとすべてのセルについて、そのセルの幅にマージンを加えます。これはループ内の次のアイテムに渡されます。それが最初の項目でない場合は、origin.xを実行中の計算されたマージンに設定し、新しい要素を配列に追加します。


これは、リンクされた質問に対するChris Wagnerのソリューション(stackoverflow.com/a/15554667/482557)よりもはるかに優れていると思います。実装のUICollectionViewLayout呼び出しが繰り返されないためです。
Evan R

1
UICollectionViewFlowLayoutが中央に配置するため、行に単一のアイテムがある場合、これは機能しないように見えます。センターをチェックして問題を解決できます。次のようなものattribute.center.x == self.collectionView?.bounds.midX
ライアン・プールス2015

私はこれを実装しましたが、何らかの理由で最初のセクションで壊れます:最初のセルは最初の行の左側に表示され、2番目のセル(同じ行に収まらない)は次の行に移動しますが、左にはありません整列。代わりに、そのx位置は最初のセルが終わるところから始まります。
Nicolas Miari、2015年

@NicolasMiariループは、それが新しい行であると判断leftMarginし、if (attributes.frame.origin.x == self.sectionInset.left) {チェックでをリセットします。これは、デフォルトのレイアウトで新しい行のセルがとフラッシュされるように配置されていることを前提としていますsectionInset.left。そうでない場合、記述した動作が発生します。ループ内にブレークポイントを挿入し、比較の直前に2行目のセルの両側をチェックします。幸運を祈ります。
マイクサンド

3
おかげで... UICollectionViewLeftAlignedLayout:github.com/mokagio/UICollectionViewLeftAlignedLayoutを使用してしまいました。これは、さらにいくつかのメソッドをオーバーライドします。
Nicolas Miari、2015年

9

同じ問題が発生しました。CocoapodUICollectionViewLeftAlignedLayoutを試してみてください。それをプロジェクトに含めて、次のように初期化するだけです。

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];

レイアウトの初期化で角かっこがありません
RyanOfCourse 2015年

私はgithub.com/eroth/ERJustifiedFlowLayoutとUICollectionViewLeftAlignedLayoutの2つの回答をテストしました。見積もられたサイズ(セルフサイジング)を使用すると、2番目のものだけが正しい結果を生成しました。
EckhardN、2015

5

Michael Sandの回答に基づいて、UICollectionViewFlowLayout左、右、または完全(基本的にはデフォルト)の水平位置合わせを行うサブクラス化ライブラリを作成しました。これにより、各セル間の絶対距離を設定することもできます。それに水平方向の中央揃えと垂直方向の位置揃えも追加する予定です。

https://github.com/eroth/ERJustifiedFlowLayout


5

これがSwiftの元の答えです。それはまだほとんどうまくいきます。

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElementsInRect(rect)

        var leftMargin = sectionInset.left

        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        }

        return attributes
    }
}

例外:セルの自動サイズ設定

残念ながら大きな例外が1つあります。あなたが使用している場合はUICollectionViewFlowLayout「S estimatedItemSize。内部的にUICollectionViewFlowLayoutは少し物事を変えています。私はそれを完全に追跡していませんが、layoutAttributesForElementsInRectセルのサイズを自己調整している間に他のメソッドを呼び出すことは明らかです。私の試行錯誤から、私はそれがlayoutAttributesForItemAtIndexPathより頻繁に自動サイズ調整中に各セルを個別に要求するように見えることを発見しました。この更新は、でLeftAlignedFlowLayoutうまく動作しestimatedItemSizeます。静的サイズのセルでも機能しますが、追加のレイアウト呼び出しにより、セルの自動サイズ設定が不要な場合はいつでも元の回答を使用できます。

class LeftAlignedFlowLayout: UICollectionViewFlowLayout {

    private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes

        // First in a row.
        if layoutAttribute?.frame.origin.x == sectionInset.left {
            return layoutAttribute
        }

        // We need to align it to the previous item.
        let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
        guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
            return layoutAttribute
        }

        layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing

        return layoutAttribute
    }
}

これは最初のセクションで機能しますが、テーブルがスクロールするまでラベルは2番目のセクションでサイズ変更されません。私のコレクションビューはテーブルセルにあります。
EIキャプテンv2.0

5

迅速に。マイケルズの回答によると

override func layoutAttributesForElementsInRect(rect: CGRect) ->     [UICollectionViewLayoutAttributes]? {
    guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }
    let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
    var newAttributes = [UICollectionViewLayoutAttributes]()
    var leftMargin = self.sectionInset.left
    for attributes in oldAttributes {
        if (attributes.frame.origin.x == self.sectionInset.left) {
            leftMargin = self.sectionInset.left
        } else {
            var newLeftAlignedFrame = attributes.frame
            newLeftAlignedFrame.origin.x = leftMargin
            attributes.frame = newLeftAlignedFrame
        }

        leftMargin += attributes.frame.width + spacing
        newAttributes.append(attributes)
    }
    return newAttributes
}

4

すべての回答に基づいて、私は少し変更し、それは私にとってはうまくいきます

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0


    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY
                   || layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }

        if layoutAttribute.frame.origin.x == sectionInset.left {
            leftMargin = sectionInset.left
        }
        else {
            layoutAttribute.frame.origin.x = leftMargin
        }

        leftMargin += layoutAttribute.frame.width
        maxY = max(layoutAttribute.frame.maxY, maxY)
    }

    return attributes
}

4

あなたの誰かが問題に直面している場合- コレクションビューの境界を超えるコレクションビューの右側にあるセルの一部。次に、これを使用してください-

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin : CGFloat = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }
        return attributes
    }
}

CGFloat値を比較する代わりにINTを使用します。


3

ここでの回答に基づいていますが、コレクションビューにもヘッダーまたはフッターがある場合のクラッシュと整列の問題を修正しました。左のみのセルを揃える:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var prevMaxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in

            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if layoutAttribute.frame.origin.y >= prevMaxY {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            prevMaxY = layoutAttribute.frame.maxY
        }

        return attributes
    }
}

2

Michael Sandの回答に感謝します。私はそれを複数の行(各行の同じ配置Top y)のソリューションに変更しました。これは左揃えで、各アイテムの間隔も同じです。

static CGFloat const ITEM_SPACING = 10.0f;

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    CGRect contentRect = {CGPointZero, self.collectionViewContentSize};

    NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect];
    NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];

    CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
    NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];

    for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
        UICollectionViewLayoutAttributes *attr = attributes.copy;

        CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue];
        if (lastLeftMargin == 0) lastLeftMargin = leftMargin;

        CGRect newLeftAlignedFrame = attr.frame;
        newLeftAlignedFrame.origin.x = lastLeftMargin;
        attr.frame = newLeftAlignedFrame;

        lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
        [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]];
        [newAttributesForElementsInRect addObject:attr];
    }

    return newAttributesForElementsInRect;
}

1

これがSwift 5で動作する最適なコードを探す旅です。このスレッドと他のいくつかのスレッドからの回答にいくつか参加して、直面した警告と問題を解決しました。コレクションビューをスクロールすると、警告といくつかの異常な動作がありました。コンソールは以下を出力します:

これは、フローレイアウト「xyz」がUICollectionViewFlowLayoutによって返された属性をコピーせずに変更しているために発生している可能性があります。

私が直面した別の問題は、いくつかの長いセルが画面の右側で切り取られていることです。また、デリゲート関数でセクションインセットとminimumInteritemSpacingを設定していたため、値がカスタムクラスに反映されませんでした。そのための修正は、コレクションビューに適用する前に、これらの属性をレイアウトのインスタンスに設定することでした。

コレクションビューのレイアウトの使用方法は次のとおりです。

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

ここにフローレイアウトクラスがあります

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) || layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }

            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

あなたは私の日を救った
デビッド 'mArm' Ansermot

0

UICollectionViewの問題は、利用可能な領域にセルを自動的に収めようとすることです。まず、行と列の数を定義してから、その行と列のセルサイズを定義しました。

1)UICollectionViewのセクション(行)を定義するには:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2)セクション内のアイテム数を定義します。セクションごとに異なる数のアイテムを定義できます。'section'パラメータを使用してセクション番号を取得できます。

(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

3)各セクションと行のセルサイズを個別に定義します。セクション番号と行番号を取得するには、「indexPath」パラメーターを使用します([indexPath section]セクション番号と[indexPath row]行番号)。

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

4)次に、次を使用して行とセクションにセルを表示できます。

(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath

注:UICollectionView

Section == Row
IndexPath.Row == Column

0

Mike Sandの答えは良いですが、私はこのコードでいくつかの問題を経験しました(長いセルが切り取られたように)。そして新しいコード:

#define ITEM_SPACE 7.0f

@implementation LeftAlignedCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
        if (nil == attributes.representedElementKind) {
            NSIndexPath* indexPath = attributes.indexPath;
            attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
        }
    }
    return attributesToReturn;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* currentItemAttributes =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];

    if (indexPath.item == 0) { // first item of section
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = frame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.origin.x + previousFrame.size.width + ITEM_SPACE;

    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(0,
                                              currentFrame.origin.y,
                                              self.collectionView.frame.size.width,
                                              currentFrame.size.height);

    if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
        // the approach here is to take the current frame, left align it to the edge of the view
        // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
        // is on the same line, otherwise it is on it's own new line
        CGRect frame = currentItemAttributes.frame;
        frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = frame;
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}

0

minimumInteritemSpacingデリゲートのリスペクトからの尊敬に対するエンジェルガルシアオロキの回答を編集collectionView(_:layout:minimumInteritemSpacingForSectionAt:)しました(実装されている場合)。

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0
    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY {
            leftMargin = sectionInset.left
        }

        layoutAttribute.frame.origin.x = leftMargin

        let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
        let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing

        leftMargin += layoutAttribute.frame.width + spacing
        maxY = max(layoutAttribute.frame.maxY , maxY)
    }

    return attributes
}

0

上記のコードは私にとってはうまくいきます。それぞれのSwift 3.0コードを共有したいと思います。

class SFFlowLayout: UICollectionViewFlowLayout {

    let itemSpacing: CGFloat = 3.0

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
        var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
        var leftMargin = self.sectionInset.left
        for tempAttribute in attriuteElementsInRect! {
            let attribute = tempAttribute 
            if attribute.frame.origin.x == self.sectionInset.left {
                leftMargin = self.sectionInset.left
            }
            else {
                var newLeftAlignedFrame = attribute.frame
                newLeftAlignedFrame.origin.x = leftMargin
                attribute.frame = newLeftAlignedFrame
            }
            leftMargin += attribute.frame.size.width + itemSpacing
            newAttributeForElement.append(attribute)
        }
        return newAttributeForElement
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.