Sel Rata Kiri di UICollectionView


98

Saya menggunakan UICollectionView dalam proyek saya, di mana ada beberapa sel dengan lebar berbeda pada satu baris. Menurut: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

itu menyebarkan sel ke luar garis dengan padding yang sama. Ini terjadi seperti yang diharapkan, kecuali saya ingin kiri membenarkan mereka, dan kode keras lebar padding.

Saya pikir saya perlu membuat subclass UICollectionViewFlowLayout, namun setelah membaca beberapa tutorial dll secara online, saya sepertinya tidak mengerti cara kerjanya.


Jawaban:


176

Solusi lain di utas ini tidak berfungsi dengan baik, jika garis hanya terdiri dari 1 item atau terlalu rumit.

Berdasarkan contoh yang diberikan oleh Ryan, saya mengubah kode untuk mendeteksi baris baru dengan memeriksa posisi Y elemen baru. Sangat sederhana dan cepat dalam performa.

Cepat:

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
    }
}

Jika Anda ingin tampilan tambahan mempertahankan ukurannya, tambahkan berikut ini di bagian atas closure dalam forEachpanggilan:

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
Terima kasih! Saya mendapatkan peringatan, yang diperbaiki dengan membuat salinan atribut dalam array let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
Bagi siapa pun yang mungkin telah menemukan ini mencari versi rata tengah seperti yang saya lakukan, saya telah memposting versi jawaban Angel yang dimodifikasi di sini: stackoverflow.com/a/38254368/2845237
Alex Koshy

1
Mencoba solusi ini tetapi tidak dapat mengaitkan tata letak dengan collectionView di panel Interface Builder Utilies. Akhirnya melakukannya dengan kode. Ada petunjuk kenapa?
acrespo

1
Bagaimana Anda membuatnya sejajar dengan kiri dalam sistem kiri-ke-kanan, dan ke kanan dalam sistem kanan-ke-kiri?
Rick

1
Untuk penggunaan lakukan ini self.collectionView.collectionViewLayout = LeftAlignedCollectionViewFlowLayout ()
blackops

64

Ada banyak ide hebat yang disertakan dalam jawaban atas pertanyaan ini. Namun, kebanyakan dari mereka memiliki beberapa kekurangan:

  • Solusi yang tidak memeriksa sel y nilai hanya bekerja untuk layout single-line . Mereka gagal untuk tata letak tampilan koleksi dengan beberapa baris.
  • Solusi yang melakukan cek y nilai seperti jawaban Malaikat García Olloqui ini hanya bekerja jika semua sel memiliki ketinggian yang sama . Mereka gagal untuk sel dengan tinggi variabel.
  • Sebagian besar solusi hanya mengesampingkan layoutAttributesForElements(in rect: CGRect)fungsi. Mereka tidak menimpa layoutAttributesForItem(at indexPath: IndexPath). Ini menjadi masalah karena tampilan koleksi secara berkala memanggil fungsi terakhir untuk mengambil atribut tata letak untuk jalur indeks tertentu. Jika Anda tidak mengembalikan atribut yang sesuai dari fungsi itu, Anda kemungkinan akan mengalami semua jenis bug visual, misalnya selama penyisipan dan penghapusan animasi sel atau saat menggunakan sel yang mengukur sendiri dengan menyetel tata letak tampilan koleksi estimatedItemSize. The Apel docs negara:

    Setiap objek tata letak kustom diharapkan mengimplementasikan layoutAttributesForItemAtIndexPath:metode ini.

  • Banyak solusi juga membuat asumsi tentang rectparameter yang diteruskan ke layoutAttributesForElements(in rect: CGRect)fungsi. Misalnya, banyak yang didasarkan pada asumsi bahwa rectselalu dimulai di awal baris baru yang belum tentu demikian.

Jadi dengan kata lain:

Sebagian besar solusi yang disarankan di halaman ini berfungsi untuk beberapa aplikasi tertentu, tetapi tidak berfungsi seperti yang diharapkan dalam setiap situasi.


