써니쿠키의 IOS 개발일기
[iOS] CollectionView Layout Custom, 부채꼴 만들기(2/2) 본문
안녕하세요 써니쿠키입니다 :)
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.x
를 angle
값을 이용해 적절히 변환해 스크롤링을 구현할겁니다.
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. 마지막으로 endIndex
가 startIndex
보다 작은 경우 범위를 0...0으로 만드는 안전 검사를 추가합니다.
이 엣지 케이스는 매우 빠른 속도로 스크롤하고 모든 셀이 완전히 화면 밖으로 나갈 때 발생합니다.
다음은 위의 계산을 시각적으로 설명하는 그림입니다.
이제 화면에 표시되는 셀과 아닌 셀을 파악했으니까
prepare()
에서 레이아웃 속성을 계산하는 데 사용되는 범위를 업데이트해야 합니다.
이전에 map메서드로 모든 셀에 attributes를 부여했잖아여?
그 map의 범위인 Array를 지정해주면됩니다!
// 이부분을 수정합니다
// attributesList = (0..<collectionView!.numberOfItems(inSection: 0).map { ... }
attributesList = (startIndex...endIndex).map { ... }
이제 빌드하고 실행하면 화면은 똑같은데
Xcode ViewHierarchy를 보면 적은 수의 셀이 준비중임을 알 수 있습니다.
왼쪽 그림이 최적화 전, 오른쪽이 최적화 후입니다!
이렇게 이틀에걸쳐..
collectionView의 Layout을 부채꼴 모양으로 수정해보는 포스팅을 해봤습니당 :)
ㄲ ㅡ ㅌ✋🏻