😩 길고도 ν—˜ν–ˆλ˜ ν•€ν„°λ ˆμŠ€νŠΈ UI λ˜λŠ” Masonry

κΉ€μž¬ν˜•Β·2024λ…„ 4μ›” 28일
1
post-custom-banner

ν•€ν„°λ ˆμŠ€νŠΈ UI?

일단 μ‹œμž‘ν•˜κΈ° μ•žμ„œβ€¦ μ΄μž‘μ—…μ„ ν•˜λ£¨ν•˜κ³ λ„ μ ˆλ°˜μ„ μŸμ•„μ„œ μ–»μ–΄λ‚Έ κ²°κ³Όμž…λ‹ˆλ‹€.
그만큼 μ‹œν–‰ μ°©μ˜€κ°€ 정말 λ§Žμ•˜κ³  머리도 μ•„ν”„κ³ .. γ…  μ•„λ¬΄νŠΌ κ·Έ 과정듀을 μ†Œκ°œ ν• κΉŒ ν•©λ‹ˆλ‹€.

ν•€ν„°λ ˆμŠ€νŠΈ UI κ°€ λ¬΄μ—‡μ΄λ‚˜λ©΄

μœ„μ™€κ°™μ€ μ‚¬μ§„μ²˜λŸΌ μ‚¬μ§„μ˜ 크기에 따라
UIκ°€ κ²°μ •λ˜λŠ” μ •ν™•νžˆλŠ” λ ˆμ΄μ•„μ›ƒμ΄ μ •ν•΄μ§€λŠ” UIλ₯Ό ν•€ν„°λ ˆμŠ€νŠΈ UI라고 ν•©λ‹ˆλ‹€.
λ‹€λ₯Έ μš©μ–΄λ‘œλŠ” Masonry λ ˆμ΄μ•„μ›ƒ 이라고도 ν•©λ‹ˆλ‹€.

μ»΄ν¬μ§€μ…”λ„λ‘œ 도전해 볼까?

κ²°λ‘ λΆ€ν„° 말씀 λ“œλ¦¬μžλ©΄ μ €λŠ” μ»΄ν¬μ§€μ…”λ„λ‘œλŠ” μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.
μ΄μœ κ°€ 기본적인 컴포지셔널 λ ˆμ΄μ•„μ›ƒ μ½”λ“œλ₯Ό 보며 μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.

func createBasicListLayout() -> UICollectionViewLayout { 
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                  
                                         heightDimension: .fractionalHeight(1.0))    
    let item = NSCollectionLayoutItem(layoutSize: itemSize)  
  
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                          
                                          heightDimension: .absolute(44))    
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,                                                   
                                                     subitems: [item])  
  
    let section = NSCollectionLayoutSection(group: group)    

    let layout = UICollectionViewCompositionalLayout(section: section)    
    return layout
}

μœ„μ™€κ°™μ΄ 컴포지셔널 λ ˆμ΄μ•„μ›ƒμ„ κ΅¬μ„±ν•˜κ²Œ 될텐데
잘 λ³΄μ‹œλ©΄ 이게 ν•˜λ‚˜ ν•˜λ‚˜ μ—κ²Œ μ£ΌλŠ” 방식이 μ•„λ‹ˆλΌ
μ „μ²΄μ μœΌλ‘œ μ΄λŸ΄κ±°μ•Ό~ λΌλŠ” λ ˆμ΄μ•„μ›ƒ ꡬ성방식이라
ν•˜λ‚˜ν•˜λ‚˜μ˜ λΉ„μœ¨μ„ 적용 μ‹œν‚€κΈ°μ—λŠ” 무리가 μžˆμ—ˆλ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€.

  • 4/29 좔가사항 -> μ»΄ν¬μ§€μ…”λ„λ‘œλ„ κ΅¬ν˜„μ΄ κ°€λŠ₯ν•œ 정보λ₯Ό μ•Œκ²Œλ˜μ–΄ 후에 μƒˆλ‘œμš΄ κΈ€λ‘œ μ—…λ°μ΄νŠΈ ν•˜κ² μŠ΅λ‹ˆλ‹€.

그럼 ν”Œλ‘œμš°?

μ΅œμ’…μ  μœΌλ‘œλ„ ν”Œλ‘œμš° λ ˆμ΄μ•„μ›ƒμ΄κΈ΄ ν•˜λ‚˜ 그과정을 μ†Œκ°œν•©λ‹ˆλ‹€.

extension UserProfileViewController: UICollectionViewDelegateFlowLayout {
     func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
         // λͺ¨λΈ....
         let model = viewModel.realModel[indexPath.row]
         // λΉ„μœ¨
         let asecpct =  CGFloat(Double(model.content3) ?? 1)
         print("μž‘λ™μ€ ν•˜λ‹ˆ? \(collectionView.bounds.width)")
         // μ»¬λ ‰μ…˜λ·° 전체
         let totalWidth = collectionView.bounds.width
         
         let spacing: CGFloat = 5
         
         let itemsRow: CGFloat = 2
         
         let estWidth = totalWidth - (spacing * (itemsRow + 1))
         
         let cellWidth = estWidth / itemsRow
         