AlignedCollectionViewFlowLayout

Untuk mengatasi masalah ini, saya telah membuat UICollectionViewFlowLayoutsubclass yang mengikuti ide serupa seperti yang disarankan oleh matt dan Chris Wagner dalam jawaban mereka untuk pertanyaan serupa. Itu bisa menyelaraskan sel

⬅︎ kiri :

Tata letak rata kiri

atau ➡︎ kanan :

Tata letak rata kanan

dan juga menawarkan opsi untuk menyelaraskan secara vertikal sel di barisnya masing-masing (jika tingginya bervariasi).

Anda cukup mendownloadnya di sini:

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

Penggunaannya langsung dan dijelaskan di file README. Pada dasarnya Anda membuat sebuah instance AlignedCollectionViewFlowLayout, menentukan perataan yang diinginkan dan menetapkannya ke collectionViewLayoutproperti tampilan koleksi Anda :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Ini juga tersedia di Cocoapods .)


Cara kerjanya (untuk sel rata kiri):

Konsepnya di sini hanyalayoutAttributesForItem(at indexPath: IndexPath) mengandalkan fungsi . Di dalam layoutAttributesForElements(in rect: CGRect)kita cukup mendapatkan jalur indeks dari semua sel di dalam rectdan kemudian memanggil fungsi pertama untuk setiap jalur indeks untuk mengambil bingkai yang benar:

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()Fungsi ini hanya membuat salinan dalam dari semua atribut tata letak dalam larik. Anda dapat melihat kode sumber untuk implementasinya.)

Jadi sekarang satu-satunya hal yang harus kita lakukan adalah mengimplementasikan layoutAttributesForItem(at indexPath: IndexPath)fungsi dengan benar. Kelas super UICollectionViewFlowLayoutsudah menempatkan jumlah sel yang benar di setiap baris jadi kita hanya perlu menggesernya ke kiri dalam baris masing-masing. Kesulitannya terletak pada menghitung jumlah ruang yang kita butuhkan untuk menggeser setiap sel ke kiri.

Karena kita ingin memiliki jarak tetap antara sel, ide intinya adalah dengan menganggap bahwa sel sebelumnya (sel kiri sel yang saat ini diletakkan) sudah diposisikan dengan benar. Kemudian kita hanya perlu menambahkan jarak sel ke maxXnilai bingkai sel sebelumnya dan itulah origin.xnilai untuk bingkai sel saat ini.

Sekarang kita hanya perlu mengetahui kapan kita telah mencapai awal baris, sehingga kita tidak menyelaraskan sel di sebelah sel pada baris sebelumnya. (Ini tidak hanya akan menghasilkan tata letak yang salah tetapi juga akan sangat lamban.) Jadi kita perlu memiliki jangkar rekursi. Pendekatan yang saya gunakan untuk menemukan jangkar rekursi adalah sebagai berikut:

Untuk mengetahui apakah sel pada indeks i berada pada baris yang sama dengan sel dengan indeks i-1 ...

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

... Saya "menggambar" persegi panjang di sekitar sel saat ini dan merentangkannya di atas lebar tampilan keseluruhan koleksi. Sebagai UICollectionViewFlowLayoutpusat, semua sel secara vertikal, setiap sel dalam garis yang sama harus berpotongan dengan persegi panjang ini.

Jadi, saya cukup memeriksa apakah sel dengan indeks i-1 berpotongan dengan persegi panjang garis ini yang dibuat dari sel dengan indeks i .

  • Jika berpotongan, sel dengan indeks i bukanlah sel paling kiri dalam baris.
    → Dapatkan bingkai sel sebelumnya (dengan indeks i − 1 ) dan pindahkan sel saat ini ke sebelahnya.

  • Jika tidak berpotongan, sel dengan indeks i adalah sel paling kiri dalam baris.
    → Pindahkan sel ke tepi kiri tampilan koleksi (tanpa mengubah posisi vertikalnya).

