써니쿠키의 IOS 개발일기

[iOS] CollectionView Layout Custom, 부채꼴 만들기(2/2) 본문

카테고리 없음

[iOS] CollectionView Layout Custom, 부채꼴 만들기(2/2)

sunnyCookie 2023. 5. 5. 19:54

안녕하세요 써니쿠키입니다 :) 
1편에서 이어지는 포스팅입니다.

[iOS] CollectionView Layout Custom, 부채꼴 만들기(1/2)

이어서 고고링


 

 

4️⃣ AnchorPoint 지정하기

AnchorPoint는 CALayer의 속성으로 회전, 크기 조정 변환이 발생하는 속성입니다.

디폴트값은 1편 마지막 사진처럼 중앙입니다

그래서 이제 이 AnchorPoint를 중앙에서 바깥으로 이동시켜줘야합니다. (x축은 그래도, y축은 밑으로(+))

실제 고정점에서 x값은 0.5입니다.

그리고 y는 부채꼴의 중심이 되야하므로 아래로 내려서 radius + (itemSize.height / 2) 가 돼야합니다.

 

그래서 prepare() 내부에 아래처럼 anchorPointY 를 선언합니다.

let anchorPointY = ((itemSize.height / 2.0) + radius) / itemSize.height

그리고 map으로 item 속성을 주던부분에 아래 코드를 추가합니다

attributes.anchorPoint =  CGPoint (x: 0.5 , y: anchorPointY)

 

그럼 이제 Cell에 적용한 attribues에도 적용해줘야해요!

Cell을 커스텀한 NumberCell.swift 파일에서

apply(_ layoutAttributes: ) 메서드를 override 해줍니다.

 

여기서 center, transform 같은 기본 속성은 적용되도록 super.apply 를 한 번 불러줍니다.

그리고 anchorPoint는 저희가 y값을 변경해준 앵커포인트로 변경해줍니다.

또한 anchorPoint.y의 변경 사항을 보정하기 위해 레이아웃 원의 중심으로 center.y를 업데이트합니다.

override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)

        let circularLayoutAttributes = layoutAttributes as! CircularLayoutAttributes
        self.layer.anchorPoint = circularLayoutAttributes.anchorPoint
        self.center.y += (circularLayoutAttributes.anchorPoint.y - 0.5) * CGRectGetHeight(self.bounds)
    }

여기까지 하면 이렇게 부채꼴로 펼쳐진 모양으로 Layout이 바뀌었을거에요

하지만 스크롤은 원하는 방향으로 되지않습니다..ㅠㅠ!

이제 스크롤을 만져보죠..!

 

 

 


5️⃣ Scroll 시 원형으로 돌아가게 만들기

스크롤을 구현하기 위해선 각도값을 계산하고 만져야해요..!

CircularLayout 클래스에 아래 코드를 추가합니다.

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

이렇게 true를 반환하도록하면 collectionView가 스크롤할 때 레이아웃을 무효화하도록하고,

업데이트된 각도 위치로 셀의 레이아웃을 다시 계산할 수 있는 prepare()이 호출됩니다.

 

각도를 계산해봅시다..

angle은 첫번째(0번) cell의 각도 위치로 정의됩니다.

이제 contentOffset.xangle 값을 이용해 적절히 변환해 스크롤링을 구현할겁니다.

 

contentOffset.x는 스크롤할 때 0에서

collectionViewContentSize.width - CGRectGetWidth(collectionView!.bounds)로 이동합니다.

contentOffset.x의 극단값을 maxContentOffset으로 호출합니다.

 

0에서 중앙에 0번째 셀이 있어야하고 마지막엔 마지막 셀이 화면 중앙에 있어야합니다.

즉, 마지막 항목의 각도 위치도 0이 되야합니다

오른쪽의 시나리오를 고려해서 만든 다음 방정식들을 풀어 봅시다

 

angle_for_last_item = angle_for_zero_item + (totalItems - 1) * anglePerItem
0 = angle_for_zero_item + (totalItems - 1) * anglePerItem
angle_for_zero_item = -(totalItems - 1) * anglePerItem

(totalItems - 1) * anglePerItem

angleAtExtreme으로 정의하면 다음과 같이 작성할 수 있습니다.

contentOffset.x = 0
angle = 0

contentOffset.x = maxContentOffset
angle = angleAtExtreme

 

여기에서 다음 공식을 사용하여 contentOffset.x의 모든 값에 대한 각도를 수정하는 것은 매우 쉽습니다.

angle = -angleAtExtreme * contentOffset.x / maxContentOffset

이 모든 수학과정을 이해한다면!