         let cellHeight = cellWidth / asecpct
         
         return CGSize(width: cellWidth, height: cellHeight)
     }
 }

μœ„μ˜ μ½”λ“œλ₯Ό λ³΄μ‹œλ©΄ content3 κ°€ 보이싀 텐데 μ €κ°€ μ„œλ²„μ—
μ›λž˜μ˜ 이미지 λΉ„μœ¨μ„ 전솑을 ν•˜κ³  κ·Έ 값을 λ‹€μ‹œ λ°›μ•„μ˜¬λ•Œ 이미지 λΉ„μœ¨μ„ 계산할 ν•„μš”μ—†μ΄
λ°”λ‘œ μ μš©ν• μˆ˜ 있게 (μ„œλ²„μ™€μ˜ λ”œλ ˆμ΄ μ΅œμ†Œν™”) ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 쒌우 여백을 생각 ν•˜μ§€ λͺ»ν•΄μ„œ 일자둜 μ­‰ λ‚˜μ˜€κ²Œ λ˜λŠ” 일이 λ°œμƒν•˜κ²Œ λ˜λŠ”λ°,
그것을 λ³΄μ™„ν•˜κ³ μž μ•„λž˜μ™€ 같이 μ„Ήμ…˜μΈμ…‹μ„ 가져와 μΆ”κ°€ν•˜λΌκ³  ν–ˆμ—ˆμŠ΅λ‹ˆλ‹€.

extension UserProfileViewController: UICollectionViewDelegateFlowLayout {
     func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // λͺ¨λΈμ—μ„œ λΉ„μœ¨μ„ κ°€μ Έμ˜΅λ‹ˆλ‹€.
        let model = viewModel.realModel[indexPath.row]
        let aspectRatio = CGFloat(Double(model.content3) ?? 1)
        
        // μ»¬λ ‰μ…˜λ·°μ˜ μ„Ήμ…˜ 인셋을 κ°€μ Έμ˜΅λ‹ˆλ‹€.
        let sectionInsets = (
            collectionView.collectionViewLayout as? UICollectionViewFlowLayout
        )?.sectionInset ?? UIEdgeInsets.zero
        
        // μ…€ 간격을 μ •μ˜ν•©λ‹ˆλ‹€.
        let spacing: CGFloat = 5
        // ν•œ 쀄에 ν‘œμ‹œλ  μ•„μ΄ν…œμ˜ 수λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.
        let itemsPerRow: CGFloat = 2
        print(sectionInsets)
        // μ‚¬μš© κ°€λŠ₯ν•œ λ„ˆλΉ„λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
        let totalSpacing = (spacing * (itemsPerRow - 1)) + sectionInsets.left + sectionInsets.right
        let availableWidth = collectionView.bounds.width - totalSpacing
        
        // μ…€μ˜ λ„ˆλΉ„μ™€ 높이λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
        let cellWidth = availableWidth / itemsPerRow
        let cellHeight = cellWidth / aspectRatio
        
        return CGSize(width: cellWidth, height: cellHeight)
     }
 }

μ½”λ“œλ§Œ 보면 벌써 μ™„μ„± 된거 같은데 κ²°κ³ΌλŠ” λ­”κ°€ μ•„μ‰¬μš΄ 결과물이 νƒ„μƒν•©λ‹ˆλ‹€.

흠… λ­”κ°€ λ˜λŠ”κ±° κ°™κΈ΄ν•œλ° μ°Έ …. 이쁘죠?
μ΄μœ κ°€ λ­˜κΉŒμš”?
μ½”λ“œκ°€ μ΄μƒν• κΉŒμš”?
ν”Œλ‘œμš° λ ˆμ΄μ•„μ›ƒμ˜ κ°œλ…μ„ λ‹€μ‹œ μ‚΄νŽ΄ λ΄…λ‹ˆλ‹€.

ν”Œλ‘œμš° λ ˆμ΄μ•„μ›ƒμ€ νŠΉμ • Line λ”°λΌμ„œ Cell을 λ°°μΉ˜ν•˜λŠ”λ°μš”
곡간이 λΆ€μ‘±ν•΄μ§ˆλ•Œ μƒˆλ‘œμš΄ 라인을 생성해 λ ˆμ΄μ•„μ›ƒμ„ κ·Έλ¦¬λŠ” κ΅¬μ‘°μž…λ‹ˆλ‹€.
κ·Έλž˜μ„œ μœ„μ™€κ°™μ€ 생기닀 만 μΉœκ΅¬κ°€ νƒ„μƒν•˜κ²Œ λ©λ‹ˆλ‹€.

μ»€μŠ€ν…€ λ ˆμ΄μ•„μ›ƒ 클래슀 λ§Œλ“€μž~

κ·Έλž˜μ„œβ€¦ μ €λŸ°κ²ƒλ“€μ„ μ •μ˜ν• μˆ˜μžˆλŠ” 클래슀λ₯Ό μƒμ„±ν•˜κ²Œ λ˜λŠ”λ°μš”
같이 ν•œλ²ˆ λ³΄μ‹€κΉŒμš”?

핡심뢀뢄

