티스토리 뷰
이전 글에서 간단한(?) carousel effect가 들어간 컬렉션뷰를 만들어봤었는데요. 이제 우리가 최종적으로 만들려고 하는 위와 같은 컬렉션뷰를 만들어 보겠습니다.
기존 코드에서 우리가 만들었던 setupLayout 메서드 일부와 transformLayoutAttributes 메서드만 수정해주면 됩니다 ㅎㅎ
접근 방식
1. 우선 스케일의 변화가 기존 2단에서 3단으로 변하기 때문에, 거리에 따라 달라지는 ratio 공식을 수정해야합니다.
2. 아이템간의 스페이싱이 음수이기 때문에, (즉 여러 아이템들이 겹쳐지는 컬렉션뷰이기 때문에) 앞에 있는 아이템이 뒤에 있는 아이템에 가려지지 않도록 하는 적절한 조치가 필요합니다.
3. paging이 가능하도록 만들자!
● setupLayout()
변경 전
let scaledItemOffset = (itemWidth - itemWidth*self.sideItemScale) / 2
self.minimumLineSpacing = spacing - scaledItemOffset
변경 후
let scaledItemOffset = (itemWidth - (itemWidth*(self.sideItemScale + (1 - self.sideItemScale)/2))) / 2
self.minimumLineSpacing = spacing - scaledItemOffset
scaledItemOffset이 조금 달라졌죠??, 이렇게 달라진 이유는, 스케일이 3단으로 변하기 때문에 그 영향을 고려해 주기 위함입니다.
만약 변경 전의 코드를 이해 하셨다면, 변경 후의 코드도 이해 하실 수 있을겁니다 :)
● transformLayoutAttributes(attributes: )
private func transformLayoutAttributes(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else {return attributes}
let collectionCenter = collectionView.frame.size.width / 2
let contentOffset = collectionView.contentOffset.x
let center = attributes.center.x - contentOffset
let maxDistance = 2*(self.itemSize.width + self.minimumLineSpacing)
let distance = min(abs(collectionCenter - center), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = alpha
if abs(collectionCenter - center) > maxDistance + 1 {
attributes.alpha = 0
}
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let dist = attributes.frame.midX - visibleRect.midX
var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
transform = CATransform3DTranslate(transform, 0, 0, -abs(dist/1000))
attributes.transform3D = transform
return attributes
}
transformLayoutAttributes(attributes: ) 메서드의 경우 어느 부분이 바뀌었는지 아시겠나요? 많이 바뀌는 부분은 없습니다. 스케일링 되는 아이템이 두개이므로 기존 maxDistance에 2를 곱하면 끝이에요! 이렇게 되면 스케일링 되는 범주가 두배 만큼 늘어나는 거죠,
그리고 추가가 된 코드가 있는데요. 바로 이 코드 입니다.
if abs(collectionCenter - center) > maxDistance + 1 {
attributes.alpha = 0
}
만약 컬렉션뷰 중앙을 기준으로 각 아이템들의 오프셋된 거리가 maxDistance + 1 보다 클 경우 투명도 값을 0으로 처리하겠다는 의미입니다. 왜 maxDistance가 아니라 maxDistance + 1 이지? 라고 의아해 할 수 있을 텐데요. + 1을 해준 이유는 약간의 오차를 보정하기 위함입니다.
이렇게 해주고 실행을 해보면 다음과 같이 우리가 만들려던...! 뷰를 볼 수 있습니다 ㅎㅎ
하지만 이렇게하면 아직 페이징은 안된다는거.. 페이징이 가능하면 더 좋겠죠? (만약 여기서 단순히 collectionView.isPagingEnabled = true 로 하면 우리가 원하던 대로 페이징이 되지 않을꺼에요!)
하단의 코드를 우리가 만든 CarouselLayout에 추가해줘야 합니다.
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
우리는 targetContentOffset 메서드를 오버라이드 해서 사용 할 겁니다. 왜냐?? 애플 개발자 문서에 들어가서 이 메서드에 대한 설명을 보면 이 메서드를 재정의해서 사용하면 스크롤이 중지되는 지점을 변경 할 수 있다고 하거든요!
이 targetContentOffset 메서드는 스크롤을 중지할 지점을 찾아서 반환하는 메소드 입니다.
입력 파라미터로는 1. proposedContentOffset 2. velocity 두개가 있습니다.
1. proposedContentOffset - 스크롤이 자연스럽게 중지 되는 값, point는 visable content의 좌측 위 모서리를 가리킵니다.
2. velocity - 스크롤 속도 입니다. (points / sec)
이제 이 메서드가 뭔지 알았으니 코드를 작성합시다.
우리는 proposedContentOffset 를 통해 스크롤뷰가 어느 지점에서 중지되는지를 알 수 있죠? 위에 point는 visable content의 좌측 위 모서리를 가리킨다고 했는데, 이 visable content는 collectionView bounds의 좌측 모서리라 생각하며 될 것 같습니다.
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
이제 이 proposedContentOffset.x에 collectionView.frame.width/2를 더하면, 스크롤 뷰가 멈출 중앙 지점을 얻을 수 있습니다.
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
(참고. 기존에 선언했던 var offsetAdjustment = CGFloat.greatestFiniteMagnitude는 CGFloat 값중 가장 큰 값을 의미합니다.)
itemHorizontalCenter - 각 아이템(셀)들의 x축 중앙 값
이제 offsetAdjustment라는 것을 우리가 구해줄겁니다. 이건 어디에 사용하려고 하느냐? 스크롤뷰가 멈출 것으로 예상되는 포인트에서 가장 가까운 아이템(셀)의 중앙 값을 얻어 온 후, 그 값이 스크롤뷰가 멈출 것으로 예상되는 포인트와 얼마나 차이나는지 계산해서 멈출 지점을 보정해주는데 사용하기 위함입니다. 반복문을 통해, 가장 작은 offsetAdjustment를 갖게 되고 이 값을 스크롤이 멈출 것으로 예상되는 지점인 proposedContentOffset.x에 더해주면 됩니다.
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
이렇게 하면 이제 셀이 정 가운데로 올 수 있도록 페이징이 가능 합니다.
● 전체 코드
레이아웃
import UIKit
class CarouselLayout: UICollectionViewFlowLayout {
public var sideItemScale: CGFloat = 0.5
public var sideItemAlpha: CGFloat = 0.5
public var spacing: CGFloat = 10
public var isPagingEnabled: Bool = false
private var isSetup: Bool = false
override public func prepare() {
super.prepare()
if isSetup == false {
setupLayout()
isSetup = true
}
}
private func setupLayout() {
guard let collectionView = self.collectionView else {return}
let collectionViewSize = collectionView.bounds.size
let xInset = (collectionViewSize.width - self.itemSize.width) / 2
let yInset = (collectionViewSize.height - self.itemSize.height) / 2
self.sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset)
let itemWidth = self.itemSize.width
let scaledItemOffset = (itemWidth - (itemWidth*(self.sideItemScale + (1 - self.sideItemScale)/2))) / 2
self.minimumLineSpacing = spacing - scaledItemOffset
self.scrollDirection = .horizontal
}
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect),
let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
else { return nil }
return attributes.map({ self.transformLayoutAttributes(attributes: $0) })
}
private func transformLayoutAttributes(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else {return attributes}
let collectionCenter = collectionView.frame.size.width / 2
let contentOffset = collectionView.contentOffset.x
let center = attributes.center.x - contentOffset
let maxDistance = 2*(self.itemSize.width + self.minimumLineSpacing)
let distance = min(abs(collectionCenter - center), maxDistance)
let ratio = (maxDistance - distance)/maxDistance
let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
attributes.alpha = alpha
if abs(collectionCenter - center) > maxDistance + 1 {
attributes.alpha = 0
}
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let dist = attributes.frame.midX - visibleRect.midX
var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
transform = CATransform3DTranslate(transform, 0, 0, -abs(dist/1000))
attributes.transform3D = transform
return attributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
뷰컨트롤러
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView! // 컬렉션뷰 사이즈: width = 315 height = 347
override func viewDidLoad() {
super.viewDidLoad()
addCollectionView()
collectionView.backgroundColor = .clear
self.view.backgroundColor = .brown
}
func addCollectionView(){
let layout = CarouselLayout()
layout.itemSize = CGSize(width: collectionView.frame.size.width*0.796, height: collectionView.frame.size.height)
layout.sideItemScale = 175/251
layout.spacing = -197
layout.isPagingEnabled = true
layout.sideItemAlpha = 0.5
collectionView.collectionViewLayout = layout
self.collectionView?.delegate = self
self.collectionView?.dataSource = self
self.collectionView?.register(CarouselCell.self, forCellWithReuseIdentifier: "carouselCell")
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "carouselCell", for: indexPath) as! CarouselCell
cell.customView.backgroundColor = .white
return cell
}
}
컬렉션뷰 셀
import UIKit
class CarouselCell: UICollectionViewCell {
let customView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 12
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.customView)
self.customView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
self.customView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.customView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1).isActive = true
self.customView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 1).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
긴 글 읽어주셔서 감사합니다 :)
혹시 글을 읽고서 이해가 잘 안되는 부분이 있거나 잘못된 정보가 있다면 꼭 댓글 달아주세요 !!
'Swift&iOS > iOS' 카테고리의 다른 글
[iOS] UITableView, UICollectionView Cell 버튼 상태 유지 방법 (0) | 2020.10.28 |
---|---|
[iOS] Cell을 재사용시 생기는 문제점들과 해결 방법 (1) | 2020.10.26 |
[iOS] CollectionView 3D 전환, carousel effect 주기 (1) (0) | 2020.08.01 |
[iOS] UICollectionView 기초 정리(2) (0) | 2020.07.25 |
[iOS] UICollectionView 기초 정리(1) (0) | 2020.07.25 |