iOS

Compositional Layout으로 제보하기 화면 만들기

서서리 2024. 8. 28. 20:14
안녕하세요. 한끼족보의 iOS 개발자 서은수입니다! 👋🏻
이번에 iOS 파트의 두번째 타자로 아티클을 작성하게 되었는데요.
제가 이야기 하고 싶은 주제는 Compositional Layout 입니다.

 

 

UICollectionViewCompositionalLayout | Apple Developer Documentation

A layout object that lets you combine items in highly adaptive and flexible visual arrangements.

developer.apple.com

 

제가 했던 한끼족보의 초기 '제보하기' 뷰 스케치는 아래와 같습니다.
초기 제보하기 화면의 iOS 뷰 스케치 (물론 지금은 이것과 달라졌습니다...)
뷰를 하나씩 뜯어볼까요?
우선 가장 먼저 보였던 건 '식당을 등록해주세요', '식당의 종류를 알려주세요', '메뉴를 추가해주세요'는 텍스트만 다른 같은 스타일의 Label로, Header로 넣을 수 있다는 것이었습니다.
그리고 식당 종류 태그와 메뉴 이름 및 가격 부분은 컬렉션뷰 셀로 만들 수 있을 것 같아요!

 

저는 공통 스타일의 헤더가 들어간다는 점과 컬렉션뷰가 여러 번 들어간다는 점에서 Compositional Layout을 도입하게 되었습니다.

 

Compositional Layout을 사용해보신 분들은 아시겠지만 주로 쓰이는 코드의 형태가 비슷합니다.

저는 이 부분을 함수화 해서 코드의 중복과 가독성을 높이고자 했습니다.

//  CompositionalLayoutFactory.swift

enum SupplementaryItemType {
    case header
    case footer
}

// MARK: - Compositional Layout

class CompositionalLayoutFactory {
    
    /// Item 생성 시 사용합니다.
    func createItem(
        widthDimension: NSCollectionLayoutDimension,
        heightDimension: NSCollectionLayoutDimension,
        contentInsets: NSDirectionalEdgeInsets = .zero
    ) -> NSCollectionLayoutItem {
        let itemSize = NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: heightDimension)
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets = contentInsets
        return item
    }
    
    /// Group 생성 시 사용합니다.
    func createGroup(
        item: [NSCollectionLayoutItem],
        widthDimension: NSCollectionLayoutDimension,
        heightDimension: NSCollectionLayoutDimension,
        contentInsets: NSDirectionalEdgeInsets = .zero
    ) -> NSCollectionLayoutGroup {
        let groupSize = NSCollectionLayoutSize(widthDimension: widthDimension, heightDimension: heightDimension)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: item)
        group.contentInsets = contentInsets
        return group
    }
    
    /// Header 또는 Footer 생성 시 사용합니다.
    /// - type에 .header 또는 .footer를 작성
    func createBoundarySupplementaryItem(
        type: SupplementaryItemType,
        widthDimension: NSCollectionLayoutDimension,
        heightDimension: NSCollectionLayoutDimension,
        alignment: NSRectAlignment = .top
    ) -> NSCollectionLayoutBoundarySupplementaryItem {
        return NSCollectionLayoutBoundarySupplementaryItem(
            layoutSize: .init(
                widthDimension: widthDimension,
                heightDimension: heightDimension
            ),
            elementKind: type == .header ? UICollectionView.elementKindSectionHeader : UICollectionView.elementKindSectionFooter,
            alignment: alignment
        )
    }
    
    /// Section 생성 시 사용합니다.
    func createLayoutSection(
        group: NSCollectionLayoutGroup,
        orthogonalScrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .none,
        sectionContentInsets: NSDirectionalEdgeInsets = .zero,
        boundarySupplementaryItems: [NSCollectionLayoutBoundarySupplementaryItem]? = nil
    ) -> NSCollectionLayoutSection {
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = orthogonalScrollingBehavior
        section.contentInsets = sectionContentInsets
        if let supplementaryItems = boundarySupplementaryItems {
            section.boundarySupplementaryItems = supplementaryItems
        }
        return section
    }
}

CompositionalLayoutFactory라는 파일을 따로 만들어서 클래스 안에 item, group, section, header 및 footer를 만드는 함수를 작성했습니다.

 

그리고 제보하기 화면(ReportViewController)에서 collectionView를 아래와 같이 생성하는데요!

//  ReportViewController.swift

private let compositionalfactory: ReportCompositionalLayoutFactory = ReportCompositionalLayoutFactory()
private lazy var compositionalLayout: UICollectionViewCompositionalLayout = compositionalfactory.create()
private lazy var collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout)

UICollectionView의 collectionViewLayout 인자로 들어가는 UICollectionViewCompositionalLayout 타입을

ReportCompositionalLayoutFactory라는 인스턴스의 create 함수로부터 얻습니다.

 

이제부터 이 ReportCompositionalLayoutFactory 클래스를 설명드릴게요!

코드는 아래와 같습니다.

//  ReportCompositionalLayoutFactory.swift

enum ReportSectionType: Int {
    case search
    case category
    case image
    case menu
    case addMenu
}

final class ReportCompositionalLayoutFactory: CompositionalLayoutFactory {
    
    var isImageSelected: Bool = false
    