final class CustomPinterestLayout: UICollectionViewFlowLayout {
/// ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
    private let numberOfColums: Int // ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
    /// μ…€μ˜ νŒ¨λ”©
    private let cellPadding: CGFloat // μ…€μ˜ νŒ¨λ”©
    /// ν•„μš”μ˜ 경우 ν—€λ”μ˜ 높이 지정: Int
    private
    let headerHeight: CGFloat?
    
     // 1.END μ»¬λ ‰μ…˜λ·°μ˜ 컨텐츠 μ‚¬μ΄μ¦ˆλ₯Ό 지정
    override var collectionViewContentSize: CGSize {
        let minHeight = collectionView?.bounds.height ?? 0
        
        return CGSize(width: contetnsWidth, height: max(contentsHeight,minHeight))
    }
    
    // 2. μ»¬λ ‰μ…˜λ·°κ°€ 처음 μ΄ˆκΈ°ν™” ν˜Ήμ€ λ·°κ°€ λ³€κ²½λ λ•Œ μ‹€ν–‰λ˜λŠ” λ©”μ„œλ“œ
  
    override func prepare() 
    
    // 3. λͺ¨λ“  μ…€κ³Ό μ„œλΈŒ 뷰의 λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•΄μ€λ‹ˆλ‹€. (Rect) 기반
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
    
    // 4. λͺ¨λ“  μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?

μœ„μ™€κ°™μ€ λ©”μ„œλ“œλ“€μ„ 직접 λ‹€ μ •μ˜ν• κ±΄λ° 이 것듀을 효과적으둜 μ „λ‹¬ν•˜κΈ° μœ„ν•΄μ„œ
λ”œλ¦¬κ²Œμ΄νŠΈλ₯Ό λ¨Όμ € μƒμ„±ν•˜λ©΄μ„œ μ‹œμž‘ν•΄ λ³Όκ»˜μš”

protocol CustomPinterestLayoutDelegate: AnyObject {
		
    func collectionView(for collectionView: UICollectionView, heightForAtIndexPath indexPath: IndexPath) -> CGFloat
    
}

μœ„μ˜ μ½”λ“œλŠ” λ”œλ¦¬κ²Œμ΄νŠΈ νŒ¨ν„΄μœΌλ‘œ ν™œμš©ν•  ν”„λ‘œν† μ½œμž…λ‹ˆλ‹€.
λͺ©μ μ€ μƒκΉ€μƒˆμ™€ 같이 μ»¬λ ‰μ…˜λ·°μ™€ 인덱슀패슀λ₯Ό λ°›μ•„μ„œ CGFloat을 λ°˜ν™˜ν•©λ‹ˆλ‹€.
즉 높이λ₯Ό λ°˜ν™˜ν•˜λŠ” κ±°μ£ 

κ΅¬μ„±μ˜ μ‹œμž‘

final class CustomPinterestLayout: UICollectionViewFlowLayout {
    weak var delegate: CustomPinterestLayoutDelegate?
	// ν•œν–‰μ˜ μ•„μ΄ν…œ 갯수
    private let numberOfColums: Int
  // μ…€μ˜ νŒ¨λ”©
    private let cellPadding: CGFloat
  // ν•„μš”ν•  κ²½μš°μ— ν—€λ”μ˜ 높이λ₯Ό 지정
    private let headerHeight: CGFloat?

    init(numberOfColums: Int, cellPadding: CGFloat, headerHeight: CGFloat? = nil) {
        self.numberOfColums = numberOfColums
        self.cellPadding = cellPadding
        self.headerHeight = headerHeight
        super.init()
    }

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

μ‚¬μ΄μ¦ˆλ₯Ό μ–΄λ–»κ²Œ κ³„μ‚°ν•˜μ§€?

이뢀뢄을 μ’€ 고민을 ν•˜μ˜€λŠ”λ° ν”Œλ‘œμš° λ ˆμ΄μ•„μ›ƒμ΄λ‹€ λ³΄λ‹ˆ μ…€μ˜ κ°―μˆ˜κ°€ 화면에 λΉ„ν•΄ λΆ€μ‘±ν•˜λ©΄
슀크둀이 μ•ˆλ˜λŠ” 참사가 이뀄지기 λ•Œλ¬Έμ— λ‹€μŒκ³Ό 같은 μ½”λ“œλ‘œ κ³„μ‚°ν•©λ‹ˆλ‹€.

override var collectionViewContentSize: CGSize {
    let minHeight = collectionView?.bounds.height ?? 0
    return CGSize(width: contetnsWidth, height: max(contentsHeight, minHeight))
}

슀크둀 κ°€λŠ₯ν•œ μ˜μ—­μ„ μ •μ˜ν•˜κ³ , 컨텐츠가 뷰의 크기보닀 μž‘μ„λ•Œλ„ 슀크둀이 κ°€λŠ₯ν•˜κ²Œ ν•©λ‹ˆλ‹€.

What the Prepare!

λ‹€μŒ μ½”λ“œμ— μ•žμ„œμ„œ λ‚˜μ˜€κ²Œ λ˜λŠ” Prepare λΌλŠ” λ©”μ„œλ“œκ°€ λ“±μž₯ν•˜λŠ”λ°
λ ˆμ΄μ•„μ›ƒμ˜ 초기 계산과 μΉ μš”ν•œ λ ˆμ΄μ•„μ•„μˆ˜ μ •λ³΄μ˜ μ„ΈνŒ…μ„ λ‹΄λ‹Ήν•©λ‹ˆλ‹€.
( μ»¬λ ‰μ…˜λ·° 컨텐츠 ν‘œμ‹œν•˜κΈ°μ „μ˜ 호좜됨)

κ΅¬μ„±λœ Prepare

// Options λ‹€μ‹œ λ ˆμ΄μ•„μ›ƒμ„ 계산할 ν•„μš”κ°€ 없도둝 λ©”λͺ¨λ¦¬μ— μ €μž₯ν•œλ‹€.
    private
    var cache: [UICollectionViewLayoutAttributes] = []
    
