๐Ÿ“ฑiOS

CollectionView Custom Layout

akwlak 2022. 12. 5. 22:40

CollectionView๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋ฉด, Flow๋‚˜ Compositional๋กœ๋Š” ๊ตฌํ˜„ํ•˜๊ธฐ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค์–ด์•ผ ํ•˜๊ฑฐ๋‚˜, 

 

ํ˜น์€ ์—ฌ๋Ÿฌ ๋ฐฉํ–ฅ์œผ๋กœ ์Šคํฌ๋กคํ•ด์•ผ ํ•˜๋Š” ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค์–ด์•ผ ํ•  ๋•Œ๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋Ÿด ๋•Œ๋Š” Layout์„ Custom ํ•˜๋Š” ๊ฒŒ ๋” ์‰ฝ๊ฑฐ๋‚˜ ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์ธ๋ฐ์š”,

 

UICollectionViewLayout์„ Subclassingํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋ฉด ์šฐ๋ฆฌ๋Š” ์ด ์•ˆ์—์„œ ๋‘๊ฐ€์ง€ ์ž‘์—…์„ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

 

1. ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ content ์˜์—ญ ์ง€์ •ํ•˜๊ธฐ

 

2. ์…€๊ณผ ๋ทฐ๋“ค์— ๋Œ€ํ•œ attribute objects ์ œ๊ณตํ•˜๊ธฐ

 

 

attribute object๋Š” ์…€์ด๋‚˜ ๋ณด์ถฉ ๋ทฐ์˜ ๋ ˆ์ด์•„์›ƒ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ํ•„์š”ํ•œ ์†์„ฑ๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

frame, bounds, center, alpha ๋“ฑ

 

์ด๋ฅผ ์ด์šฉํ•ด์„œ ์…€์ด๋‚˜ ๋ณด์ถฉ ๋ทฐ๋ฅผ ๊ทธ๋ฆฌ๋Š” ๊ฒƒ์ด์ฃ .

 

๊ทธ๋Ÿฌ๋ฉด ์ด๋Ÿฐ ์ž‘์—…๋“ค์„ ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?

 

ํŠน์ • ๋ฉ”์„œ๋“œ๋“ค์„ overrideํ•ด์„œ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

1. prepare()

์—ฌ๊ธฐ์„œ ๋ ˆ์ด์•„์›ƒ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ์„ ํ–‰ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

2. collectionViewContentSize

์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ์˜์—ญ์„ return ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์œ„ prepare์—์„œ ์ˆ˜ํ–‰ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

3. layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

์—ฌ๊ธฐ์„œ ํŠน์ • rect์•ˆ์— ์žˆ์–ด์•ผ ํ•˜๋Š” ์…€๋“ค์ด๋‚˜ ๋ณด์ถฉ ๋ทฐ์— ๋Œ€ํ•œ attributes๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ด rect๊ฐ€ ๊ผญ ํ™”๋ฉด์— ๋ณด์ด๋Š” rect๋Š” ์•„๋‹™๋‹ˆ๋‹ค.

 

CollectionView๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ž‘์—…์„ ํ•  ๋•Œ ์œ„ ๋ฉ”์„œ๋“œ๋“ค์„ ํ˜ธ์ถœํ•˜๊ณ  ์ˆœ์„œ๋„ ์ง€์ผœ์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆผ์œผ๋กœ ๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

 

์ด๋ ‡๊ฒŒ๋งŒ ๋ณด๋‹ˆ ์ž˜ ๊ฐ์ด ์•ˆ ์žกํžˆ๋Š” ๊ฒƒ ๊ฐ™๋„ค์š”.

 

์ง์ ‘ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค๋ฉด์„œ ์ดํ•ดํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๋ณต์žกํ•ฉ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด์ง€๋งŒ, ์ดํ•ด๋ฅผ ์œ„ํ•ด ์‰ฌ์šด ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

๊ฒฐ๊ณผ

 

์ฝ”๋“œ๋Š” Layout๊ณผ DataSource์— ์ง‘์ค‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์šฐ์„  DataSource๋ฅผ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

 

