본문 바로가기

Swift/UIKit

Compositional Layout으로 여러가지 모양(?) 적용기

난 Compositional Layout을 다룰줄 몰랐다

정확히는 Compositional Layout으로 collectionView를 만들었지만 한 collection view 안에 여러 섹션으로 모양을 다양하게 다룰줄 몰랐다.

 

그런데 이번에 디자인 기획안이 너무나도 Compositional Layout으로 구현하기 좋게 나왔고 안드로이드 개발자분한테 swift에는 이런 방식이 있다 말씀드리니 자기도 그런 비슷한 걸 시도해볼까 한다고 하셔서 같이 도전하기로 했다(물론 각자지만 마음만은 ㅋ)

 

맨 하단에 전체 코드를 첨부할거지만 사실 뭘... 딱히 이해하기에 크게 어렵진 않았던 것 같다

왜냐면 단일 collectionView 선언에서 section만 여러개 만드는 느낌? 인 것 같아서 그럴수도..

(그냥 평소에 겁먹고 공부하기를 미뤘는데 반성한다...!)

 

각설하고 가장 중요한 부분은 CompositionalLayout을 반환하는 부분인 것 같은데

    private func createLayout() -> UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in
            switch sectionIndex {
            case 0:
                return self?.firstPurchaseSectionLayout()
            case 1:
                return self?.sixItemsSectionLayout()
            default:
                return self?.threeItemsSectionLayout()
            }
        }
    }

이렇게 section Index에 따라 각각의 sectionLayout을 반환해 그에 맞는 (내가 원하는?) 디자인으로 collection view를 만들 수 있다

 

결과 화면

 

전체 코드

- CollectionView 선언 부

import UIKit
import SnapKit
import SwiftUI

final class HomeSectionView: UIView {
    init() {
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var sectionCollectionView = UICollectionView()
    
    func setViewContens() {
        self.sectionCollectionView = createSectionCollectionView()
        setLayout()
    }
    
    private func setLayout() {
        self.addSubview(sectionCollectionView)
        
        sectionCollectionView.snp.makeConstraints {
            $0.top.leading.trailing.bottom.equalToSuperview()
            $0.height.equalTo(calculateCVHeigt())
        }
    }
    
    let sections: [SectionData] = HomeSectionDummyData.sections

}

extension HomeSectionView: UICollectionViewDataSource, UICollectionViewDelegate {
    func calculateCVHeigt() -> Double {
        var height = 0.0
        
        for section in self.sections {
            height += section.sectionCase.height
        }
        
        return height
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return sections.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return sections[safe: section]?.cellInfo.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "\(HomeFirstPurchaseCVCell.self)", for: indexPath) as? HomeFirstPurchaseCVCell,
              let cellInfo = sections[safe:indexPath.section]?.cellInfo[safe: indexPath.row] else {
            return UICollectionViewCell()
        }
        
        cell.setCellContents(color: cellInfo)
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
         if kind == UICollectionView.elementKindSectionHeader {
             if indexPath.section == 0 {
                 guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "\(secondHeaderView.self)", for: indexPath) as? secondHeaderView else {
                     return secondHeaderView()
                 }
                 
                 return header
             } else {
                 guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "\(firstHeaderView.self)", for: indexPath) as? firstHeaderView else {
                     return firstHeaderView()
                 }
                 
                 return header
             }
         } else {
             return firstHeaderView()
         }
     }
}

//MARK: - make collectionView layout
extension HomeSectionView {
    private func createSectionCollectionView() -> UICollectionView {
        let layout = createLayout()
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        
        collectionView.register(HomeFirstPurchaseCVCell.self, forCellWithReuseIdentifier: "\(HomeFirstPurchaseCVCell.self)")
        
        collectionView.register(firstHeaderView.self,
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: "\(firstHeaderView.self)")
        
        collectionView.register(secondHeaderView.self,
                                forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
                                withReuseIdentifier: "\(secondHeaderView.self)")
        
        collectionView.isScrollEnabled = false
        return collectionView
    }
    