 // 2. μ»¬λ ‰μ…˜λ·°κ°€ 처음 μ΄ˆκΈ°ν™” λ˜κ±°λ‚˜, λ·°κ°€ λ³€κ²½λ λ•Œ μ‹€ν–‰λ©λ‹ˆλ‹€.
    // ... ν•΄λ‹Ή λ©”μ„œλ“œλŠ” λ ˆμ΄μ•„μ›ƒμ„ 미리 계산 ν•˜κ³   λ©”λͺ¨λ¦¬μ— μΊμ‰¬ν•˜μ—¬
    // ... λΆˆν•„μš”ν•œ 반볡적인 연산을 ν•˜λŠ”κ²ƒμ„ λ°©μ§€ν•˜λ„λ‘ ν•΄μ•Όν•œλ‹€.
    override func prepare() {
        guard let collectionView = collectionView,
              collectionView.numberOfSections > 0,
              collectionView.numberOfItems(inSection: 0) > 0,
              cache.isEmpty else {
            return
        }
        cache.removeAll()
        
        let cellWidth = contetnsWidth / CGFloat(numberOfColums)
        
        var yOffSet:[CGFloat] = []
        
        if let headerHeight {
            // 헀더 λ ˆμ΄μ•„μ›ƒ 속성
            let headerIndexPath = IndexPath(item: 0, section: 0)

            let headerAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: headerIndexPath)
            
            headerAttribute.frame = CGRect(
                x: 0,
                y: 0,
                width: collectionView.bounds.width,
                height: headerHeight
            ) // 헀더높이 지정
            
            cache.append(headerAttribute)
            // cell 의 Yμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
            yOffSet = [CGFloat](repeating: headerAttribute.frame.maxY, count: numberOfColums)
            
            yOffSet[0] = headerAttribute.frame.maxY
            
        } else {
            yOffSet = [CGFloat](repeating: 0, count: numberOfColums)
        }
        
        // cell 의 Xμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
        let xOffSet:[CGFloat] = [0, cellWidth]

        var colum: Int = 0 // ν˜„μž¬ ν–‰μ˜ μœ„μΉ˜
        
        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            // 인덱슀 패슀λ₯Ό 톡해
            let indexPath = IndexPath(item: item, section: 0)
            // 인덱슀 νŒ¨μŠ€μ— λ§žλŠ” μ…€μ˜ 크기λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
            let customContentHeight = delegate?.collectionView(for: collectionView, heightForAtIndexPath: indexPath) ?? 100
            // 상항 νŒ¨λ”©(νŒ¨λ”© 2λ°°) 에 μ»¨ν…ŒνŠΈ 높이λ₯Ό λ”ν•œ 값은 높이
            let height = cellPadding * 2 + customContentHeight
            
            let frame = CGRect(
                x: xOffSet[colum],
                y: yOffSet[colum],
                width: cellWidth,
                height: height
            )
            
            // μƒμ†Œν•œ dx, dyκ°€ λ‚˜μ™”λŠ”λ°
            // dx: xμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> 쒌우 ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            // dy: yμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> μƒν•˜ ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            let insetFrame = frame.insetBy(
                dx: cellPadding, dy: cellPadding
            )
            
            // κ³„μ‚°ν•œ Frameλ₯Ό 톡해 λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό λ°˜μ˜ν•˜κ³  캐쉬에 μ €μž₯
            let attribute = UICollectionViewLayoutAttributes(
                forCellWith: indexPath
            )
            // λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό 반영
            attribute.frame = insetFrame
            cache.append(attribute)
            
            // μ»¬λ ‰μ…˜λ·°μ˜ 높이λ₯Ό λ‹€μ‹œ μ§€μ •ν•œλ‹€.
            // frame.maxY -> ν˜„μž¬ μ…€μ˜ ν”„λ ˆμž„μ΄ λλ‚˜λŠ” YμΆ• μœ„μΉ˜ -> μ…€ 상단 μœ„μΉ˜μ—μ„œ μ…€ 높이λ₯Ό λ”ν•œκ°’
            contentsHeight = max(contentsHeight, frame.maxY)
            // μƒˆλ‘œμš΄ μ…€μ˜ frame.maxY쀑 더큰 값을 μ„ νƒν•˜μ—¬ μ»¬λ ‰μ…˜λ·°μ˜ 전체 컨텐츠 높이λ₯Ό μ—…λ°μ΄νŠΈ
            
