PinterestTutorial-iOS

πŸ₯Ί
Pinterest Layout Tutorial

이미지 크기에 λ”°λΌμ„œ λ™μ μœΌλ‘œ μ…€μ˜ λ ˆμ΄μ•„μ›ƒμ„ μ„€μ •ν•˜λŠ” ν•€ν„°λ ˆμŠ€νŠΈ λ ˆμ΄μ•„μ›ƒ κ΅¬ν˜„ν•΄ λ³΄μ•˜λ‹€.

μ™„μ„±

μ½”λ“œ

  • UICollectionViewDelegateFlowLayout 의 μ„œλΈŒν΄λž˜μŠ€μΈ PinterestLayout 생성.
CGFloat
}

class PinterestLayout: UICollectionViewFlowLayout {

// delegate둜 ViewController λ₯Ό λ‚˜νƒ€λ‚Έλ‹€.
weak var delegate: PinterestLayoutDelegate?

private var contentHeight: CGFloat = 0

private var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width – (insets.left + insets.right)
}

// 1. μ½œλ ‰μ…˜ 뷰의 μ½˜ν…μΈ  μ‚¬μ΄μ¦ˆλ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}

// λ‹€μ‹œ λ ˆμ΄μ•„μ›ƒμ„ 계산할 ν•„μš”κ°€ 없도둝 λ©”λͺ¨λ¦¬μ— μ €μž₯ν•©λ‹ˆλ‹€.
private var cache: [UICollectionViewLayoutAttributes] = []

// 2. μ½œλ ‰μ…˜ λ·°κ°€ 처음 μ΄ˆκΈ°ν™”λ˜κ±°λ‚˜ λ·°κ°€ 변경될 λ–„ μ‹€ν–‰λ©λ‹ˆλ‹€. 이 λ©”μ„œλ“œμ—μ„œ λ ˆμ΄μ•„μ›ƒμ„
// 미리 κ³„μ‚°ν•˜μ—¬ λ©”λͺ¨λ¦¬μ— μ μž¬ν•˜κ³ , ν•„μš”ν•  λ•Œλ§ˆλ‹€ 효율적으둜 μ ‘κ·Όν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν•΄μ•Ό ν•©λ‹ˆλ‹€.
override func prepare() {
guard let collectionView = collectionView, cache.isEmpty else { return }

let numberOfColumns: Int = 2 // ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
let cellPadding: CGFloat = 5
let cellWidth: CGFloat = contentWidth / CGFloat(numberOfColumns)

let xOffSet: [CGFloat] = [0, cellWidth] // cell 의 x μœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄
var yOffSet: [CGFloat] = .init(repeating: 0, count: numberOfColumns) // // cell 의 y μœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄

var column: Int = 0 // ν˜„μž¬ ν–‰μ˜ μœ„μΉ˜

for item in 0.. yOffSet[1] ? 1 : 0
}
}

// 3. λͺ¨λ“  μ…€κ³Ό 보좩 뷰의 λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•©λ‹ˆλ‹€. ν™”λ©΄ ν‘œμ‹œ μ˜μ—­ 기반(Rect)의 μš”μ²­μ΄ λ“€μ–΄μ˜¬ λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []

for attributes in cache {
if attributes.frame.intersects(rect) { // μ…€ frame κ³Ό μš”μ²­ Rect κ°€ κ΅μ°¨ν•œλ‹€λ©΄, 리턴 값에 μΆ”κ°€ν•©λ‹ˆλ‹€.
visibleLayoutAttributes.append(attributes)
}
}

return visibleLayoutAttributes
}

// 4. λͺ¨λ“  μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•©λ‹ˆλ‹€. IndexPath 둜 μš”μ²­μ΄ λ“€μ–΄μ˜¬ λ•Œ 이 λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
override func layoutAttributesForItem(at indexPath: IndexPath)
-> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
“>

// PinterestLayout μ—μ„œ 각 이미지 높이λ₯Ό μ•Œ 수 μžˆλ„λ‘ Delegate 생성.
protocol PinterestLayoutDelegate: AnyObject {
    func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
}

class PinterestLayout: UICollectionViewFlowLayout {
    
    // delegate둜 ViewController λ₯Ό λ‚˜νƒ€λ‚Έλ‹€.
    weak var delegate: PinterestLayoutDelegate?
    
    private var contentHeight: CGFloat = 0
    
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
    // 1. μ½œλ ‰μ…˜ 뷰의 μ½˜ν…μΈ  μ‚¬μ΄μ¦ˆλ₯Ό μ§€μ •ν•©λ‹ˆλ‹€.
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    // λ‹€μ‹œ λ ˆμ΄μ•„μ›ƒμ„ 계산할 ν•„μš”κ°€ 없도둝 λ©”λͺ¨λ¦¬μ— μ €μž₯ν•©λ‹ˆλ‹€.
    private var cache: [UICollectionViewLayoutAttributes] = []
    