Saya tidak akan memposting implementasi sebenarnya dari layoutAttributesForItem(at indexPath: IndexPath)fungsi di sini karena menurut saya bagian yang paling penting adalah memahami idenya dan Anda selalu dapat memeriksa penerapan saya di kode sumber . (Ini sedikit lebih rumit daripada yang dijelaskan di sini karena saya juga mengizinkan .rightperataan dan berbagai opsi perataan vertikal. Namun, ini mengikuti ide yang sama.)


Wow, saya rasa ini adalah jawaban terpanjang yang pernah saya tulis di Stackoverflow. Saya harap ini membantu. 😉


Saya menerapkan skenario ini stackoverflow.com/questions/56349052/… tetapi tidak berhasil. maukah Anda memberi tahu saya bagaimana saya bisa mencapai ini
Nazmul Hasan

Bahkan dengan ini (yang merupakan proyek yang sangat mengagumkan) saya mengalami flashing dalam tampilan koleksi scrolling vertikal aplikasi saya. Saya memiliki 20 sel, dan pada gulungan pertama, sel pertama kosong dan semua sel bergeser satu posisi ke kanan. Setelah satu gulir penuh ke atas dan ke bawah, semuanya sejajar dengan benar.
Parth Tamane

Proyek luar biasa! Akan sangat bagus untuk melihatnya diaktifkan untuk Kartago.
pjuzeliunas

30

Dengan Swift 4.1 dan iOS 11, sesuai kebutuhan Anda, Anda dapat memilih salah satu dari 2 implementasi lengkap berikut untuk memperbaiki masalah Anda.


# 1. Meninggalkan autoresizing menyelaraskan UICollectionViewCells

Pelaksanaan di bawah ini menunjukkan bagaimana menggunakan UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSizedan UILabel's preferredMaxLayoutWidthuntuk sel menyelaraskan autoresizing kiri dalam 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")
    }

}

Hasil yang diharapkan:

masukkan deskripsi gambar di sini


# 2. Rata kiri UICollectionViewCelldengan ukuran tetap

Implementasi di bawah ini menunjukkan bagaimana menggunakan UICollectionViewLayout's layoutAttributesForElements(in:)dan UICollectionViewFlowLayout' itemSizeuntuk menyelaraskan kiri sel dengan ukuran yang telah ditentukan dalam 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")
    }

}

Hasil yang diharapkan:

masukkan deskripsi gambar di sini


Saat mengelompokkan atribut sel berdasarkan baris, apakah penggunaan 10nomor arbitrer?
amariduran

27

Solusi sederhana di tahun 2019

Ini adalah salah satu pertanyaan menyedihkan di mana banyak hal telah berubah selama bertahun-tahun. Sekarang mudah.

Pada dasarnya Anda hanya melakukan ini:

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

Yang Anda butuhkan sekarang adalah kode boilerplate:

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
}

Cukup salin dan tempel ke dalam UICollectionViewFlowLayout- Anda sudah selesai.

Solusi kerja lengkap untuk menyalin dan menempel:

Ini semuanya:

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
    }
}

masukkan deskripsi gambar di sini

Dan akhirnya...

Terima kasih kepada @AlexShubin di atas yang pertama kali mengklarifikasi ini!


Hei. Aku sedikit bingung. Menambahkan spasi untuk setiap string di awal dan akhir masih menunjukkan kata saya di sel item tanpa spasi yang telah saya tambahkan. Ada ide bagaimana melakukannya?
Mohamed Lee

1
Jika Anda memiliki header bagian dalam tampilan koleksi Anda, ini tampaknya mematikannya.
TylerJames

22

Pertanyaannya sudah lama tapi tidak ada jawaban dan itu pertanyaan yang bagus. Jawabannya adalah dengan mengganti satu metode dalam subkelas UICollectionViewFlowLayout:

@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