            yOffSet[colum] = yOffSet[colum] + height
            
            // λ‹€λ₯Έ 이미지 크기둜 인해, ν•œμͺ½μ—΄μ—λ§Œ 이미지가 좔가됨을 방지
            colum = yOffSet[0] > yOffSet[1] ? 1 : 0
            // λ§Œμ•½ 첫번째 μ—΄μ˜ 높이가 λ‘λ²ˆμ§Έ μ—΄μ˜ 높이보닀 크닀면
            // μƒˆ μ…€λ“€ λ‘λ²ˆμ§Έ 열에 λ°°μΉ˜ν•˜λŠ”λ°
            // μ•„λ‹μ‹œ μƒˆ 셀을 첫번째 열에 λ°°μΉ˜ν•œλ‹€.
        }
        
    }
    

μ–΄μš°β€¦. λ„ˆλ¬΄ κΈΈμ£ ?
μž˜λΌμ„œ λ³΄κ² μŠ΅λ‹ˆλ‹€.

Prepare() 1. 뷰의 μƒνƒœλ₯Ό ν™•μΈν•˜κ³  초기 μ„€μ • μ§„ν–‰ν•˜κΈ°

  • μ»¬λ ‰μ…˜λ·°κ°€ 쑴재 ν•˜λ‚˜μš”?
  • ν•˜λ‚˜ μ΄μƒμ˜ μ„Ήμ…˜μ€ μžˆλ‚˜μš”?
  • μΊμ‹œκ°€ λΉ„μ–΄μžˆλ‚˜μš”?
  • μœ„ 3개의 따라 λΆˆν•„μš”ν•œ 계산을 방지할 λ ˆμ΄μ•„μ›ƒμ€ μ΅œμ‹  μƒνƒœλ₯Ό λ°˜μ˜ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.
guard let collectionView = collectionView else { return }
  if collectionView.numberOfSections == 0 || cache.isEmpty == false {
      cache.removeAll()
 }

Prepate() 2. μ…€ 및 ν—€λ”μ˜ λ ˆμ΄μ•„μ›ƒ 속성 계산 및 캐싱

 let cellWidth = contetnsWidth / CGFloat(numberOfColums)
        
 var yOffSet:[CGFloat] = []
        
 if let headerHeight {
    // 헀더 λ ˆμ΄μ•„μ›ƒ 속성
   let headerIndexPath = IndexPath(item: 0, section: 0)

   let headerAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: headerIndexPath)
            
   headerAttribute.frame = CGRect(
       x: 0,
       y: 0,
       width: collectionView.bounds.width, // μ»¬λ ‰μ…˜λ·° 자체의 넓이에 
       height: headerHeight // 헀더 높이 
   ) 
            
   cache.append(headerAttribute)
   // cell 의 Yμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
   yOffSet = [CGFloat](repeating: headerAttribute.frame.maxY, count: numberOfColums)
            
   yOffSet[0] = headerAttribute.frame.maxY
            
} else {
   yOffSet = [CGFloat](repeating: 0, count: numberOfColums)
}

λ ˆμ΄μ•„μ›ƒμ˜ 각 μ…€κ³Ό 헀더(선택적) μœ„μΉ˜μ™€ 크기λ₯Ό κ³„μ‚°ν•˜μ—¬μ„œ
UICollectionViewLayoutAttributes 객체에 μ €μž₯ν•©λ‹ˆλ‹€.

