본문 바로가기

iOS/힙한 UI 따라 만들기

iOS 힙한 UI 따라 만들기 Ep.03 "Frip" 홈 화면 UICompositionalLayout으로 만들기👋

오랜만에 3번째 에피소드로 돌아왔습니다 👋
이번에는 최근 업데이트된 Frip앱의 홈화면을 UICompositionalLayout으로 만들어보려고 합니다.
(물론 제가 몸담고 있어서 힙하다고 얘기하는 것은..아니고..흠흠)

요즘 커머스앱을 들어가면 대부분 이런방식으로 여러가지 형태의 내용들이 들어간 스크롤 형식의 뷰가 보여지곤합니다.
옛날에는 이런 화면들 도대체 어떻게 만들지 싶은 생각도 들고, 만들어도 무언가.. 엄청 복잡하게만 만들어져서 깔끔하게 만들어보고 싶다는 생각이 들곤 했었습니다.

이전에는 어떻게 만들었나?

UICompositionalLayout으로 만들기 전에 이런 화면들은 UICollectionViewCell안에 다른 UICollectionView를 정의하여 사용하곤 했습니다. 그렇게 만들고 나서서 동작하기는 했지만.. 무언가 찝찝함이 조금씩 남아 있긴 했었어요. 이렇게 해도 괜찮은건가? 이상하게 깔끔하지 않은듯한 기분이 항상 남아있었습니다. 하지만 그 외에 구현할 수 있는 다른 방법이 떠오르진 않아서 이 방법을 계속 사용중이였습니다.
하지만 iOS 13부터 이런 복잡한 UI를 간단하게 정의할 수 있는 UICompositionalLayout이 제공됩니다.  그래서 이번 기회에 UICompositionalLayout을 사용해 만들어보려고 해요!

UICompositionalLayout

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

UICompositionalLayout은 시각적으로 유연한 정렬 (복잡한 레이아웃)아이템을 묶어주는 레이아웃이라고 합니다.
해당 레이아웃을 이해하기 위해서는 먼저 Seciton, Group, Item이란 컴포넌트를 알아야 합니다. 

UICompositionalLayout 설명 (출처: Apple Developer Document )

큰 범위의 개념에서 작은 범위의 개념으로 접근하면 Section > Group > Item 순서입니다.
반대로 얘기하면 Item들이 모여서 Group을 구성하고, Group들이 모여서 Section을 구성합니다.
그래서 우리는 UICompositionalLayout을 정의할 때, Item, Group, Section들을 정의해서 알려주면 됩니다.
여기에서 Item이 우리가 이전에 UICollectionView에서 사용하던 Cell 하나라고 생각하면 이해하기 쉬워집니다.

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
}

공식 문서에 UICompositionalLayout를 정의하는 하나의 예시 코드가 있는데요 하나씩 확인해봅시다.

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


먼저 item을 정의를 해주는데요, widthDimestion과 heightDimension 값을 정해주는데 fractional___를 사용했습니다.
fractional__를 찾아보면 이렇게 설명해주고 있네요.

아이템을 포함하고 있는 Group의 비율로 계산해준다고 합니다. 그러니까 부모의 영역을 따라간다는 얘기겠네요!
1.0으로 설정했으니 그룹의 width와 같은 값을 가지겠군요. Height도 마찬가지구요!

let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),                                          
                                          heightDimension: .absolute(44))    
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,                                                   
                                                     subitems: [item]) // 그룹을 구성하는 아이템 종류

Item 사이즈를 결정했으니 그 다음은 Group 정의해봅시다.
Group도 Item과 마찬가지로 사이즈를 결정해주는데 여기에서는 heightDimension을 .absolute(44)로 설정했네요. 그룹은 절대값 44의 높이를 가지도록 설정해주었습니다.

Group을 정의할 때 그룹 안에 어떤 아이템이 들어가는지도 같이 정의해주면서 아이템의 정렬 방향도 정해주었습니다.
NSCollectionLayoutGroup.horizontal로 정의를 했네요. 아이템들이 수평으로 나열이 된다는 의미겠죠?

let section = NSCollectionLayoutSection(group: group)    

let layout = UICollectionViewCompositionalLayout(section: section)

Group까지 모두 정의했고 마지막으로 Group으로 구성된 Section을 구성해줍니다.
여기 문장에서는 별다른 옵션 설정 없이 바로 섹션을 구성해주었네요.
이렇게 정의한 레이아웃은 아래 사진처럼 구성될 것이라 예상합니다.

Item의 width와 Group의 Width가 같으므로 Group안에 Item이 하나씩만 들어갈 것입니다. 그리고 Group이 수평 방향으로 나열되었으므로 횡스크롤이 될 것입니다.


그럼 이제 Frip 홈 화면을 만들어볼까요?

