UICollectionViewTransitionLayout 이란?

UICollectionViewTransitionLayout 이란?

LINE iOS App을 살펴보다가 이미지 피커에서 PinchGesture를 통해 Grid의 column 개수를 조절하는 방식이 인상적어서 어떻게 구현할 수 있는지 찾아보게 되었어요.

1. 왜 필요한가?

보통 레이아웃을 변경하려면 setCollectionViewLayout을 이용합니다.

collectionView.setCollectionViewLayout(newLayout, animated: true)

하지만 이 방식은 “처음”과 “끝” 두 지점만 정의할 수 있고, 전환 과정은 시스템이 자동으로 처리합니다. 따라서 사용자의 제스처에 따라 실시간으로 레이아웃 전환 상태를 제어하는 것은 불가능합니다.

예를 들어, 사진 갤러리에서 핀치 제스처를 통해 column 수를 점차 늘리거나 줄이는 UI를 구현하려면, 사용자의 손가락 움직임에 따라 매 프레임마다 레이아웃 상태를 미세하게 조정할 수 있어야 합니다.

이런 상황에서 UICollectionViewTransitionLayout이 유용하게 사용될 수 있습니다.

2. TransitionLayout이란?

UICollectionViewTransitionLayout은 기존 레이아웃과 새로운 레이아웃 사이의 중간(interpolated) 상태를 만들어주는 레이아웃입니다.

즉, 기존 layoutA에서 새로운 layoutB로 전환되는 과정을 직접 progress 값으로 제어할 수 있도록 도와줍니다.

3. 사용법

// 1. 새로운 레이아웃 준비
let newLayout = ZoomedLayout()

// 2. 전환 시작
let transitionLayout = collectionView.startInteractiveTransition(to: newLayout) {
    // 전환 완료 시 호출됨
}

// 3. 제스처를 통해 전환 진행도 업데이트
transitionLayout.transitionProgress = 0.3

// 4. 전환 완료 또는 취소
collectionView.finishInteractiveTransition()
// 또는
collectionView.cancelInteractiveTransition()

4. Pinch Gesture와 사용하기

📎 전체 코드: GitHub

🧠 동작 개요

  1. 사용자가 핀치하면 scale 값이 들어옵니다 (1보다 크면 확대, 작으면 축소).
  2. log2(scale)을 통해 확대/축소 정도를 정규화하고 방향을 판단합니다.
  3. 이 값을 기반으로 새 레이아웃을 선택하고 전환을 시작합니다.
  4. 제스처가 진행될수록 transitionProgress를 업데이트합니다.
  5. 제스처가 끝났을 때 진행도가 50% 이상이면 전환 완료, 아니면 취소합니다.
@objc private func handlePinch(_ gesture: UIPinchGestureRecognizer) {
    switch gesture.state {
    case .changed:
        handlePinchChanged(with: gesture.scale)

    case .ended, .cancelled:
        completeOrCancelTransition()

    default:
        break
    }
}

private func handlePinchChanged(with scale: CGFloat) {
    let logScale = log2(scale)

    if interactiveLayout == nil {
        guard let targetIndex = calculateTargetLayoutIndex(from: logScale) else { return }
        startInteractiveTransition(to: targetIndex)
    }

    interactiveLayout?.transitionProgress = calculateProgress(from: logScale)
}

private func startInteractiveTransition(to targetIndex: Int) {
    let targetLayout = layouts[targetIndex]

    interactiveLayout = collectionView.startInteractiveTransition(to: targetLayout) { [weak self] _, finished in
        guard let self = self else { return }
        if finished {
            self.currentLayoutIndex = targetIndex
        }
        self.interactiveLayout = nil
    }
}

private func completeOrCancelTransition() {
    guard let layout = interactiveLayout else { return }

    if layout.transitionProgress > 0.5 {
        collectionView.finishInteractiveTransition()
    } else {
        collectionView.cancelInteractiveTransition()
    }

    interactiveLayout = nil
}

5. LINE vs 샘플 앱 비교

라인 iOS Application line

TransitionLayout 구현 Sample App sample