UICollectionView'de Hücreleri Sola Hizala


100

Bir satırda farklı genişliklerde birden çok hücre olduğu projemde bir UICollectionView kullanıyorum. Şuna göre: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

hücreleri eşit dolgu ile çizgi boyunca yayar. Bu beklendiği gibi olur, ancak onları sola yaslamak ve bir dolgu genişliğini sabit kodlamak istemem dışında.

UICollectionViewFlowLayout alt sınıfına ihtiyacım olduğunu anlıyorum, ancak bazı öğreticileri vb. Çevrimiçi okuduktan sonra bunun nasıl çalıştığını anlamıyorum.


Yanıtlar:


177

Bu iş parçacığındaki diğer çözümler, satır yalnızca 1 maddeden oluştuğunda veya aşırı karmaşık olduğunda düzgün çalışmaz.

Ryan tarafından verilen örneğe dayanarak, yeni elemanın Y konumunu inceleyerek kodu yeni bir satırı algılayacak şekilde değiştirdim. Performansta çok basit ve hızlı.

Swift:

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

Ek görünümlerin boyutlarını korumasını istiyorsanız, forEachgörüşmedeki kapanışın en üstüne aşağıdakileri ekleyin :

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

Amaç-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
Teşekkürler! let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
Dizideki

2
Benim yaptığım gibi merkeze hizalanmış bir versiyon arayan buna rastlamış olabilecek herkes için, Angel'ın yanıtının değiştirilmiş bir versiyonunu burada yayınladım: stackoverflow.com/a/38254368/2845237
Alex Koshy

1
Bu çözümü denedim, ancak düzeni Arayüz Oluşturucu Yardımcı Programları panelindeki collectionView ile ilişkilendiremedi. Kodla yapmak bitti. Herhangi bir ipucu neden?
acrespo

1
Bunu soldan sağa sistemlerde sola, sağdan sola sistemlerde sağa hizalamayı nasıl yaparsınız?
Rick

1
Kullanım için bunu yapın self.collectionView.collectionViewLayout = LeftAlignedCollectionViewFlowLayout ()
blackops

65

Bu sorunun cevaplarında birçok harika fikir var. Ancak çoğunun bazı dezavantajları vardır:

  • Hücrenin y değerini kontrol etmeyen çözümler yalnızca tek satırlı düzenler için çalışır . Birden çok satır içeren koleksiyon görünümü düzenlerinde başarısız olurlar.
  • Çözümler yapmak kontrol y gibi değerini Melek García Olloqui cevabı tüm hücreler aynı yüksekliğe sahip olursa, sadece işin . Değişken yüksekliğe sahip hücreler için başarısız olurlar.
  • Çoğu çözüm yalnızca layoutAttributesForElements(in rect: CGRect)işlevi geçersiz kılar . Geçersiz kılmazlar layoutAttributesForItem(at indexPath: IndexPath). Bu bir sorundur çünkü koleksiyon görünümü, belirli bir dizin yolunun düzen özniteliklerini almak için ikinci işlevi düzenli olarak çağırır. Bu işlevden uygun öznitelikleri döndürmezseniz, büyük olasılıkla her türlü görsel hatayla karşılaşabilirsiniz, örneğin hücrelerin ekleme ve silme animasyonları sırasında veya koleksiyon görünümü düzenlerini ayarlayarak kendi boyutlarını belirleyen hücreleri kullanırken estimatedItemSize. Elma dokümanlar durumu:

    Her özel düzen nesnesinin layoutAttributesForItemAtIndexPath:yöntemi uygulaması beklenir .

  • Birçok çözüm rect, layoutAttributesForElements(in rect: CGRect)işleve aktarılan parametre hakkında da varsayımlarda bulunur . Örneğin, çoğu, recther zaman yeni bir satırın başlangıcında başladığı varsayımına dayanır ki bu zorunlu değildir.

Yani başka bir deyişle:

Bu sayfada önerilen çözümlerin çoğu bazı özel uygulamalar için çalışır, ancak her durumda beklendiği gibi çalışmazlar.


AlignedCollectionViewFlowLayout

Bu sorunları ele almak için, benzer bir soruya verdikleri cevaplarda matt ve Chris Wagner'inUICollectionViewFlowLayout önerdiği gibi benzer bir fikri izleyen bir alt sınıf oluşturdum . Ya hücreleri hizalayabilir