    private func createLayout() -> UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in
            switch sectionIndex {
            case 0:
                return self?.firstPurchaseSectionLayout()
            case 1:
                return self?.sixItemsSectionLayout()
            default:
                return self?.threeItemsSectionLayout()
            }
        }
    }
    
    private func firstPurchaseSectionLayout() -> NSCollectionLayoutSection {
        let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.8))
        let item = NSCollectionLayoutItem(layoutSize: size)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.8))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPaging
        
        return section
    }
    
    private func sixItemsSectionLayout() -> NSCollectionLayoutSection {
        let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1 / 2), heightDimension: .fractionalWidth(1 / 2))
        let item = NSCollectionLayoutItem(layoutSize: size)
        item.contentInsets = .init(top: 0, leading: 5, bottom: 16, trailing: 5)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalWidth(1 / 2))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        let sectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(60))
        section.boundarySupplementaryItems = [.init(layoutSize: sectionHeaderSize,
                                              elementKind: UICollectionView.elementKindSectionHeader,
                                              alignment: .topLeading)]
        section.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 15)
        return section
    }
    
    private func threeItemsSectionLayout() -> NSCollectionLayoutSection {
        let size = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(0.3))
        let item = NSCollectionLayoutItem(layoutSize: size)
        item.contentInsets = .init(top: 5, leading: 20, bottom: 5, trailing: 20)
        
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
                                               heightDimension: .fractionalWidth(0.3))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        let sectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50))
        section.boundarySupplementaryItems = [.init(layoutSize: sectionHeaderSize,
                                              elementKind: UICollectionView.elementKindSectionHeader,
                                              alignment: .topLeading)]
        return section
    }
}


final class firstHeaderView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .red
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

final class secondHeaderView: UICollectionReusableView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .blue
    }

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

 

- cell 선언부

import UIKit
import SnapKit

final class HomeFirstPurchaseCVCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setCellContents(color: UIColor) {
        self.backgroundColor = color
    }
}

 

- Section Data 선언부(현재는 더미 데이터지만 추후 request 구현 후 대체 예정)

import UIKit

struct HomeSectionDummyData {
    static let sections = [
        SectionData(sectionCase: .firstPurchase, title: "첫 구매 ONLY 990원 특가!", cellInfo: [.systemBlue ]), //firstSection
        SectionData(sectionCase: .sixItemSection, title: "0원 샘플로 제품을 체험해 보세요!", cellInfo: [.red, .orange, .yellow, .green, .blue, .purple]),
        SectionData(sectionCase: .threeItemSection, title: "다른 사람들은 이런 화장품을 보고 있어요", cellInfo: [.systemGreen, .mainBlue, .warning])
    ]
}

struct SectionData {
    let sectionCase: SectionCase
    let title: String?
    let cellInfo: [UIColor]
}

enum SectionCase {
    case firstPurchase
    case sixItemSection
    case threeItemSection
    
    //heigt의 경우 figma디자인에 따라 높이 그대로 가도 되면 figma의 높이로
    //높이를 그대로 가져가기 애매할 경우 계상된 높이로
    var height: Double {
        switch self {
        case .firstPurchase:
            return (UIScreen.main.bounds.width * 0.8)
        case .sixItemSection:
            return (UIScreen.main.bounds.width * 1.55)
        case .threeItemSection:
            return (UIScreen.main.bounds.width * 1.02)
        }
    }
}

 

 

추가로 group을 horizontal이 아닌 vertical로 구현하면 

 

 

이렇게도 가넝

참고
 

iOS) UICollectionView custom layout에 대한 고찰- 2 (UICollectionViewCompositionalLayout)

iOS) UICollectionView custom layout에 대한 고찰- 1 (UICollectionViewFlowLayout, UICollectionViewLayout) iOS) UICollectionView custom layout에 대한 고찰- 1 (UICollectionViewFlowLayout, UICollectionViewLayout) Collection View에서 복잡한 레이

demian-develop.tistory.com