    func create() -> UICollectionViewCompositionalLayout {
        return UICollectionViewCompositionalLayout { [self] (sectionIndex, _) -> NSCollectionLayoutSection? in
            guard let sectionType: ReportSectionType = ReportSectionType(rawValue: sectionIndex) else {
                return nil
            }
            let section: NSCollectionLayoutSection
            switch sectionType {
            case .search:
                section = getSearchLayoutSection()
            case .category:
                section = getCategoryLayoutSection()
            case .image:
                section = getImageLayoutSection()
            case .menu:
                section = getMenuLayoutSection()
            case .addMenu:
                section = getAddMenuLayoutSection()
            }
            return section
        }
    }
}

extension ReportCompositionalLayoutFactory {
    
    // MARK: - Search Section
    
    func getSearchLayoutSection() -> NSCollectionLayoutSection {
        let item = createItem(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(112))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(112))
        let section = createLayoutSection(group: group, orthogonalScrollingBehavior: .groupPaging)
        return section
    }
    
    // MARK: - Category Section

    func getCategoryLayoutSection() -> NSCollectionLayoutSection {
        let header = createBoundarySupplementaryItem(type: .header, widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50), alignment: .topLeading)
        let item = createItem(widthDimension: .estimated(50), heightDimension: .fractionalHeight(1))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(36))
        group.interItemSpacing = .fixed(6)
        let section = createLayoutSection(group: group, sectionContentInsets: .init(top: 16, leading: 22, bottom: 24, trailing: 10), boundarySupplementaryItems: [header])
        section.interGroupSpacing = 8

        return section
    }
    
    // MARK: - Image Section

    func getImageLayoutSection() -> NSCollectionLayoutSection {
        let header = createBoundarySupplementaryItem(type: .header, widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(53), alignment: .topLeading)
        let item = createItem(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(21 + 24 + (isImageSelected ? 84 : 58) + 24))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(21 + 24 + (isImageSelected ? 84 : 58) + 24))
        let section = createLayoutSection(group: group, sectionContentInsets: .init(top: 0, leading: 22, bottom: 0, trailing: 22), boundarySupplementaryItems: [header])
        return section
    }
    
    // MARK: - Menu Section
    
    func getMenuLayoutSection() -> NSCollectionLayoutSection {
        let item = createItem(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(95))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(95))
        let section = createLayoutSection(group: group, sectionContentInsets: .init(top: 0, leading: 22, bottom: 0, trailing: 14))
        section.interGroupSpacing = 4
        return section
    }
    
    // MARK: - Add Menu Section
    
    func getAddMenuLayoutSection() -> NSCollectionLayoutSection {
        let footer = createBoundarySupplementaryItem(type: .footer, widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(80), alignment: .bottom)
        let item = createItem(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(32))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(32))
        let section = createLayoutSection(group: group, sectionContentInsets: .init(top: 8, leading: 14, bottom: 52, trailing: 22), boundarySupplementaryItems: [footer])
        return section
    }
}

이 ReportCompositionalLayoutFactory는 위에서 만들었던 CompositionalLayoutFactory 클래스를 상속받습니다.

이를 통해 CompositionalLayoutFactory 클래스 안에 있는 함수를 사용할 수 있어요.

 

우선 제보하기 섹션은 총 5개이고 ReportSectionType라는 enum으로 타입을 설정합니다.

 

create 함수에서는 UICollectionViewCompositionalLayout 타입을 반환합니다. 섹션마다 다른 함수를 호출해서 section 값을 할당하고 반환합니다.

 

섹션이 총 5개로 각각 다른 함수를 통해 section 값을 받아옵니다.

예를 들어 getCategoryLayoutSection 함수를 설명하자면 이 함수는 세번째 섹션을 만드는 함수입니다.

세번째 섹션의 구조는 이와 같아요. header와 item이 필요합니다!

 

// MARK: - Category Section

    func getCategoryLayoutSection() -> NSCollectionLayoutSection {
        let header = createBoundarySupplementaryItem(type: .header, widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50), alignment: .topLeading)
        let item = createItem(widthDimension: .estimated(50), heightDimension: .fractionalHeight(1))
        let group = createGroup(item: [item], widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(36))
        group.interItemSpacing = .fixed(6)
        let section = createLayoutSection(group: group, sectionContentInsets: .init(top: 16, leading: 22, bottom: 24, trailing: 10), boundarySupplementaryItems: [header])
        section.interGroupSpacing = 8

        return section
    }

그래서 함수 안에서 CompositionalLayoutFactory에 있는 함수를 통해 header를 만들고, item과 group, section을 순서대로 레이아웃을 맞춰 만들 수 있습니다.

 

이렇게 섹션에 따라 다른 함수를 호출함으로써 create 함수의 가독성을 높일 수 있습니다.

 

 

이러한 방식으로 한끼족보 iOS는 Compositional Layout 작성 코드를 분산시켜 파일 하나가 너무 길어지는 것을 방지하고, 불필요한 로직을 숨겼습니다.

 

아직 많이 부족하지만 앞으로 더 좋은 코드를 만들어나가는 개발자가 되도록 노력하겠습니다!

한끼족보 iOS의 깃허브 레포지토리를 첨부해둘 테니 많이 봐주시고 피드백 주시면 감사하겠습니다 🧡

 

GitHub - Team-Hankki/hankki-iOS: 한끼줍쇼아니라고52817번말했다

한끼줍쇼아니라고52817번말했다. Contribute to Team-Hankki/hankki-iOS development by creating an account on GitHub.

github.com

 

'iOS' 카테고리의 다른 글

한끼족보 iOS의 프로젝트 초기세팅  (3) 2024.08.08