final class CustomDataSource: NSObject, UICollectionViewDataSource {
    
    // ๋ฐ์ดํ„ฐ
    private var messages: [Message] = []
    
    // ๋ฐ์ดํ„ฐ๊ฐ€ ์œ„์น˜ํ•  offset๋“ค
    private(set) var offsets: [CGFloat] = []
    
    // ๊ฐ ์…€์ด ๊ฐ€์งˆ ๋†’์ด, ๋ฐ์ดํ„ฐ๋กœ๋ถ€ํ„ฐ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ
    var heights: [CGFloat] {
        return messages.map { $0.height }
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return messages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let message = messages[indexPath.item]
        
        return MessageCell.dequeue(from: collectionView, at: indexPath, with: message)
    }
    
    // ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€, offset๋“ค์„ ๊ณ„์‚ฐํ•ด์ค€๋‹ค.
    func append(_ data: [Message]) {
        messages += data
        for message in data {
            offsets.append((offsets.last ?? 0) + message.height)
        }
    }
}

 

์—ฌ๊ธฐ์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ UICollectionViewDataSource๋ฅผ ์ฑ„ํƒํ•˜๊ณ 

 

Layout์— ํ•„์š”ํ•œ ์ •๋ณด๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ณต์‹๋ฌธ์„œ์— ๋ณด๋ฉด ์ด๋ ‡๊ฒŒ ๋‚˜์™€์žˆ๋„ค์š”

 

The layout object uses information provided by its data source to create the collection viewโ€™s layout. Your layout communicates with the data source by calling methods on the collectionView  property, which is accessible in all of the layoutโ€™s methods.

 

๋ฒˆ์—ญํ•˜๋ฉด ๋ ˆ์ด์•„์›ƒ ์˜ค๋ธŒ์ ํŠธ๋Š” data source๋กœ๋ถ€ํ„ฐ ์ œ๊ณต๋ฐ›์€ ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ ๋‹ค.

 

๋ ˆ์ด์•„์›ƒ์€ collectionView ํ”„๋กœํผํ‹ฐ๋ฅผ ํ†ตํ•ด data source์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์œ„ ์ฝ”๋“œ์—์„œ๋Š” ์…€์ด ์œ„์น˜ํ•  offset๊ณผ ๋†’์ด๋ฅผ ์ œ๊ณตํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์ด์ œ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์šฐ์„  prepare๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

final class CustomLayout: UICollectionViewLayout {
    
    // ์…€ Attributes ์บ์‹ฑ
    private var cellAttributes = [UICollectionViewLayoutAttributes]()
    private var computedContentSize: CGSize = .zero
    
    override var collectionViewContentSize: CGSize {
    	// prepare์—์„œ ๊ณ„์‚ฐํ•œ ํฌ๊ธฐ ๋ฆฌํ„ด
        return computedContentSize
    }
    
    override func prepare() {
        guard let collectionView, let dataSource = collectionView.dataSource as? CustomDataSource else { return }
        
        // data source๋กœ ๋ถ€ํ„ฐ ํ•„์š”ํ•œ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        let heights = dataSource.heights
        let yOffsets = dataSource.offsets
        
        // ์…€ ๋ฐฐ์น˜
        for section in 0..<collectionView.numberOfSections {
            for item in 0..<collectionView.numberOfItems(inSection: section) {
                
                let indexPath = IndexPath(item: item, section: section)
                
                let cellHeight = heights[item]
                let cellWidth = collectionView.bounds.size.width
                let cellOffset = yOffsets[item] - cellHeight
                
                let itemFrame = CGRect(x: 0, y: cellOffset, width: cellWidth, height: cellHeight)
                
                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                attributes.frame = itemFrame
                
                // attributes ์ €์žฅ
                cellAttributes.append(attributes)
            }
        }
        
        // contentSize ๊ณ„์‚ฐ
        let contentWidth = collectionView.bounds.size.width
        let contentHeight = max(collectionView.bounds.size.height, dataSource.offsets.last ?? 0)
        
        computedContentSize = CGSize(width: contentWidth, height: contentHeight)
    }
}

 