Seperti yang direkomendasikan oleh Apple, Anda mendapatkan atribut tata letak dari super dan mengulanginya. Jika itu yang pertama di baris (ditentukan oleh origin.x berada di margin kiri), Anda membiarkannya dan mengatur ulang x ke nol. Kemudian untuk sel pertama dan setiap sel, Anda menambahkan lebar sel tersebut ditambah beberapa margin. Ini diteruskan ke item berikutnya dalam loop. Jika ini bukan item pertama, Anda menyetelnya origin.x ke margin terhitung yang berjalan, dan menambahkan elemen baru ke larik.


Saya pikir ini jauh lebih baik daripada solusi Chris Wagner ( stackoverflow.com/a/15554667/482557 ) pada pertanyaan tertaut — implementasi Anda tidak memiliki panggilan UICollectionViewLayout yang berulang.
Evan R

1
Sepertinya ini tidak berfungsi jika baris memiliki satu item karena UICollectionViewFlowLayout memusatkannya. Anda dapat memeriksa bagian tengah untuk menyelesaikan masalah. Sesuatu sepertiattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos

Saya menerapkan ini, tetapi untuk beberapa alasan rusak untuk bagian pertama: Sel pertama muncul di sebelah kiri baris pertama, dan sel kedua (yang tidak muat di baris yang sama) pergi ke baris berikutnya, tetapi tidak kiri selaras. Sebaliknya, posisi x dimulai dari sel pertama berakhir.
Nicolas Miari

@NicolasMiari Perulangan memutuskan itu baris baru dan mereset leftMargindengan if (attributes.frame.origin.x == self.sectionInset.left) {cek. Ini mengasumsikan bahwa tata letak default telah menyelaraskan sel baris baru menjadi rata dengan sectionInset.left. Jika tidak demikian, perilaku yang Anda gambarkan akan terjadi. Saya akan memasukkan breakpoint di dalam loop dan memeriksa kedua sisi sel baris kedua tepat sebelum perbandingan. Semoga berhasil.
Mike Sand

3
Terima kasih ... Saya akhirnya menggunakan UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Ini menggantikan beberapa metode lainnya.
Nicolas Miari

9

Saya memiliki masalah yang sama, Cobalah Cocoapod UICollectionViewLeftAlignedLayout . Cukup sertakan dalam proyek Anda dan lakukan inisialisasi seperti ini:

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

Tidak ada tanda kurung siku pembuka dalam inisialisasi tata letak Anda di sana
RyanOfCourse

Saya menguji dua jawaban github.com/eroth/ERJustifiedFlowLayout dan UICollectionViewLeftAlignedLayout. Hanya yang kedua menghasilkan hasil yang benar saat menggunakan estimItemSize (self-sizing).
EckhardN

6

Inilah jawaban asli di Swift. Sebagian besar masih berfungsi dengan baik.

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
    }
}

Pengecualian: Mengotomatiskan Sel

Sayangnya, ada satu pengecualian besar. Jika Anda menggunakan UICollectionViewFlowLayout's estimatedItemSize. Secara internal UICollectionViewFlowLayoutmengubah sedikit. Saya belum melacaknya sepenuhnya tetapi jelas memanggil metode lain setelah layoutAttributesForElementsInRectsaat mengukur sel sendiri. Dari trial and error saya, saya menemukan tampaknya memanggil layoutAttributesForItemAtIndexPathsetiap sel secara individual selama autosizing lebih sering. Pembaruan ini LeftAlignedFlowLayoutberfungsi baik dengan estimatedItemSize. Ini berfungsi dengan sel berukuran statis juga, namun panggilan tata letak tambahan mengarahkan saya untuk menggunakan jawaban asli kapan saja saya tidak memerlukan sel ukuran otomatis.

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
    }
}

Ini berfungsi untuk bagian pertama namun label tidak mengubah ukuran pada bagian kedua hingga tabel bergulir. Tampilan koleksi saya ada di sel tabel.
EI Captain v2.0

5

