책 검색 앱 만들기 (2)

오늘은 구현을 진행하는 것 보다 이전에 짠 코드를 조금 더 이해하고 Compositional Layout에 대해 조금 더 자세하게 이해하는 방향으로 공부했다.

 

Compositional Layout

이전에 컬렉션 뷰를 다루면서 Flow Layout은 몇번 사용해 봤으니 이번에는 컴포지셔널 레이아웃을 한번 활용해 보자.

 

먼저 컴포지셔널 레이아웃은 빠르고 유연하게 컬렉션 뷰를 구현할 수 있는 CollectionViewLayout의 한 종류이며 iOS 13.0 이상부터 지원하는 방식이다.

장점

  • 복잡한 레이아웃을 선언형 API로 간단하게 구축할 수 있다.
  • 하나의 컬렉션 뷰로 다양한 레이아웃을 구성할 수 있다.
  • 속도가 빠르다.

Compositional Layout 구성

컴포지셔널 레이아웃은 Item, Group, Section으로 구성되어있다.

Item은 하나의 Item (Cell) 이라고 할 수 있고

Group은 Item의 집합

Section은 여러 Group의 집합이라고 할 수 있다.

 

NSCollectionLayoutDimension을 통해 각각 레이아웃 사이즈를 지정할 수 있다.

.fractionalWidtg, .fractionalHeight : 부모 컴포넌트 크기에 비례해서 크기를 설정

.absolute : 고정된 크기를 사용할 때 설정

.estimated : 크기가 변동될 수 있을때 설정

// fractional
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))

// absolute
let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(56), heightDimension: .absolute(56))

// estimate
let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .estimated(100))

자 여기까지 살짝 컴포지셔널 레이아웃을 설명했다면 이제 오늘 수정한 코드를 가지고 어떻게 레이아웃을 설정했는지 보자

 

Compositional Layout 활용

먼저 최근 본 책을 보여줄 좌우로 스크롤 되는 레이아웃을 구현해보자

case 0:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)

let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(100), heightDimension: .estimated(130))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

section = NSCollectionLayoutSection(group: group)
section?.interGroupSpacing = 5
section?.orthogonalScrollingBehavior = .continuous

group에 원하는 사이즈를 설정하고 item은 fractionalWidth , fractionalHeight 를 각각 1로 설정하면 사이즈는 얼추 원하는 만큼 잡히게 된다. 여기서 나중에 변경하고 싶다면 쉽게 수정하면 된다.

 

가장 중요한 점은 바로바로

section?.orthogonalScrollingBehavior = .continuous

이 설정을 꼭 넣어주어야 하는 것.. 이것을 넣지 않으면 가로로 스크롤이 되지 않고 화면 밖에 넘어간 셀들이 밑으로 내려와 버린다..

 

이 설정을 못해서 거의 처음부터 다시 컴포지셔널 레이아웃을 잡았다고 해도 무방하다.. 그래도 덕분에 컴포지셔널 레이아웃에 대해 더 이해할 수 있게 된 것 같다.

 

이렇게 설정이 완료 됐다면 이렇게 가로로 스와이프가 되는 컬렉션 뷰가 만들어진다.

이제 다음 책 목록을 보여줄 세로로 스와이프 될 컬렉션 뷰의 컴포지셔널 레이아웃을 살펴보자

case 1:
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(170))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

section = NSCollectionLayoutSection(group: group)
section?.interGroupSpacing = 10

item 이 3개 들어갈 수 있도록 fractionalWidth 의 크기를 1/3 으로 설정해 3개의 셀이 들어갈 수 있도록 설정한다.

 

그리고 각 셀이 살짝 경계를 가지고 있기 위해 leading과 trailing을 5 씩 잡아 설정한다. 이걸 설정을 안했더니 나눠지지 않은 줄 알고.. 여러번 고쳤는데 알고보니 이미 설정이 다 된 상태였다..

 

이렇게 설정을 넣어주면 바로 이렇게 레이아웃 구성이 완료된다.

 

Compositional Layout Header 영역 설정

각각 컬렉션 뷰 마다 해당하는 컬렉션 뷰의 제목을 달아주기 위해 헤더 영역을 사용해 구성해 보자!

컬렉션 뷰에 헤더를 넣기위해서는 헤더를 SupplementaryView에 넣어주어야 한다.

let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(30))
let headerSupplementary = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section?.boundarySupplementaryItems = [headerSupplementary]

return section

이렇게 설정해주고 이제 헤더를 가지고 있을 셀을 새로 만들자!

class HeaderView: UICollectionReusableView {
    static let identifier = "HeaderView"
    
    let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.boldSystemFont(ofSize: 25)
        label.textColor = .black
        return label
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.leading.equalToSuperview().offset(5)
            $0.centerY.equalToSuperview()
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(title: String) {
        titleLabel.text = title
    }
}

헤더 셀을 만들고 헤더에 들어갈 레이블을 구성해 넣어준 뒤 레이아웃도 잡아준다.

 

그 이후 메소드에 들어가야할 label 을 저장해 VC로 전달하면 새로운 셀 만들기 완성

다시 VC로 넘어와 각각 헤더에 들어갈 텍스트를 배열을 통해 만들어준다.

let sectionTitles = ["최근 본 책", "Book List"]

이렇게 만들어진 타이틀을 각각 넣어주면 완성

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    guard kind == UICollectionView.elementKindSectionHeader else {
        fatalError("Unexpected supplementary view kind")
    }
    
    let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.identifier, for: indexPath) as! HeaderView
    
    let sectionTitle: String
    switch indexPath.section {
    case 0:
        sectionTitle = "최근 본 책"
    case 1:
        sectionTitle = "Book List"
    default:
        fatalError("Unknown section")
    }
    
    headerView.configure(title: sectionTitle)
    
    return headerView
}

CollectionView DataSource를 활용해 viewForSupplementaryElementOfKind 를 불러와 그 안에 헤더에 들어갈 값을 넣어주면 된다!

 

위에 컴포지셔널레이아웃도 그렇고 두개의 컬렉션 뷰를 스위치를 활용해 각각 케이스에 나눠 설정해뒀으니 헤더도 동일하게 설정하면 헤더 구성이 완료된다.

static 을 활용한 identifier 설정

오늘 시간을 많이 날린 실수가 있어서 정리하고 넘어가려고 한다. 오늘은 각 컬렉션 뷰 셀을 연결할때 각각 셀 파일 내 스테틱을 활용해 identifier를 설정해 두었었다.

 

하지만 이전에도 그냥 “” 을 사용해 identifier를 넣어주었었는데 여기에 오타가 있어서 하루 종일 앱이 빌드되지 않고 오류가 있었다..

 

혹시나 하는 마음에 그냥 스트링으로 넣어두었던 identifier를

 guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecentlyViewedCollectionViewCell.identifier, for: indexPath) as? RecentlyViewedCollectionViewCell else { fatalError("에러입니다.") } 

이런식으로 RecentlyViewedCollectionViewCell.identifier 호출하는 형식으로 사용했더니 앱이 정상적으로 잘 빌드될 수 있었다.

 

이러한 실수를 막기위해 앞으로 선언을 해서 안전하게 불러오는 방식을 사용해야겠다고 생각했다 🙂