    // 2. μ½œλ ‰μ…˜ λ·°κ°€ 처음 μ΄ˆκΈ°ν™”λ˜κ±°λ‚˜ λ·°κ°€ 변경될 λ–„ μ‹€ν–‰λ©λ‹ˆλ‹€. 이 λ©”μ„œλ“œμ—μ„œ λ ˆμ΄μ•„μ›ƒμ„
    //    미리 κ³„μ‚°ν•˜μ—¬ λ©”λͺ¨λ¦¬μ— μ μž¬ν•˜κ³ , ν•„μš”ν•  λ•Œλ§ˆλ‹€ 효율적으둜 μ ‘κ·Όν•  수 μžˆλ„λ‘ κ΅¬ν˜„ν•΄μ•Ό ν•©λ‹ˆλ‹€.
    override func prepare() {
        guard let collectionView = collectionView, cache.isEmpty else { return }
        
        let numberOfColumns: Int = 2 // ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
        let cellPadding: CGFloat = 5
        let cellWidth: CGFloat = contentWidth / CGFloat(numberOfColumns)
        
        let xOffSet: [CGFloat] = [0, cellWidth] // cell 의 x μœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄
        var yOffSet: [CGFloat] = .init(repeating: 0, count: numberOfColumns) // // cell 의 y μœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄
        
        var column: Int = 0 // ν˜„μž¬ ν–‰μ˜ μœ„μΉ˜
        
        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            // IndexPath 에 λ§žλŠ” μ…€μ˜ 크기, μœ„μΉ˜λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
            let indexPath = IndexPath(item: item, section: 0)
            let imageHeight = delegate?.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath) ?? 180
            let height = cellPadding * 2 + imageHeight
            
            let frame = CGRect(x: xOffSet[column],
                               y: yOffSet[column],
                               width: cellWidth,
                               height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
            
            // μœ„μ—μ„œ κ³„μ‚°ν•œ Frame 을 기반으둜 cache 에 λ“€μ–΄κ°ˆ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
            
            // μ½œλ ‰μ…˜ 뷰의 contentHeight λ₯Ό λ‹€μ‹œ μ§€μ •ν•©λ‹ˆλ‹€.
            contentHeight = max(contentHeight, frame.maxY)
            yOffSet[column] = yOffSet[column] + height
            
            // λ‹€λ₯Έ 이미지 크기둜 μΈν•΄μ„œ, ν•œμͺ½ μ—΄μ—λ§Œ 이미지가 μΆ”κ°€λ˜λŠ” 것을 λ°©μ§€ν•©λ‹ˆλ‹€.
            column = yOffSet[0] > yOffSet[1] ? 1 : 0
        }
    }
    
    // 3. λͺ¨λ“  μ…€κ³Ό 보좩 뷰의 λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•©λ‹ˆλ‹€. ν™”λ©΄ ν‘œμ‹œ μ˜μ—­ 기반(Rect)의 μš”μ²­μ΄ λ“€μ–΄μ˜¬ λ•Œ μ‚¬μš©ν•©λ‹ˆλ‹€.
        override func layoutAttributesForElements(in rect: CGRect)
        -> [UICollectionViewLayoutAttributes]? {
            var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
            
            for attributes in cache {
                if attributes.frame.intersects(rect) { // μ…€ frame κ³Ό μš”μ²­ Rect κ°€ κ΅μ°¨ν•œλ‹€λ©΄, 리턴 값에 μΆ”κ°€ν•©λ‹ˆλ‹€.
                    visibleLayoutAttributes.append(attributes)
                }
            }
            
            return visibleLayoutAttributes
        }
        
        // 4. λͺ¨λ“  μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•©λ‹ˆλ‹€. IndexPath 둜 μš”μ²­μ΄ λ“€μ–΄μ˜¬ λ•Œ 이 λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
        override func layoutAttributesForItem(at indexPath: IndexPath)
        -> UICollectionViewLayoutAttributes? {
            return cache[indexPath.item]
        }
}
  • MainVC μ—μ„œ PinterestLayoutDelegate λ₯Ό 채택.
CGFloat {
let cellWidth: CGFloat = (view.bounds.width – 4) / 2 // μ…€ κ°€λ‘œ 크기
let imageHeight = imageList[indexPath.item].image.size.height
let imageWidth = imageList[indexPath.item].image.size.width
// 이미지 λΉ„μœ¨
let imageRatio = imageHeight/imageWidth

return imageRatio * cellWidth
}
}
“>

// MARK: - UICollectionViewDelegateFlowLayout
extension MainVC: PinterestLayoutDelegate {
    func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
        let cellWidth: CGFloat = (view.bounds.width - 4) / 2 // μ…€ κ°€λ‘œ 크기
        let imageHeight = imageList[indexPath.item].image.size.height
        let imageWidth = imageList[indexPath.item].image.size.width
        // 이미지 λΉ„μœ¨
        let imageRatio = imageHeight/imageWidth
        
        
        return imageRatio * cellWidth
    }
}

좜처

μΆœμ²˜γ…£Swift. UICollectionView Pinterest λ ˆμ΄μ•„μ›ƒ

GitHub

https://github.com/hyun99999/PinterestTutorial-iOS