이걸 염두에 두고 itemSize 선언 아래에 다음 속성을 추가합니다.

var angleAtExtreme: CGFloat {
      return collectionView!.numberOfItems(inSection: 0) > 0 ?
        -CGFloat(collectionView!.numberOfItems(inSection: 0) - 1) * anglePerItem : 0
    }

    var angle: CGFloat {
      return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width -
        CGRectGetWidth(collectionView!.bounds))
    }

 

그리고, prepare() 메서드 내부를 수정해줍니다

// 이부분을 수정해줍니다
// attributes.angle = (self.anglePerItem * CGFloat(i))

attributes.angle = self.angle + (self.anglePerItem * CGFloat(i))

이렇게 하면 각도 값이 각 항목에 추가되어 상수가 아니라 각도 위치가 contentOffset.x의 함수가 됩니다.

이제 빌드를 해보면 아래처럼 스크롤링됨을 확인 할 수 있습니다 :)

 

 

 

 


6️⃣ Bonus단계: 최적화하기

구현은 끝나서 끝내도 되지만 최적화할 수 있는 방법이 추가로 있습니다!

위에서는 prepare()에서 모든 항목에 대해 CircularLayoutAttributes의 인스턴스를 생성해높지만

사실 모든 cell들이 화면이 보이지 않습니다.

이러한 스크린 밖 셀들의 경우 계산을 건너뛰고 레이아웃 속성을 전혀 생성하지 않을 수 있습니다.

 

이를 하려면 어떤 cell이 화면 안에 있고 어떤 cell이 화면 밖에 있는지 알아야합니다!

 

그림으로 먼저 이해해 보자면

아래 그림에서 (-θ, θ) 범위 밖에 있는 셀들은 모두 화면에서 벗어난 상태인 셈입니다

 

예를 들어 삼각형 ABC에서 θ를 계산하려면 다음과 같이 합니다.

tanθ = (collectionView.width / 2) / (radius + (itemSize.height / 2) - (collectionView.height / 2))

prepare() 메서드에 anchorPointY 선언 바로 아래에 다음 코드를 추가합니다.

//1
let theta = atan2(CGRectGetWidth(collectionView!.bounds) / 2.0,
            radius + (itemSize.height / 2.0) - (CGRectGetHeight(collectionView!.bounds) / 2.0))

// 2
var startIndex = 0
var endIndex = collectionView!.numberOfItems(inSection: 0) - 1

// 3
if (angle < -theta) {
  startIndex = Int(floor((-theta - angle) / anglePerItem))
}

// 4
endIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))

// 5
if (endIndex < startIndex) {
  endIndex = 0
  startIndex = 0
}

 

1. tan 메서드를 사용해 theata 를 찾습니다.

 

2. startIndex와 endIndex를 각각 0과 마지막 항목 인덱스로 초기화합니다.

 

3. 0번째 셀의 각도 위치가 -theta보다 작으면 화면 밖에 있습니다.
이 경우 화면의 첫 번째 항목은 -θ와 angle을 anglePerItem으로 나눈 값의 차이입니다.

 

4. 화면의 마지막 셀은 θ와 angle을 anglePerItem으로 나눈 값의 차이이며 min은 endIndex가 총 항목 수를 초과하지 않도록 추가 검사 역할을 합니다.

 

5. 마지막으로 endIndexstartIndex보다 작은 경우 범위를 0...0으로 만드는 안전 검사를 추가합니다.
이 엣지 케이스는 매우 빠른 속도로 스크롤하고 모든 셀이 완전히 화면 밖으로 나갈 때 발생합니다.

 

다음은 위의 계산을 시각적으로 설명하는 그림입니다.

 

이제 화면에 표시되는 셀과 아닌 셀을 파악했으니까

prepare()에서 레이아웃 속성을 계산하는 데 사용되는 범위를 업데이트해야 합니다.

 

이전에 map메서드로 모든 셀에 attributes를 부여했잖아여?

그 map의 범위인 Array를 지정해주면됩니다!

// 이부분을 수정합니다
// attributesList = (0..<collectionView!.numberOfItems(inSection: 0).map { ... } 

attributesList = (startIndex...endIndex).map { ... }

이제 빌드하고 실행하면 화면은 똑같은데

Xcode ViewHierarchy를 보면 적은 수의 셀이 준비중임을 알 수 있습니다.

왼쪽 그림이 최적화 전, 오른쪽이 최적화 후입니다!


 

이렇게 이틀에걸쳐..

collectionView의 Layout을 부채꼴 모양으로 수정해보는 포스팅을 해봤습니당 :)

 

ㄲ ㅡ ㅌ✋🏻

반응형
Comments