몇개의 섹션으로 만들까요?

홈 화면을 그려보기 전에 머리를 먼저 굴려봅시다. 아래 사진처럼 홈 화면 일부의 화면만 그린다고 해볼께요!
Section, Group, Item을 어떻게 구성하면 될까요?

홈 화면의 일부

위에서 보여지는 화면을 나누어본다면 크게 두가지 섹션으로 나눌겁니다.
섹션을 나눈 기준은, 셀 종류에 따라서 나누었어요.
첫번째 섹션은 횡스크롤이 가능한 섹션이고 두번째 섹션은 상품들이 보여지는 섹션입니다.

첫번째 섹션은 다음과 같이 정의했습니다.

private func createShortcutSection() -> NSCollectionLayoutSection {
    let cellSize = CGSize(width: 58, height: 82)
    let item = NSCollectionLayoutItem(layoutSize: .init(
        widthDimension: .absolute(cellSize.width),
        heightDimension: .absolute(cellSize.height)
    ))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(
        widthDimension: .absolute(cellSize.width),
        heightDimension: .absolute(cellSize.height)
    ), subitems: [item])  // Group의 width와 item의 width가 같도록 설정
    let section = NSCollectionLayoutSection(group: group)
        
    section.orthogonalScrollingBehavior = .continuous
    section.contentInsets = .init(top: 12, leading: 10, bottom: 24, trailing: 10)
    return section
}

Item의 사이즈와 Group의 사이즈가 같게 설정해주었고, 횡스크롤이 가능해야 하므로 orthogonalScrollingBehavior 값을 .continuous로 설정해주었습니다.

두번째 섹션은 다음과 같이 정의했습니다.

private func createProductCollectionSection() -> NSCollectionLayoutSection {
    let cellSize = CGSize(width: 187, height: 350)
    let item = NSCollectionLayoutItem(layoutSize: .init(
        widthDimension: .absolute(cellSize.width),
        heightDimension: .absolute(cellSize.height)
    ))
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(
        widthDimension: .fractionalWidth(1),
        heightDimension: .estimated(cellSize.height)
    ), subitems: [item])
        
    group.interItemSpacing = .fixed(1)
    let section = NSCollectionLayoutSection(group: group)
    
    // 누구보다 빠르게... 로 시작하는 헤더 뷰 추가
    section.boundarySupplementaryItems = [
        .init(
            layoutSize: .init(
                widthDimension: .fractionalWidth(1),
                heightDimension: .absolute(200)
            ),
            elementKind: UICollectionView.elementKindSectionHeader,
            alignment: .top
        )
    ]
        
    return section
}

두번째로 보여지는 섹션은 수직 방향으로 스크롤이 되어야 하고 한 줄에 2개의 상품이 노출되어야 합니다.
그래서 1개의 Group 안에 2개의 Item을 넣어두고 Group이 수직방향으로 나열되도록 정의했습니다.
하나의 그룹은 디바이스 화면 폭의 전체를 먹어야 하기 때문에 widthDimension 을 .fractionalWidth(1)로 설정했습니다.
또한 하나의 Group 안에 Item들은 왼쪽부터 수평으로 나열되어야 하므로 NSCollectionLayoutGroup.horizontal로 설정했습니다.

이제 두 섹션을 가지고 있는 UICompositionalLayout을 정의해줍시다.

private func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
    let layout = UICollectionViewCompositionalLayout { sectionIndex, _ in
        switch HomeSectionType(sectionIndex: sectionIndex) {                
        case .shortcut:
            return self.createShortcutSection()
                
        case .productCollection:
            return self.createProductCollectionSection()
                
        case .unknown:
            return nil
        }
    }        
    return layout
}

위에서 선언한 두개의 함수를 사용해서 UICompositionalLayout을 정의하고 CollectionView에 레이아웃으로 설정해주면 끝납니다!
이외에 나머지 데이터 바인딩은 기존의 UICollectionView와 동일하기 때문에 생략합니다.

새로운 API를 사용해본 경험

iOS 13부터 제공되어서 늦은감이 있긴 하지만,,, 이제서라도 사용해볼 수 있어서 좋았어요!
무엇보다도 지저분하다고 생각되었던 레이아웃 코드를 깔끔하게 정리할 수 있어서 편-안함을 느꼈습니다.
사용해보면서 불편하다고 생각한 점이 하나 있었어요. 위에서 레이아웃을 정의한 코드처럼 sectionIndex를 가지고 인덱스별로 섹션을 정의할 수 있었는데 섹션도 데이터에 따라 유동적으로 변하게 할 수 있다면 더 좋겠다고 생각했습니다. (현재 문서상의 API에서 제공되는 것으로는 불가능하다고 생각됩니다..)