Prepare() 2.2 μ…€μ˜ μ •μ˜

  // cell 의 Xμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
        let xOffSet:[CGFloat] = [0, cellWidth]

        var colum: Int = 0 // ν˜„μž¬ ν–‰μ˜ μœ„μΉ˜
        
        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            // 인덱슀 패슀λ₯Ό 톡해
            let indexPath = IndexPath(item: item, section: 0)
            // 인덱슀 νŒ¨μŠ€μ— λ§žλŠ” μ…€μ˜ 크기λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
            let customContentHeight = delegate?.collectionView(for: collectionView, heightForAtIndexPath: indexPath) ?? 100
            // 상항 νŒ¨λ”©(νŒ¨λ”© 2λ°°) 에 μ»¨ν…ŒνŠΈ 높이λ₯Ό λ”ν•œ 값은 높이
            let height = cellPadding * 2 + customContentHeight
            
            let frame = CGRect(
                x: xOffSet[colum],
                y: yOffSet[colum],
                width: cellWidth,
                height: height
            )
            
            // μƒμ†Œν•œ dx, dyκ°€ λ‚˜μ™”λŠ”λ°
            // dx: xμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> 쒌우 ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            // dy: yμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> μƒν•˜ ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            let insetFrame = frame.insetBy(
                dx: cellPadding, dy: cellPadding
            )
            
            // κ³„μ‚°ν•œ Frameλ₯Ό 톡해 λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό λ°˜μ˜ν•˜κ³  캐쉬에 μ €μž₯
            let attribute = UICollectionViewLayoutAttributes(
                forCellWith: indexPath
            )
            // λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό 반영
            attribute.frame = insetFrame
            cache.append(attribute)
            
            // μ»¬λ ‰μ…˜λ·°μ˜ 높이λ₯Ό λ‹€μ‹œ μ§€μ •ν•œλ‹€.
            // frame.maxY -> ν˜„μž¬ μ…€μ˜ ν”„λ ˆμž„μ΄ λλ‚˜λŠ” YμΆ• μœ„μΉ˜ -> μ…€ 상단 μœ„μΉ˜μ—μ„œ μ…€ 높이λ₯Ό λ”ν•œκ°’
            contentsHeight = max(contentsHeight, frame.maxY)
            // μƒˆλ‘œμš΄ μ…€μ˜ frame.maxY쀑 더큰 값을 μ„ νƒν•˜μ—¬ μ»¬λ ‰μ…˜λ·°μ˜ 전체 컨텐츠 높이λ₯Ό μ—…λ°μ΄νŠΈ
            
            yOffSet[colum] = yOffSet[colum] + height
            
            // λ‹€λ₯Έ 이미지 크기둜 인해, ν•œμͺ½μ—΄μ—λ§Œ 이미지가 좔가됨을 방지
            colum = yOffSet[0] > yOffSet[1] ? 1 : 0
            // λ§Œμ•½ 첫번째 μ—΄μ˜ 높이가 λ‘λ²ˆμ§Έ μ—΄μ˜ 높이보닀 크닀면
            // μƒˆ μ…€λ“€ λ‘λ²ˆμ§Έ 열에 λ°°μΉ˜ν•˜λŠ”λ°
            // μ•„λ‹μ‹œ μƒˆ 셀을 첫번째 열에 λ°°μΉ˜ν•œλ‹€.
        }
        
    }

3. λͺ¨λ“ μ…€, μ„œλΈŒλ·°μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λŒλ €μ€˜μ•Ό ν•΄μš”

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        
        for attribute in cache {
            // intersects λ©”μ„œλ“œλŠ” 두 μ‚¬κ°ν˜•μ΄ κ²ΉμΉ˜λŠ”μ§€ μ—¬λΆ€λ₯Ό λ°˜ν™˜
            if attribute.frame.intersects(rect){
                // μ…€ ν”„λ ˆμž„κ³Ό μš”μ²­κ³Ό κ΅μ°¨ν•œλ‹€λ©΄ 리턴 κ°’μ˜ μΆ”κ°€
                    visibleLayoutAttributes.append(attribute)
            }
        }
        return visibleLayoutAttributes
    }

ν˜„μž¬ 화면에 λ³΄μ—¬μ§ˆ μ…€, 헀더, ν‘Έν„° λ“±μ˜ λ ˆμ΄μ•„μ›ƒ 속성을 κ²°μ •ν•  λ•Œ 호좜 λ˜μ–΄μ§€λŠ” λ©”μ„œλ“œ μž…λ‹ˆλ‹€.
슀크둀링이 λ°œμƒ λ λ•Œλ§ˆλ‹€ 호좜되며, 화면에 λ³΄μ΄λŠ” μš”μ†Œλ“€μ˜ λ ˆμ΄μ•„μ›ƒμ„ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.

λ§ˆμ§€λ§‰β€¦.! λͺ¨λ“ μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•˜κΈ°!

// 4. λͺ¨λ“  μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•œλ‹€. IndexPath 기반
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[indexPath.item]
    }

ν•΄λ‹Ή λ©”μ„œλ“œλŠ” νŠΉμ • 셀에 λŒ€ν•œ λ ˆμ΄μ•„μ›ƒ 속성을 λ°˜ν™˜ν•˜λŠ” κΈ°λŠ₯을 가지고 μžˆμŠ΅λ‹ˆλ‹€.
각 셀이 화면에 ν‘œμ‹œλ  λ•Œ ν•΄λ‹Ή μ…€μ˜ μœ„μΉ˜, 크기, λ³€ν˜• λ“±μ˜ λ ˆμ΄μ•„μ›ƒ 속성을 μ œκ³΅ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€.

μ „μ²΄μ½”λ“œ 곡개

//
//  Created by Jae hyung Kim on 4/27/24.
//

import UIKit
/*
 회고...!
 Masonry or Pinterest Style Layout
 collectionView.collectionViewLayout.invalidateLayout()
 */
protocol CustomPinterestLayoutDelegate: AnyObject {
    
    func collectionView(for collectionView: UICollectionView, heightForAtIndexPath indexPath: IndexPath) -> CGFloat
    
}

final class CustomPinterestLayout: UICollectionViewFlowLayout {
    
    weak var delegate: CustomPinterestLayoutDelegate?
    