⬅︎ sol :

Sola hizalı düzen

veya ➡︎ doğru :

Sağa hizalı düzen

ve ayrıca hücreleri ilgili satırlarında dikey olarak hizalamak için seçenekler sunar (yüksekliklerinin değişmesi durumunda).

Buradan kolayca indirebilirsiniz:

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

Kullanım basittir ve BENİOKU dosyasında açıklanmıştır. Temel olarak bir örneğini oluşturursunuz AlignedCollectionViewFlowLayout, istediğiniz hizalamayı belirtir ve bunu koleksiyon görünümünüzün collectionViewLayoutözelliğine atarsınız :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Ayrıca Cocoapod'larda da mevcuttur .)


Nasıl çalışır (sola hizalı hücreler için):

Buradaki kavram, yalnızca layoutAttributesForItem(at indexPath: IndexPath)işleve güvenmektir . İçinde, layoutAttributesForElements(in rect: CGRect)içindeki tüm hücrelerin dizin yollarını alıyoruz rectve ardından doğru çerçeveleri almak için her dizin yolu için ilk işlevi çağırıyoruz:

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()İşlev, dizideki tüm düzen özniteliklerinin derin bir kopyasını oluşturur. Uygulanması için kaynak koduna bakabilirsiniz .)

Yani şimdi yapmamız gereken tek şey layoutAttributesForItem(at indexPath: IndexPath)işlevi düzgün bir şekilde uygulamak . Süper sınıf UICollectionViewFlowLayoutzaten her satıra doğru sayıda hücre koyar, bu yüzden onları yalnızca ilgili satırda sola kaydırmamız gerekir. Zorluk, her bir hücreyi sola kaydırmak için ihtiyacımız olan alan miktarını hesaplamakta yatıyor.

Hücreler arasında sabit bir boşluk olmasını istediğimizden, temel fikir, önceki hücrenin (şu anda yerleştirilmiş hücrenin solundaki hücre) zaten düzgün bir şekilde konumlandırılmış olduğunu varsaymaktır. O zaman sadece hücre aralığını maxXönceki hücrenin çerçevesinin değerine eklememiz gerekir ve bu origin.x, geçerli hücrenin çerçevesinin değeridir.

Şimdi sadece satırın başına ne zaman ulaştığımızı bilmemiz gerekiyor, böylece bir hücreyi önceki satırdaki bir hücrenin yanına hizalamayız. (Bu sadece yanlış bir düzen ile sonuçlanmayacak, aynı zamanda aşırı derecede gecikmeli olacaktır.) Bu yüzden bir özyineleme çapasına ihtiyacımız var. Özyineleme çapasını bulmak için kullandığım yaklaşım şudur:

İ dizinindeki hücrenin, i-1 dizinli hücreyle aynı satırda olup olmadığını bulmak için ...

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

... Geçerli hücrenin etrafına bir dikdörtgen "çiziyorum" ve tüm koleksiyon görünümünün genişliği boyunca uzatıyorum. Olarak UICollectionViewFlowLayoutmerkezleri tüm hücreler dikey olarak aynı hat her hücre gerekir , bu dikdörtgen ile kesişir.

Böylece, basit bir kontrol halinde göstergesi hücre I-1 endeksi ile hücreden oluşturulan bu hat dikdörtgenle kesişen i .

  • Kesişirse, i indisli hücre satırdaki en soldaki hücre değildir.
    → Önceki hücrenin çerçevesini alın ( i − 1 indeksi ile ) ve yanındaki geçerli hücreyi hareket ettirin.

  • Kesişmezse, i indisli hücre satırda en soldaki hücredir.
    → Hücreyi koleksiyon görünümünün sol kenarına taşıyın (dikey konumunu değiştirmeden).

layoutAttributesForItem(at indexPath: IndexPath)Fonksiyonun gerçek uygulamasını burada yayınlamayacağım çünkü en önemli kısmın fikri anlamak olduğunu ve uyguladığımı her zaman kaynak kodda kontrol edebileceğinizi düşünüyorum . (Burada .rightanlatılandan biraz daha karmaşıktır çünkü hizalama ve çeşitli dikey hizalama seçeneklerine de izin veriyorum . Ancak, aynı fikri takip ediyor.)


Vay canına, sanırım bu Stackoverflow'da yazdığım en uzun cevap. Umarım bu yardımcı olur. 😉