์œ„์—์„œ ๋งŒ๋“  data source๋กœ๋ถ€ํ„ฐ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€ ์…€๋“ค์„ ๋ฐฐ์น˜์‹œํ‚ค๊ณ  ํฌ๊ธฐ๋ฅผ ์ •ํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

 

prepare๋Š” ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด

 

Layout updates occur the first time the collection view presents its content and whenever the layout is invalidated explicitly or implicitly because of a change to the view. During each layout update, the collection view calls this method first to give your layout object a chance to prepare for the upcoming layout operation.

 

๋ผ๊ณ  ํ•˜๋Š”๋ฐ์š”, ๋ฒˆ์—ญํ•˜๋ฉด

 

์ฒ˜์Œ present ๋  ๋•Œ, layout์ด invalidate ๋  ๋•Œ ๋ถˆ๋ฆฐ๋‹ค.

 

์—ฌ๊ธฐ์„œ ์•ž์œผ๋กœ ๋ ˆ์ด์•„์›ƒ ์ž‘์—…์— ํ•„์š”ํ•œ ์„ ํ–‰ ๊ณ„์‚ฐ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์šฐ๋ฆฌ๊ฐ€ ๋งŽ์ด ์“ฐ๋Š” reloadData()๋‚˜ invalidateLayout()๋“ฑ์„ ํ˜ธ์ถœํ•˜๋ฉด prepare ํ•œ๋‹ค๋Š” ๊ฒƒ์ธ๋ฐ์š”,

 

์—ฌ๊ธฐ์„œ ํ•˜๋Š” ์ž‘์—…์˜ ๋ฌด๊ฒŒ์— ๋”ฐ๋ผ reloadData()๊ฐ€ ๊ต‰์žฅํžˆ ๋น„ํšจ์œจ์ ์ธ ๋™์ž‘์ด ๋  ์ˆ˜๋„ ์žˆ๊ฒ ๋„ค์š”.

 

์ž˜ ์ปค์Šคํ…€ํ•ด์„œ ์‚ฌ์šฉํ•ด์•ผ๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

์ด์ œ layoutAttributesForElements๋ฅผ ๊ตฌํ˜„ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
    var attributeList = [UICollectionViewLayoutAttributes]()
    
    let index: Int? = binarySearch(cellAttributes) { (attribute) -> ComparisonResult in
        if attribute.frame.intersects(rect) {
            return .orderedSame
        }
        if attribute.frame.minY > rect.maxY {
            return .orderedDescending
        }
        return .orderedAscending
    }
    
    guard let index else { return [] }
    
    for attributes in cellAttributes[..<index].reversed() {
        guard attributes.frame.maxY >= rect.minY else { break }
        attributeList.append(attributes)
    }
    
    for attributes in cellAttributes[index...] {
        guard attributes.frame.minY <= rect.maxY else { break }
        attributeList.append(attributes)
    }
    
    return attributeList
}

private func binarySearch<T: Any>(_ a: [T], where compare: ((T)-> ComparisonResult)) -> Int? {
    
    var lowerBound = 0
    var upperBound = a.count
    
    while lowerBound < upperBound {
        
        let midIndex = lowerBound + (upperBound - lowerBound) / 2
        
        if compare(a[midIndex]) == .orderedSame {
            return midIndex
        } else if compare(a[midIndex]) == .orderedAscending {
            lowerBound = midIndex + 1
        } else {
            upperBound = midIndex
        }
    }
    
    return nil
}

 

rect์•ˆ์— ํ•„์š”ํ•œ attribute๋ฅผ ์ฐพ์•„ ๋„˜๊ฒจ์ฃผ๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

 

์ด ํ•จ์ˆ˜๋Š” ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ๋งค์šฐ ์ž์ฃผ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ๋Š”๋ฐ์š”, ์ง€๊ธˆ ์ƒํ™ฉ์—์„œ๋Š” ์Šคํฌ๋กค์„ ํ•ด์„œ

 