    /// ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
    private let numberOfColums: Int // ν•œ ν–‰μ˜ μ•„μ΄ν…œ 갯수
    /// μ…€μ˜ νŒ¨λ”©
    private let cellPadding: CGFloat // μ…€μ˜ νŒ¨λ”©
    /// ν•„μš”μ˜ 경우 ν—€λ”μ˜ 높이 지정: Int
    private
    let headerHeight: CGFloat?
    
    
    init(numberOfColums: Int, cellPadding: CGFloat,_ headerHeight: CGFloat? = nil) {
        self.numberOfColums = numberOfColums
        self.cellPadding = cellPadding
        self.headerHeight = headerHeight
        super.init()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 1. μ»¬λ ‰μ…˜λ·° 컨텐츠 μ‚¬μ΄μ¦ˆλ₯Ό μ§€μ •ν•΄μ•Όν•œλ‹€.
    // 1.1 컨텐츠 λ†’μ΄μ˜ 그릇을 λ§Œλ“€κ³  (ν”„λ‘œνΌν‹°)
    private
    var contentsHeight: CGFloat = 0
    
    // Options λ‹€μ‹œ λ ˆμ΄μ•„μ›ƒμ„ 계산할 ν•„μš”κ°€ 없도둝 λ©”λͺ¨λ¦¬μ— μ €μž₯ν•œλ‹€.
    private
    var cache: [UICollectionViewLayoutAttributes] = []
    
    // 1.2 컨텐츠 넓이λ₯Ό μ •ν•˜λŠ”λ°...
    private
    var contetnsWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        // μ—¬λ°±μ˜ 정보λ₯Ό 받아와
        let insets = collectionView.contentInset
        // μ’Œμš°μ—¬λ°±μ„ λ”ν•œκ°’μ„
        let totalInset = insets.left + insets.right
        // μ»¬λ ‰μ…˜λ·°λ„“μ΄μ— 값을 λΉΌλ©΄. 넓이가 λ‚˜μ˜¨λ‹€.
        return collectionView.bounds.width - (totalInset)
    }
    
    // 1.END μ»¬λ ‰μ…˜λ·°μ˜ 컨텐츠 μ‚¬μ΄μ¦ˆλ₯Ό 지정
    override var collectionViewContentSize: CGSize {
        let minHeight = collectionView?.bounds.height ?? 0
        
        return CGSize(width: contetnsWidth, height: max(contentsHeight,minHeight))
    }
        
    // 2. μ»¬λ ‰μ…˜λ·°κ°€ 처음 μ΄ˆκΈ°ν™” λ˜κ±°λ‚˜, λ·°κ°€ λ³€κ²½λ λ•Œ μ‹€ν–‰λ©λ‹ˆλ‹€.
    // ... ν•΄λ‹Ή λ©”μ„œλ“œλŠ” λ ˆμ΄μ•„μ›ƒμ„ 미리 계산 ν•˜κ³   λ©”λͺ¨λ¦¬μ— μΊμ‰¬ν•˜μ—¬
    // ... λΆˆν•„μš”ν•œ 반볡적인 연산을 ν•˜λŠ”κ²ƒμ„ λ°©μ§€ν•˜λ„λ‘ ν•΄μ•Όν•œλ‹€.
    override func prepare() {
        guard let collectionView = collectionView else { return }
        if collectionView.numberOfSections == 0 || cache.isEmpty == false {
            cache.removeAll()
        }
        
        let cellWidth = contetnsWidth / CGFloat(numberOfColums)
        
        var yOffSet:[CGFloat] = []
        
        if let headerHeight {
            // 헀더 λ ˆμ΄μ•„μ›ƒ 속성
            let headerIndexPath = IndexPath(item: 0, section: 0)

            let headerAttribute = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: headerIndexPath)
            
            headerAttribute.frame = CGRect(
                x: 0,
                y: 0,
                width: collectionView.bounds.width,
                height: headerHeight
            ) // 헀더높이 지정
            
            cache.append(headerAttribute)
            // cell 의 Yμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
            yOffSet = [CGFloat](repeating: headerAttribute.frame.maxY, count: numberOfColums)
            
            yOffSet[0] = headerAttribute.frame.maxY
            
        } else {
            yOffSet = [CGFloat](repeating: 0, count: numberOfColums)
        }
        
        // cell 의 Xμœ„μΉ˜λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ°°μ—΄μž…λ‹ˆλ‹€.
        let xOffSet:[CGFloat] = [0, cellWidth]

        var colum: Int = 0 // ν˜„μž¬ ν–‰μ˜ μœ„μΉ˜
        
        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            // 인덱슀 패슀λ₯Ό 톡해
            let indexPath = IndexPath(item: item, section: 0)
            // 인덱슀 νŒ¨μŠ€μ— λ§žλŠ” μ…€μ˜ 크기λ₯Ό κ³„μ‚°ν•©λ‹ˆλ‹€.
            let customContentHeight = delegate?.collectionView(for: collectionView, heightForAtIndexPath: indexPath) ?? 100
            // 상항 νŒ¨λ”©(νŒ¨λ”© 2λ°°) 에 μ»¨ν…ŒνŠΈ 높이λ₯Ό λ”ν•œ 값은 높이
            let height = cellPadding * 2 + customContentHeight
            
            let frame = CGRect(
                x: xOffSet[colum],
                y: yOffSet[colum],
                width: cellWidth,
                height: height
            )
            
            // μƒμ†Œν•œ dx, dyκ°€ λ‚˜μ™”λŠ”λ°
            // dx: xμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> 쒌우 ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            // dy: yμΆ• λ°©ν–₯의 νŒ¨λ”© κ°’ -> μƒν•˜ ν”„λ ˆμž„ μ•ˆμœΌλ‘œ 이동
            let insetFrame = frame.insetBy(
                dx: cellPadding, dy: cellPadding
            )
            