Bu senaryo ile başvuruyorum stackoverflow.com/questions/56349052/… ama çalışmıyor. bunu nasıl başarabilirim anlatır mısın
Nazmul Hasan

Bununla bile (bu gerçekten harika bir proje) uygulamalarımın dikey kaydırma koleksiyon görünümünde yanıp sönme deneyimi yaşıyorum. 20 hücrem var ve ilk kaydırmada, ilk hücre boş ve tüm hücreler bir konum sağa kaydırılmış. Yukarı ve aşağı bir tam kaydırmadan sonra, işler düzgün şekilde hizalanır.
Parth Tamane

Harika proje! Kartaca için etkinleştirildiğini görmek harika olurdu.
pjuzeliunas

30

Swift 4.1 ve iOS 11 ile ihtiyaçlarınıza göre , probleminizi çözmek için aşağıdaki 2 tam uygulamadan birini seçebilirsiniz .


# 1. Sola hizala otomatik yeniden boyutlandırma UICollectionViewCells

Aşağıdaki uygulama , a'daki otomatik yeniden boyutlandırma hücrelerini sola hizalamak için UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' estimatedItemSizeve UILabel'nin nasıl kullanılacağını gösterir :preferredMaxLayoutWidthUICollectionView

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

}

Beklenen Sonuç:

görüntü açıklamasını buraya girin


# 2. UICollectionViewCellSabit boyutta sola hizala

Aşağıdaki uygulama, hücreleri önceden tanımlanmış boyutta bir a'da sol hizalamak için UICollectionViewLayouts layoutAttributesForElements(in:)ve UICollectionViewFlowLayout'lerin nasıl kullanılacağını gösterir :itemSizeUICollectionView

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

}

Beklenen Sonuç:

görüntü açıklamasını buraya girin


Hücre niteliklerini satıra göre gruplandırırken, 10rastgele bir sayı mı kullanılır?
amariduran

28

2019'da basit çözüm

Bu, yıllar içinde işlerin çok değiştiği iç karartıcı sorulardan biri. Artık çok kolay.

Temelde sadece şunu yaparsınız:

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

Şimdi ihtiyacınız olan tek şey, standart kod:

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
}

Basitçe kopyalayıp bir dosyaya yapıştırın UICollectionViewFlowLayout- işiniz bitti.

Kopyalamak ve yapıştırmak için tam çalışma çözümü:

Hepsi bu:

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

görüntü açıklamasını buraya girin

Ve sonunda...

Bunu ilk açıklayan yukarıdaki @AlexShubin'e teşekkür edin!


Hey. Biraz kafam karıştı. Başlangıçta ve bitişte her dize için boşluk eklemek, eklediğim boşluk olmadan öğe hücresindeki kelimemi göstermeye devam ediyor. Nasıl yapılacağına dair bir fikrin var mı?
Mohamed Lee

1
Koleksiyon görünümünüzde bölüm başlıkları varsa, bu onları öldürüyor gibi görünüyor.
TylerJames

22

Bir süredir soru cevaplanmadı ve bu iyi bir soru. Cevap, UICollectionViewFlowLayout alt sınıfındaki bir yöntemi geçersiz kılmaktır:

@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 tarafından önerildiği gibi, düzen özniteliklerini super'den alır ve bunların üzerinde yinelersiniz. Satırdaki ilkse (origin.x'in sol kenar boşluğunda olmasıyla tanımlanır), onu yalnız bırakır ve x'i sıfıra sıfırlarsınız. Daha sonra, ilk hücre ve her hücre için, bu hücrenin genişliğini artı bir miktar kenar boşluğu eklersiniz. Bu, döngüdeki bir sonraki öğeye aktarılır. İlk öğe değilse, origin.x öğesini çalışan hesaplanan marj olarak ayarlarsınız ve diziye yeni öğeler eklersiniz.


Bunun Chris Wagner'in bağlantılı sorudaki çözümünden ( stackoverflow.com/a/15554667/482557 ) çok daha iyi olduğunu düşünüyorum - uygulamanızda sıfır tekrarlayan UICollectionViewLayout çağrıları var.
Evan R

1
UICollectionViewFlowLayout satırın tek bir öğesi varsa bu işe yarıyor gibi görünmüyor çünkü UICollectionViewFlowLayout onu ortalar. Sorunu çözmek için merkezi kontrol edebilirsiniz. Bir şey gibiattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos

Bunu uyguladım, ancak nedense ilk bölüm için kırılıyor: İlk hücre ilk satırın solunda görünüyor ve ikinci hücre (aynı satıra sığmayan) bir sonraki satıra gidiyor, ancak sola gitmiyor hizalı. Bunun yerine, x konumu ilk hücrenin bittiği yerde başlar.
Nicolas Miari

@NicolasMiari Döngü, bunun yeni bir satır olduğuna karar verir leftMarginve if (attributes.frame.origin.x == self.sectionInset.left) {onay ile sıfırlar . Bu, varsayılan düzenin yeni satırın hücresini sectionInset.left. Eğer durum böyle değilse, tarif ettiğiniz davranış ortaya çıkacaktır. Döngünün içine bir kesme noktası ekler ve karşılaştırmadan hemen önce ikinci satırın hücresi için her iki tarafı da kontrol ederdim. İyi şanslar.
Mike Sand

3
Teşekkürler ... UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout'u kullandım . Birkaç yöntemi daha geçersiz kılar.
Nicolas Miari

9

Aynı sorunu yaşadım, Cocoapod UICollectionViewLeftAlignedLayout'u deneyin. Sadece projenize dahil edin ve şu şekilde başlatın:

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


İki yanıtı github.com/eroth/ERJustifiedFlowLayout ve UICollectionViewLeftAlignedLayout test ettim. TahminiItemSize (kendinden boyutlandırma) kullanılırken yalnızca ikincisi doğru sonucu verdi.
EckhardN

6

İşte Swift'in orijinal cevabı. Hala çoğunlukla harika çalışıyor.

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

İstisna: Hücreleri Otomatik Boyutlandırma

Maalesef büyük bir istisna var. UICollectionViewFlowLayout'S kullanıyorsanız estimatedItemSize. İçsel UICollectionViewFlowLayoutolarak işleri biraz değiştiriyor. Tamamen takip etmedim, ancak layoutAttributesForElementsInRecthücreleri kendi kendine boyutlandırırken diğer yöntemleri çağırdığını açıkladı . Deneme layoutAttributesForItemAtIndexPathyanılmamdan, otomatik boyutlandırma sırasında her hücreyi ayrı ayrı daha sık aradığını buldum. Bu güncelleme LeftAlignedFlowLayoutile harika çalışıyor estimatedItemSize. Statik boyutlu hücrelerle de çalışıyor, ancak ekstra düzen çağrıları, hücreleri otomatikleştirmeye ihtiyacım olmadığında orijinal yanıtı kullanmamı sağlıyor.

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

Bu ilk bölüm için işe yarar, ancak tablo kaydırılıncaya kadar ikinci bölümde etiket yeniden boyutlandırılmaz. Koleksiyon görünümüm tablo hücresinde.
EI Kaptan v2.0


5

Hızlı. Michaels cevabına göre

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
}

5

Herhangi biriniz sorunla karşılaşırsa - koleksiyon görünümünün sağındaki bazı hücreler, koleksiyon görünümünün sınırlarını aşıyor . O zaman lütfen bunu kullanın -

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 değerlerini karşılaştırmak yerine INT kullanın .


4

Tüm cevaplara göre biraz değişiyorum ve benim için iyi çalışıyor

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
}

3

Buradaki yanıtlara dayalıdır, ancak koleksiyon görünümünüzde üstbilgi veya altbilgi olduğunda düzeltilen kilitlenmeler ve hizalama sorunları. Yalnızca sola hizalama:

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'in cevabı için teşekkürler . Bunu birden çok satırlık bir çözüme (her satırın aynı hizalama Üst y'si) Sola Hizalama, hatta her öğeye boşluk bırakarak değiştirdim.

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

İşte Swift 5 ile çalışan en iyi kodu bulma yolculuğum. Bu konudaki birkaç yanıta ve karşılaştığım uyarıları ve sorunları çözmek için diğer bazı konuları birleştirdim. Koleksiyon görünümümde gezinirken bir uyarı aldım ve bazı anormal davranışlar aldım. Konsol aşağıdakileri yazdırır:

Bu, büyük olasılıkla "xyz" akış düzeni UICollectionViewFlowLayout tarafından kopyalanmadan döndürülen öznitelikleri değiştirdiği için meydana gelir.