๋ณด์ด๋Š” ํ™”๋ฉด์ด ๋ฐ”๋€Œ๋ฉด ๊ณ„์† ํ˜ธ์ถœ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ rect์— ์•Œ๋งž์€ ์…€์ด๋‚˜ ๋ทฐ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฐพ์•„์„œ ๋„˜๊ธฐ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•œ๋ฐ์š”,

 

์œ„์—์„œ๋Š” ์ด์ง„ ํƒ์ƒ‰์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

 

์ด์ œ ํ•„์š”ํ•œ ํ•จ์ˆ˜๋Š” ๋ชจ๋‘ override ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ ์œ„์—์„œ ๋ณด๋ฉด prepareํ•จ์ˆ˜๋Š” ๊ผญ ์—†์–ด๋„ ๋˜์ง€ ์•Š๋‚˜?๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์–ด์ฐจํ”ผ layoutAttributesForElements์—์„œ ์ฐพ์•„์„œ ๋„˜๊ฒจ์ฃผ๋ฉด ๋˜๋‹ˆ๊น ๋ง์ด์ฃ .

 

๊ทธ๋ž˜์„œ ๊ณต์‹๋ฌธ์„œ์—์„œ๋„ prepare๋Š” ๊ผญ ์ˆ˜ํ–‰๋˜์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์œ„์—์„œ๋„ ๋งํ–ˆ๋“ฏ์ด layoutAttributesForElements์—์„œ๋Š” ์†๋„๊ฐ€ ์ค‘์š”ํ•˜๋ฏ€๋กœ

 

prepare์—์„œ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•ด ์บ์‹ฑ์„ ์ง„ํ–‰ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๋งŒ์•ฝ ์บ์‹ฑ์„ ์ง„ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๋น„์šฉ์ด ๋” ํฌ๊ฑฐ๋‚˜ ํฐ ์ด๋“์ด ์—†๋‹ค๋ฉด, 

 

์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค!

 

 

์ „์ฒด์ฝ”๋“œ

https://github.com/12251145/CustomCollectionViewLayout

 

GitHub - 12251145/CustomCollectionViewLayout

Contribute to 12251145/CustomCollectionViewLayout development by creating an account on GitHub.

github.com

 

 

Ref.

https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617769-layoutattributesforelements

 

Apple Developer Documentation

 

developer.apple.com

https://engineering.linecorp.com/ko/blog/ios-refactoring-uicollectionview-1/

 

UICollectionView๋ฅผ ์ด์šฉํ•œ LINE iOS ๋Œ€ํ™”๋ฐฉ ๋ฆฌํŒฉํ† ๋ง - 1

๋“ค์–ด๊ฐ€๋ฉฐ LINE์˜ ๋Œ€ํ™”๋ฐฉ ํ™”๋ฉด์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ํ™”๋ฉด ์ค‘ ํ•˜๋‚˜์ด๋ฉฐ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ๊ณ„์† ์ถ”๊ฐ€๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ์— ๋”ฐ๋ผ ์ฝ”๋“œ๊ฐ€ ์ ์  ๋ณต์žกํ•ด์ง€๋ฉด์„œ ์ตœ๊ทผ์— ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ–ˆ๊ณ , ๊ทธ

engineering.linecorp.com

https://itnext.io/infinite-grid-using-uicollectionview-155801e4f7f4

 

Creating an infinite grid on iOS โ€” Using UICollectionView

As a follow-up to my previous tutorial โ€œCreating an infinite grid on iOSโ€: https://itnext.io/creating-an-infinite-grid-on-ios-2bd6db28c581โ€ฆ

itnext.io

https://www.kodeco.com/4829472-uicollectionview-custom-layout-tutorial-pinterest

 

UICollectionView Custom Layout Tutorial: Pinterest

Build a UICollectionView custom layout inspired by the Pinterest app, and learn how to cache attributes and dynamically size cells.

www.kodeco.com

https://engineering.giphy.com/how-to-increase-performances-on-custom-uicollectionsviewlayout-using-binary-search/

 

How to increase performances on custom UICollectionsViewLayout

GIPHY Engineering blog.

engineering.giphy.com

 

๋Œ“๊ธ€์ˆ˜0