            // κ³„μ‚°ν•œ Frameλ₯Ό 톡해 λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό λ°˜μ˜ν•˜κ³  캐쉬에 μ €μž₯
            let attribute = UICollectionViewLayoutAttributes(
                forCellWith: indexPath
            )
            // λ ˆμ΄μ•„μ›ƒμ •λ³΄λ₯Ό 반영
            attribute.frame = insetFrame
            cache.append(attribute)
            
            // μ»¬λ ‰μ…˜λ·°μ˜ 높이λ₯Ό λ‹€μ‹œ μ§€μ •ν•œλ‹€.
            // frame.maxY -> ν˜„μž¬ μ…€μ˜ ν”„λ ˆμž„μ΄ λλ‚˜λŠ” YμΆ• μœ„μΉ˜ -> μ…€ 상단 μœ„μΉ˜μ—μ„œ μ…€ 높이λ₯Ό λ”ν•œκ°’
            contentsHeight = max(contentsHeight, frame.maxY)
            // μƒˆλ‘œμš΄ μ…€μ˜ frame.maxY쀑 더큰 값을 μ„ νƒν•˜μ—¬ μ»¬λ ‰μ…˜λ·°μ˜ 전체 컨텐츠 높이λ₯Ό μ—…λ°μ΄νŠΈ
            
            yOffSet[colum] = yOffSet[colum] + height
            
            // λ‹€λ₯Έ 이미지 크기둜 인해, ν•œμͺ½μ—΄μ—λ§Œ 이미지가 좔가됨을 방지
            colum = yOffSet[0] > yOffSet[1] ? 1 : 0
            // λ§Œμ•½ 첫번째 μ—΄μ˜ 높이가 λ‘λ²ˆμ§Έ μ—΄μ˜ 높이보닀 크닀면
            // μƒˆ μ…€λ“€ λ‘λ²ˆμ§Έ 열에 λ°°μΉ˜ν•˜λŠ”λ°
            // μ•„λ‹μ‹œ μƒˆ 셀을 첫번째 열에 λ°°μΉ˜ν•œλ‹€.
        }
        
    }
    
    // 3. λͺ¨λ“  μ…€κ³Ό μ„œλΈŒ 뷰의 λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•œλ‹€. (Rect) 기반
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        
        for attribute in cache {
            // intersects λ©”μ„œλ“œλŠ” 두 μ‚¬κ°ν˜•μ΄ κ²ΉμΉ˜λŠ”μ§€ μ—¬λΆ€λ₯Ό λ°˜ν™˜
            if attribute.frame.intersects(rect){
                // μ…€ ν”„λ ˆμž„κ³Ό μš”μ²­κ³Ό κ΅μ°¨ν•œλ‹€λ©΄ 리턴 κ°’μ˜ μΆ”κ°€
                    visibleLayoutAttributes.append(attribute)
            }
        }
        
        return visibleLayoutAttributes
    }
    
    // 4. λͺ¨λ“  μ…€μ˜ λ ˆμ΄μ•„μ›ƒ 정보λ₯Ό λ¦¬ν„΄ν•œλ‹€. IndexPath 기반
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[indexPath.item]
    }
    
    
}

λŒμ•„λ³΄λ©°

결과적으둠, μ»€μŠ€ν…€ λ ˆμ΄μ•„μ›ƒμ„ 톡해 μ›ν•˜λŠ” UIλ₯Ό κ΅¬ν˜„ν•  수 μžˆμ—ˆμ§€λ§Œ,
이 λ°°μš°λŠ” κ³Όμ •μ—μ„œ λ ˆμ΄μ•„μ›ƒμ˜ 계산 둜직과 μ„±λŠ₯ μ΅œμ ν™”μ˜ μ€‘μš”μ„±μ„ 깊이 κΉ¨λ‹¬μ•˜μŠ΅λ‹ˆλ‹€.
λ˜ν•œ, UICollectionView의 λ ˆμ΄μ•„μ›ƒ μ‹œμŠ€ν…œμ˜ 이해도λ₯Ό λ†’μ΄λŠ” 데 큰 도움이 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

λ ˆμ΄μ•„μ›ƒ ν”„λ‘œμ„ΈμŠ€μ™€ 속성 관리 방법에 λŒ€ν•΄ μ‘°κΈˆμ΄λ‚˜λ§ˆ 더 κΉŠμ€ 이해λ₯Ό ν• μˆ˜ 있게 λ˜μ–΄μ„œ
κΈ°μ©λ‹ˆλ‹€. ν•˜λ£¨ 반 정도 이것에 ν• μ•  ν•˜κ²Œ λ˜μ—ˆμ§€λ§Œ κ·Έλž˜λ„ κ²°κ³Όκ°€ λ‚˜μ™€μ„œ κΈ°μ©λ‹ˆλ‹€.!!!

profile
IOS 개발자 μƒˆμ‹Ήμ΄
post-custom-banner

0개의 λŒ“κΈ€