UICollectionViewFlowLayout을 서브클래싱하여
중앙 정렬된 셀 레이아웃을 제공하는 YZCenterFlowLayout이라는 클래스를 살펴봅니다.
(아직 업데이트 중!)
출처:
https://github.com/yudiz-solutions/YZCenterFlowLayout
셀 사이의 간격, 스크롤 목표지점 등 수정을 해야합니다..
[수정내용]
→ func scrollToPage 현재 필요 없으므로 삭제
→ 셀 사이의 간격은 CollectionViewCell에서 아래 spacingMode 숫자를 수정하여 재설정
layout.spacingMode = .fixed(spacing: -5)
1. 열거형 정의
두 개의 열거형 정의:
아이템 간의 간격 설정 + 애니메이션 모드 설정
// 아이템 간의 간격을 설정하는 열거형
enum YZCenterFlowLayoutSpacingMode {
case fixed(spacing: CGFloat) // 고정 간격
case overlap(visibleOffset: CGFloat) // 겹치는 간격 (아이템이 겹치도록)
}
// 애니메이션 모드를 설정하는 열거형
enum YZCenterFlowLayoutAnimation {
case rotation(sideItemAngle: CGFloat, sideItemAlpha: CGFloat, sideItemShift: CGFloat) // 회전 애니메이션
case scale(sideItemScale: CGFloat, sideItemAlpha: CGFloat, sideItemShift: CGFloat) // 스케일 애니메이션
}
2. YZCenterFlowLayout 클래스 정의
UICollectionViewFlowLayout을 상속받아 YZCenterFlowLayout 클래스를 정의
※ 여기서, .fixed(spacing) 값을 수정하거나, fixed(spacing) → .overlap(visibleOffset: int) int 값 수정 시 셀 간격 조정
class YZCenterFlowLayout: UICollectionViewFlowLayout {
// 레이아웃 상태 구조체
fileprivate struct LayoutState {
var size: CGSize
var direction: UICollectionView.ScrollDirection
// 현재 상태와 다른 상태를 비교하는 함수
func isEqual(_ otherState: LayoutState) -> Bool {
return self.size.equalTo(otherState.size) && self.direction == otherState.direction
}
}
// 레이아웃 상태와 기타 속성 정의
fileprivate var state = LayoutState(size: CGSize.zero, direction: .horizontal)
var spacingMode = YZCenterFlowLayoutSpacingMode.fixed(spacing: 0)
var animationMode = YZCenterFlowLayoutAnimation.scale(sideItemScale: 0.7, sideItemAlpha: 0.6, sideItemShift: 0.0)
// 페이지 폭을 계산하는 계산 프로퍼티
fileprivate var pageWidth: CGFloat {
switch self.scrollDirection {
case .horizontal:
return self.itemSize.width + self.minimumLineSpacing
case .vertical:
return self.itemSize.height + self.minimumLineSpacing
default:
return 0.0
}
}
/// 현재 중앙에 정렬된 페이지의 인덱스를 계산하는 프로퍼티
var currentCenteredIndexPath: IndexPath? {
guard let collectionView = self.collectionView else { return nil }
let currentCenteredPoint = CGPoint(x: collectionView.contentOffset.x + collectionView.bounds.width/2, y: collectionView.contentOffset.y + collectionView.bounds.height/2)
return collectionView.indexPathForItem(at: currentCenteredPoint)
}
// 현재 중앙에 있는 페이지의 인덱스를 반환하는 프로퍼티
var currentCenteredPage: Int? {
return currentCenteredIndexPath?.row
}
}
3. prepare() 메서드: 컬렉션 뷰의 상태를 설정
override func prepare() {
super.prepare()
guard let collectionView = self.collectionView else { return }
let currentState = LayoutState(size: collectionView.bounds.size, direction: self.scrollDirection)
if !self.state.isEqual(currentState) {
self.setupCollectionView()
self.updateLayout()
self.state = currentState
}
}
4. 레이아웃 변경에 대한 무효화 설정: 경계가 변경될 때 레이아웃을 무효활 할 것인가? 여부 결정
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
5. 특정 영역의 레이아웃 속성 반환: 지정된 사각형 내의 모든 레이아웃 속성을 리턴
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($0) })
}
6. 스크롤 목표 지점 설정: 스크롤이 멈출 목표 지점을 계산 (← 스무스하게 수평 스크롤 되도록 수정할 때 적용)
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView , !collectionView.isPagingEnabled,
let layoutAttributes = self.layoutAttributesForElements(in: collectionView.bounds)
else { return super.targetContentOffset(forProposedContentOffset: proposedContentOffset) }
let isHorizontal = (self.scrollDirection == .horizontal)
let midSide = (isHorizontal ? collectionView.bounds.size.width : collectionView.bounds.size.height) / 2
let proposedCenterOffset = (isHorizontal ? proposedContentOffset.x : proposedContentOffset.y) + midSide
var targetContentOffset: CGPoint
if isHorizontal {
let closest = layoutAttributes.sorted { abs($0.center.x - proposedCenterOffset) < abs($1.center.x - proposedCenterOffset) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: floor(closest.center.x - midSide), y: proposedContentOffset.y)
} else {
let closest = layoutAttributes.sorted { abs($0.center.y - proposedCenterOffset) < abs($1.center.y - proposedCenterOffset) }.first ?? UICollectionViewLayoutAttributes()
targetContentOffset = CGPoint(x: proposedContentOffset.x, y: floor(closest.center.y - midSide))
}
return targetContentOffset
}
7. 페이지로 스크롤하는 메서드: 특정 인덱스의 페이지로 스크롤 (프로그래밍 방식) (← 필요 없으므로 삭제 )
func scrollToPage(atIndex index: Int, animated: Bool = true) {
guard let collectionView = self.collectionView else { return }
let proposedContentOffset: CGPoint
let shouldAnimate: Bool
switch scrollDirection {
case .horizontal:
let pageOffset = CGFloat(index) * self.pageWidth - collectionView.contentInset.left
proposedContentOffset = CGPoint(x: pageOffset, y: collectionView.contentOffset.y)
shouldAnimate = abs(collectionView.contentOffset.x - pageOffset) > 1 ? animated : false
collectionView.setContentOffset(proposedContentOffset, animated: shouldAnimate)
case .vertical:
let pageOffset = CGFloat(index) * self.pageWidth - collectionView.contentInset.top
proposedContentOffset = CGPoint(x: collectionView.contentOffset.x, y: pageOffset)
shouldAnimate = abs(collectionView.contentOffset.y - pageOffset) > 1 ? animated : false
collectionView.setContentOffset(proposedContentOffset, animated: shouldAnimate)
default:
print("Default Case...")
}
}
8. 프라이빗 메서드: 컬렉션 뷰를 설정하고 레이아웃을 업데이트
※ 여기 updateLayout() 메서드 내에서 minimumLineSpacing 값을 직접 조정해서.. minimumLineSpacing 값을 설정하는 로직을 수정하여 셀 사이의 간격을 조정해 보기 시도 → 실패
// 추가한 코드
let desiredSpacing: CGFloat = 10 // 원하는 간격 값으로 설정!
private extension YZCenterFlowLayout {
// 컬렉션 뷰를 설정하는 메서드
func setupCollectionView() {
guard let collectionView = self.collectionView else { return }
if collectionView.decelerationRate != UIScrollView.DecelerationRate.fast {
collectionView.decelerationRate = UIScrollView.DecelerationRate.fast
}
}
// 레이아웃을 업데이트하는 메서드
func updateLayout() {
guard let collectionView = self.collectionView else { return }
let collectionSize = collectionView.bounds.size
let isHorizontal = (self.scrollDirection == .horizontal)
let yInset = (collectionSize.height - self.itemSize.height) / 2
let xInset = (collectionSize.width - self.itemSize.width) / 2
self.sectionInset = UIEdgeInsets.init(top: yInset, left: xInset, bottom: yInset, right: xInset)
let side = isHorizontal ? self.itemSize.width : self.itemSize.height
var scale: CGFloat = 1.0
switch animationMode {
case .scale(let sideItemScale, _, _):
scale = sideItemScale
default:
break
}
let scaledItemOffset = (side - side * scale) / 2
switch self.spacingMode {
case .fixed(let spacing):
self.minimumLineSpacing = spacing - scaledItemOffset
case .overlap(let visibleOffset):
let fullSizeSideItemOverlap = visibleOffset + scaledItemOffset
let inset = isHorizontal ? xInset : yInset
self.minimumLineSpacing = inset - fullSizeSideItemOverlap
}
}
// 레이아웃 속성을 변형하는 메서드
func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
guard let collectionView = self.collectionView else { return attributes }
let isHorizontal = (self.scrollDirection == .horizontal)
let collectionCenter: CGFloat = isHorizontal ? collectionView.frame.size.width/2 : collectionView.frame.size.height/2
let offset = isHorizontal ? collectionView.contentOffset.x : collectionView.contentOffset.y
let normalizedCenter = (isHorizontal ? attributes.center.x : attributes.center.y
'iOS 앱 개발자 프로젝트' 카테고리의 다른 글
[iOS] picker 버튼에 이미지 추가하기 (UIButton) (0) | 2024.06.05 |
---|---|
[iOS] 옵션 선택을 위한 picker 버튼 만들기 (0) | 2024.06.05 |
[iOS] UICollectionView에서 제네릭으로 코드 수정하기 (0) | 2024.06.03 |
[iOS] UICollectionView 내의 Carousel UI 수정하기 (2) (0) | 2024.06.03 |
[iOS] UICollectionView 내의 Carousel UI 수정하기 (1) (0) | 2024.06.03 |