UIScrollView 스크롤이 작동을 안해요!! UICollectionView Compositional Layout으로 정교한 스크롤 기능 구현하기

RYEOL·2024년 8월 1일

Swift

목록 보기
10/15
post-thumbnail

여기서 잠깐! UICollectionView Compositional Layout으로 전환해보셨나요?

여러분, UICollectionView를 사용하면서 한 번쯤은 "이렇게 하면 더 깔끔하지 않을까?" 고민해본 적 있지 않으신가요? 특히 스크롤 뷰 안에 컬렉션 뷰를 넣었을 때 스크롤 동작이 어색하거나 오류가 발생했을 때는 문제가 더 심각하게 느껴지죠. 이번 포스트에서는 이 문제를 해결하기 위해 UICollectionView Compositional Layout으로의 전환 과정을 소개해드리고자 합니다.

Compositional Layout 소개

UICollectionViewCompositionalLayout는 iOS 13에서 도입된 새로운 레이아웃 시스템입니다. 이 시스템은 UICollectionView의 레이아웃을 더 쉽게 구성하고, 다양한 레이아웃을 조합하여 더욱 복잡하고 유연한 사용자 인터페이스를 구현할 수 있도록 도와줍니다.

프로젝트 예제

이제 실제로 어떻게 UIScrollView에서 UICollectionView로 전환했는지 코드 예제를 통해 살펴보겠습니다.

1. Compositional Layout 생성

가장 먼저 해야 할 일은 UICollectionView의 레이아웃을 정의하는 것입니다. 각 섹션(Section)을 정의하고 각 섹션마다 다른 레이아웃을 적용할 수 있습니다.

private func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, layoutEnvironment in
        guard let self = self else { return nil }
        
        let sectionType = self.dataSource.snapshot().sectionIdentifiers[sectionIndex]
        switch sectionType {
        case .navigation:
            return self.createNavigationSection()
        case .carousel:
            return self.createCarouselSection()
        case .menu:
            return self.createMenuSection()
        case .pharmacyMap:
            return self.createPharmacyMapSection()
        }
    }
    return layout
}

위 코드는 컴포지셔널 레이아웃을 생성하는 함수입니다. sectionType에 따라 각 섹션에 맞는 레이아웃을 반환합니다.

2. 각 섹션의 레이아웃 정의

각 섹션의 레이아웃을 정의하여 다양한 레이아웃을 구현할 수 있습니다.

2.1. 네비게이션 섹션

private func createNavigationSection() -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 24, bottom: 0, trailing: 24)
    
    return section
}

이 코드는 네비게이션 섹션의 레이아웃을 정의합니다. 아이템과 그룹의 크기를 지정하고, 섹션의 인셋을 설정합니다.

2.2. 캐러셀 섹션

private func createCarouselSection() -> NSCollectionLayoutSection {
    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(300))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .groupPaging
    section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0)
    
    return section
}

캐러셀 섹션은 가로 스크롤이 가능한 그룹 페이징 동작을 갖도록 설정합니다. 이를 통해 캐러셀 뷰가 자연스럽게 구동됩니다.

2.3. 메뉴 섹션

private func createMenuSection() -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(160))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(160))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item])
    group.interItemSpacing = .fixed(16)
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 24, bottom: 0, trailing: 24)
    
    return section
}

메뉴 섹션은 두 개의 아이템이 가로로 나란히 배치되는 구조입니다.

2.4. 약국 지도 섹션

private func createPharmacyMapSection() -> NSCollectionLayoutSection {
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(160))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(160))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 24, leading: 24, bottom: 24, trailing: 24)
    
    return section
}

약국 지도 섹션은 단일 아이템으로 구성된 그룹입니다. 이를 통해 약국 지도를 한 화면에 집중적으로 보여줄 수 있습니다.

3. 데이터 소스 구성

데이터 소스를 구성하여 각 섹션에 아이템을 추가합니다.

private func configureDataSource() {
    let navigationCellRegistration = UICollectionView.CellRegistration<CustomNavigationTitleCell, String> { cell, indexPath, item in
        cell.configure(with: item)
    }
    
    let carouselCellRegistration = UICollectionView.CellRegistration<CarouselCell, CarouselItem> { cell, indexPath, item in
        cell.configure(with: item.content)
    }. 

    let menuCellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, String> { cell, indexPath, item in
        cell.backgroundColor = .systemGray6
        cell.layer.cornerRadius = 16
    }
    
    let pharmacyMapCellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, String> { cell, indexPath, item in
        cell.backgroundColor = .systemGray6
        cell.layer.cornerRadius = 16
    }
    
    dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { collectionView, indexPath, item in
        let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
        switch section {
        case .navigation:
            return collectionView.dequeueConfiguredReusableCell(using: navigationCellRegistration, for: indexPath, item: item as? String)
        case .carousel:
            return collectionView.dequeueConfiguredReusableCell(using: carouselCellRegistration, for: indexPath, item: item as? CarouselItem)
        case .menu:
            return collectionView.dequeueConfiguredReusableCell(using: menuCellRegistration, for: indexPath, item: item as? String)
        case .pharmacyMap:
            return collectionView.dequeueConfiguredReusableCell(using: pharmacyMapCellRegistration, for: indexPath, item: item as? String)
        }
    }
    
    var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
    snapshot.appendSections([.navigation, .carousel, .menu, .pharmacyMap])
    snapshot.appendItems(["Navigation"], toSection: .navigation)
    snapshot.appendItems(["Search", "Notification"], toSection: .menu)
    snapshot.appendItems(["Pharmacy Map"], toSection: .pharmacyMap)
    dataSource.apply(snapshot, animatingDifferences: false)
    
    setupInfiniteScroll()
}

이 코드에서는 네 가지 섹션에 대해 셀을 등록하고, 스냅샷을 통해 섹션과 아이템을 추가합니다.

프로젝트 결과물 비교

UIScrollViewCompositional Layout

결론

UICollectionView Compositional Layout을 이용하면 복잡한 레이아웃도 쉽게 구성할 수 있으며, 다양한 섹션을 한 화면에 배치해도 자연스럽게 동작합니다. 이번 포스트에서 소개한 예제 코드를 통해 여러분도 프로젝트에 유연한 레이아웃을 구현해보세요! 새로운 레이아웃 시스템을 통한 더 나은 사용자 경험을 기대해봅시다. 🚀

profile
Flutter, Swift 모바일 개발자의 스타트업에서 살아남기

0개의 댓글