Karşılaştığım bir başka sorun da, bazı uzun hücrelerin ekranın sağ tarafında kırpılması. Ayrıca, delege işlevlerinde bölüm eklemelerini ayarlıyordum ve minimumInteritemSpacing, değerlerin özel sınıfa yansıtılmamasıyla sonuçlandı. Bunun düzeltmesi, koleksiyon görünümüme uygulamadan önce bu öznitelikleri mizanpajın bir örneğine ayarlamaktı.

Koleksiyon görünümüm için düzeni şu şekilde kullandım:

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)

İşte akış düzeni sınıfı

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


1

Minimum dağıtım hedef iOS 13 ise, ben son derece yararlanmak önermek Bileşim Düzeni (doc burada , WWDC sunum burada ).

Başlangıçta buradaki en iyi cevaplardan bazılarını denedim. Maalesef, bazı hücrelerin aralıklı olarak kaybolma eğiliminde olduğu bir sorunla karşılaştık. Bizim için bu, UICollectionView reloadDataişlevini çağırdıktan sonra olur . Hücrelerimizin değişken genişliğe, yani otomatik boyutlandırmaya sahip olduğuna dikkat etmek de önemlidir.

Size bir örnek göstereyim. Diyelim ki, anahtar kelime balonları listesi içeren bir sayfa görüntülememiz gerekiyor.

görüntü açıklamasını buraya girin

İşte bunu Kompozisyon Düzeni kullanarak başarmak için ihtiyacınız olabilecek şeyler.

override func viewDidLoad() {
  super.viewDidLoad()
  ...
  collectionView.collectionViewLayout = createLeftAlignedLayout()
}

private func createLeftAlignedLayout() -> UICollectionViewLayout {
  let item = NSCollectionLayoutItem(          // this is your cell
    layoutSize: NSCollectionLayoutSize(
      widthDimension: .estimated(40),         // variable width
      heightDimension: .absolute(48)          // fixed height
    )
  )
  
  let group = NSCollectionLayoutGroup.horizontal(
    layoutSize: .init(
      widthDimension: .fractionalWidth(1.0),  // 100% width as inset by its Section
      heightDimension: .estimated(50)         // variable height; allows for multiple rows of items
    ),
    subitems: [item]
  )
  group.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
  group.interItemSpacing = .fixed(10)         // horizontal spacing between cells
  
  return UICollectionViewCompositionalLayout(section: .init(group: group))
}

Gördüğünüz gibi, çok basit.


0

UICollectionView ile ilgili sorun, hücreleri mevcut alana otomatik olarak sığdırmaya çalışmasıdır. Bunu önce satır ve sütun sayısını tanımlayarak ve ardından bu satır ve sütun için hücre boyutunu tanımlayarak yaptım.

1) UICollectionView'umun Bölümlerini (Satırlarını) tanımlamak için:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Bir bölümdeki öğe sayısını tanımlamak için. Her bölüm için farklı sayıda öğe tanımlayabilirsiniz. 'bölüm' parametresini kullanarak bölüm numarasını alabilirsiniz.

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

3) Hücre boyutunu her bölüm ve satır için ayrı ayrı tanımlamak. "İndexPath" parametresini, yani bölüm numarası ve satır numarası için kullanarak [indexPath section]bölüm numarasını ve [indexPath row]satır numarasını alabilirsiniz.

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

4) Ardından, hücrelerinizi aşağıdakileri kullanarak satırlar ve bölümler halinde görüntüleyebilirsiniz:

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

NOT: UICollectionView'da

Section == Row
IndexPath.Row == Column

0

Mike Sand'in cevabı güzel ancak bu kodla ilgili bazı sorunlar yaşadım (Uzun hücre kesilmiş gibi). Ve yeni kod:

#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

Angel García Olloqui'nin , eğer uygularsa, minimumInteritemSpacingdelegenin saygısına cevabını düzenledi 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

Yukarıdaki kod benim için çalışıyor. İlgili Swift 3.0 kodunu paylaşmak istiyorum.

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

0

Tüm cevaplara göre. LeftToRight ve rightToLeft için çalışır

class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

        let ltr = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight
        var leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.y >= maxY
            {
                leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
            }

            layoutAttribute.frame.origin.x = leftMargin - (ltr ? 0 : layoutAttribute.frame.width)

            if (ltr)
            {
                leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            }
            else
            {
                leftMargin -= layoutAttribute.frame.width + minimumInteritemSpacing
            }
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.