본문 바로가기

iOS/Deep dive!!!

[iOS] 컬랙션 뷰 Flow -> Compositional layout으로 리펙터링하기 #1

 

안녕하세요.

 

이번 포스트에서 컬랙션 뷰 flow layout으로 구현했던 기존 여행 앱 피드 화면을 compositional layout으로 리빌딩한 경험을 공유하려고 합니다: ]

리빌딩을 하게된 이유

기존 화면
새롭게 개선된 화면


왼쪽 동영상은 기존에 구현했던 화면입니다.
그리고 디자이너분이 새롭게 오른쪽 동영상처럼 더 이쁜 디자인을 만들어 주셨습니다. 피드 홈에서는 사용자의 여행 후기를 보여주는 포스트 cell로 이루어진 컬랙션 뷰로 위 화면이 구성되어있는데 기존 구현은 컬랙션 뷰의 flow layout을 이용했습니다.

TableView vs CollectionView 선택 과정..


피드의 cell 구성입니다. 컬랙션 뷰를 선택한 이유는 cell의 높이가 서로 다르기 때문입니다. (초기에는 thumbnail 영역 뷰 높이 또한 이미지 개수 여부에 따라 달랐습니다.) 서로 다른 데이터에 따라 dynamic한 cell의 높이를 제공하고자 UICollectionViewDelegateFlowLayout 프로토콜을 활용해서 동적으로 cell 높이를 계산하는 방법이 괜찮겠다고 생각했습니다.  물론 테이블 뷰 셀 또한 쉽게 self-resizing이 가능합니다. (cell's self-resizing관련 개념 활용 포스트 정리 링크)

 

물론 이 화면의 경우는 위에서 아래로 스크롤되기에, 테이블 뷰와 tableView.estimatedRowHeight 속성을 이용해서 테이블 뷰가 자동으로 런타임시 계산하는 선택지도 있었습니다. 그러나 위에 언급한 flow layout의 델리게이트를 활용해 더 정확하게 계산해주고 싶었기 때문에 선택했었습니다.

CollectionView Flow layout vs Compositional layout

 

새롭게 개선된 화면에서는 기존에 없던 cell 영역에 shadow가 추가 됬고 화면의 맨 위, 맨 아래에 cornerRadius가 추가됬습니다. 그래서 기존에 구현한 컬랙션 뷰 자체에 Inset과 radius, shadow를 주면 빠르게 새롭게 디자인된 화면을 구성할 수 있었습니다.

 

 

팀원이 구현한 이 화면의 일부에서도 사용되는데, 컴포지셔널로 구현했기 때문에,,, 저도 피드의 cell 영역을 플로우 레이아웃에서 컴포지셔널 레이아웃으로 전환하기로 결정했습니다.

 

Compositional layout의 장점 및 느낀점

  1. 위 사진처럼 item, group, section의 조화를 통해 가로 세로 스크롤 여부에 유연하게 대응할 수 있습니다. (view depth 단축)
  2. tableView 처럼 estimated를 활용해 item의 크기를 추정하고 런타임시 동적으로 item 크기를 계산해 화면에 보여줍니다.
  3. decorationView를 통해 section의 view를 커스텀할 수 있습니다.

 

 

저는 flow layout delegate 메서드를 활용해 데이터에 따라 동적으로 텍스트 높이를 계산해 cell에 적용했던 코드를 제거할 수 있었습니다. 

 

private var postSection: NSCollectionLayoutSection {
  typealias LayoutSize = NSCollectionLayoutSize
  typealias Const = Constants.Layout
  let groupHeight = Const.estimatedCellHeight // 289
  let itemSize = LayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(groupHeight))
  let groupSize = LayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(groupHeight))
  let item = NSCollectionLayoutItem(layoutSize: itemSize)
  let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
  let section = NSCollectionLayoutSection(group: group)

  // decorationView 추가
  let whiteRoundView = NSCollectionLayoutDecorationItem.background(elementKind: InnerRoundRectReusableView.id)
  section.contentInsets = Const.groupInset
  whiteRoundView.contentInsets = Const.groupInset
  section.decorationItems = [whiteRoundView]
  return section
}

 

item의 크기, group의 크기를 선언합니다. 그 후 item을 기반으로하는 group, group기반의 section을 설정하면 됩니다. 여기서 NSCollectionLayoutSize를 통해서 item size, group size를 지정할 수 있는데, estimated가 좋긴 합니다... 기존의 cell 높이 동적 계산을 구체적으로 하지 않아도 되기 때문입니다.

 

여기서 itemSize height를 동적으로 추정해도 groupSize의 height를 명확하게 지정하면 실질적으로 반영이 안될 수 있습니다. 주의해야 합니다.

 

 

func makePostLayout() -> UICollectionViewLayout {
  return UICollectionViewCompositionalLayout { [weak self] (sectionIndex, _) -> NSCollectionLayoutSection? in
    guard sectionIndex == (self?.sectionIndex ?? 0) else { return nil }
    return self?.postSection
  }.set {
    // 컴포지셔널 레이아웃의 인스턴스에 decoration view를 등록합니다.
    $0.register(
      InnerRoundRectReusableView.self,
      forDecorationViewOfKind: InnerRoundRectReusableView.id)
  }
}

 

section을 기반으로 UICollectionViewCompostiionalLayout을 생성 후 collectionViewLayout에 대입하면 됩니다.

 

컴포지셔널을 사용하면서 장점은 한 개의 컬랙션 뷰에 서로 다른 scroll 방향과 크기를 가진 section을 통해 화면을 유연하게 구현할 수 있다는 장점이었습니다.

section의 decorationItems도 많이 대박인 것 같습니다... section의 외관을 커스텀할 수 있다니..