Berdasarkan jawaban Michael Sand , saya membuat UICollectionViewFlowLayoutperpustakaan subclass untuk melakukan justifikasi horizontal kiri, kanan, atau penuh (pada dasarnya default) —ini juga memungkinkan Anda menyetel jarak absolut antara setiap sel. Saya berencana menambahkan justifikasi tengah horizontal dan justifikasi vertikal juga.

https://github.com/eroth/ERJustifiedFlowLayout


5

Dengan cepat. Menurut jawaban Michaels

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

Berdasarkan semua jawaban, saya berubah sedikit dan itu bekerja dengan baik untuk saya

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

Jika ada di antara Anda yang menghadapi masalah - beberapa sel di sebelah kanan tampilan koleksi melebihi batas tampilan koleksi . Kalau begitu tolong gunakan ini -

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
    }
}

Gunakan INT sebagai ganti membandingkan nilai CGFloat .


3

Berdasarkan jawaban di sini, tetapi perbaiki masalah mogok dan penyelarasan saat tampilan koleksi Anda juga memiliki header atau footer. Menyejajarkan sel hanya kiri:

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

Terima kasih atas jawaban Michael Sand . Saya memodifikasinya menjadi solusi beberapa baris (perataan yang sama Atas y dari setiap baris) yaitu Left Alignment, bahkan memberi jarak ke setiap item.

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

Inilah perjalanan saya untuk menemukan kode terbaik yang bekerja dengan Swift 5. Saya telah bergabung dengan beberapa jawaban dari utas ini dan beberapa utas lainnya untuk memecahkan peringatan dan masalah yang saya hadapi. Saya mendapat peringatan dan beberapa perilaku tidak normal saat menggulir melalui tampilan koleksi saya. Konsol mencetak yang berikut ini:

Hal ini mungkin terjadi karena tata letak aliran "xyz" mengubah atribut yang dikembalikan oleh UICollectionViewFlowLayout tanpa menyalinnya.

Masalah lain yang saya hadapi adalah beberapa sel yang panjang dipotong di sisi kanan layar. Juga, saya mengatur insets bagian dan minimumInteritemSpacing di fungsi delegasi yang menghasilkan nilai tidak tercermin di kelas kustom. Perbaikan untuk itu adalah menyetel atribut tersebut ke instance tata letak sebelum menerapkannya ke tampilan koleksi saya.

Inilah cara saya menggunakan tata letak untuk tampilan koleksi saya:

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)

Berikut kelas tata letak aliran

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
    }
}

Anda menyelamatkan hari saya
David 'mArm' Ansermot

0

Masalah dengan UICollectionView adalah mencoba menyesuaikan sel secara otomatis di area yang tersedia. Saya telah melakukan ini dengan terlebih dahulu menentukan jumlah baris dan kolom dan kemudian menentukan ukuran sel untuk baris dan kolom itu

1) Untuk menentukan Bagian (Baris) dari UICollectionView saya:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Untuk menentukan jumlah item dalam suatu bagian. Anda dapat menentukan jumlah item yang berbeda untuk setiap bagian. Anda bisa mendapatkan nomor bagian menggunakan parameter 'bagian'.

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

3) Untuk menentukan ukuran sel untuk setiap bagian dan baris secara terpisah. Anda bisa mendapatkan nomor bagian dan nomor baris menggunakan parameter 'indexPath' yaitu [indexPath section]untuk nomor bagian dan [indexPath row]nomor baris.

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

4) Kemudian Anda dapat menampilkan sel Anda dalam baris dan bagian menggunakan:

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

CATATAN: Di UICollectionView

Section == Row
IndexPath.Row == Column

0

Jawaban Mike Sand bagus tetapi saya mengalami beberapa masalah dengan kode ini (Seperti sel yang panjang terpotong). Dan kode baru:

#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

Diedit jawaban Angel García Olloqui tentang rasa hormat minimumInteritemSpacingdari delegasi collectionView(_:layout:minimumInteritemSpacingForSectionAt:), jika itu mengimplementasikannya.

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

Kode di atas berfungsi untuk saya. Saya ingin membagikan kode Swift 3.0 masing-masing.

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
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.