이 포스팅은 "[번외편] 사용성 최대로 올려보자!"의 Series 중 일부입니다.
[번외편][이론] iOS 사용성 최대로 올려보자! UICollectionView Custom Layout
[번외편][실전] iOS 사용성 최대로 올려보자! UICollectionView Custom Layout
[번외편] iOS 사용성 최대로 올려보자! Shimmer
이 포스트는 iOS 사용성 최대로 올려보자! UICollectionView Custom Layout 실전편입니다.
이론편을 확인하고 오시면 더욱 이해하기 수월합니다.
Demo

Layout 구성
Heights
각 Cell 의 높이를 표시합니다.

Layouts
각 Cell 의 View 를 표시합니다.

Layout은 위와 같이 구성되어 있습니다.
- Srction 0 Header: HeaderView
- Section 0 Cell : Horizontal UICollectionView
- Section 1 Cell : Vertical UICollectionView
자 이제 들어가봅시다!
이 예제는 CustomLayout.swift 만 다루고 있습니다.
Nested Vertical Scrolling
눈치채셨겠지만 Horizontal ContentSize 마지막까지 스크롤을 하게되면 Vertical은 스크롤이 되지 않습니다.
Vertical 스크롤이 동작하게 하기 위해선
전체 UICollectionView 의 contentOffset 을 Vertical 로 전달해줘야 합니다.

class DayOfWeekVerticalContainerCell: UICollectionViewCell, NibForName { | |
override func awakeFromNib() { | |
NotificationCenter.default.addObserver(self, selector: #selector(handleSetOffset), name: Notification.Name(rawValue: NotificationNames.setOffset), object: nil) | |
} | |
@objc func handleSetOffset(notification: Notification) { | |
if let offset = notification.object as? CGFloat { | |
collectionView.contentOffset = CGPoint(x: 0, y: offset) | |
} | |
} | |
} | |
class ViewController: UIViewController { | |
func scrollViewDidScroll(_ scrollView: UIScrollView) { | |
let offsetY = scrollView.contentOffset.y | |
let headerHeightMaxChange = Metrics.HorizontalWeatherCellHeight | |
var subOffset: CGFloat = 0 | |
if offsetY > headerHeightMaxChange { | |
subOffset = offsetY - headerHeightMaxChange | |
} else { | |
subOffset = 0 | |
} | |
NotificationCenter.default.post(name: Notification.Name(rawValue: NotificationNames.setOffset), object: subOffset) | |
} | |
} | |
Sticky
Sticky 가 적용되지 않은 그림을 확인해보죠!

HeaderView 를 고정시키기 위해선 ContentOffset 에 따라서 HeaderView 의 위치를 조정해줄 필요가 있습니다.
extension CustomLayout { | |
override public func prepare() { | |
let sections = collectionView.numberOfSections | |
for section in 0..<sections { | |
let itemCount = collectionView.numberOfItems(inSection: section) | |
let layoutDelegate = collectionView.delegate as? UICollectionViewDelegateLayoutAttribute | |
if let flowDelegateLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout, | |
let headerSize = flowDelegateLayout.collectionView?(collectionView, layout: self, referenceSizeForHeaderInSection: section) { | |
let kind = UICollectionView.elementKindSectionHeader | |
let indexPath = IndexPath(item: 0, section: section) | |
let headerAttributes = layoutDelegate?.collectionView?( | |
collectionView, | |
kind: kind, | |
forSupplementary: indexPath | |
) ?? UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: kind, with: indexPath) | |
prepareHeaderFooterElement(size: headerSize, kind: UICollectionView.elementKindSectionHeader, attributes: headerAttributes) | |
} | |
private func updateAttributes(collectionView: UICollectionView, attributes: UICollectionViewLayoutAttributes, indexPath: IndexPath) { | |
switch attributes { | |
case is OverlayAlphaLayoutAttributes: | |
attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y)) | |
} | |
} | |
private func prepareHeaderFooterElement(size: CGSize, kind: String, attributes: UICollectionViewLayoutAttributes) { | |
guard size != .zero else { return } | |
switch kind { | |
case UICollectionView.elementKindSectionHeader: | |
guard let alphaAttr = attributes as? OverlayAlphaLayoutAttributes else { return } | |
alphaAttr.initialOrigin = CGPoint(x: 0, y: contentHeight) | |
attributes.frame = CGRect(origin: alphaAttr.initialOrigin, size: size) | |
attributes.zIndex = 1 | |
contentHeight = attributes.frame.maxY | |
cache[.sectionHeader]?[attributes.indexPath] = attributes | |
default: break | |
} | |
} | |
} |
initialOrigin 는 최초 View의 CGRect 상태 정보를 저장합니다.
attributes.transform = CGAffineTransform(translationX: 0, y: max(attributes.initialOrigin.y, contentOffset.y))
max(initialOrigin, contentOffset) 에서 큰 값을 transform으로 지정하면 Sticky Header 가 완성됩니다.
Header Alpha Animation
여기까지 오셨다면 Alpha animation 을 어떻게 동작시키는지 눈치채셨을 겁니다.

private func updateAttributes(collectionView: UICollectionView, attributes: UICollectionViewLayoutAttributes, indexPath: IndexPath) { | |
switch attributes { | |
case is OverlayAlphaLayoutAttributes: | |
let attributes = attributes as! OverlayAlphaLayoutAttributes | |
let headerSize = attributes.frame.size | |
let alphaVelocity: CGFloat = 1.4 | |
attributes.headerOverlayAlpha = max(0, 1 - (contentOffset.y / headerSize.height) * alphaVelocity) | |
} | |
} | |
} | |
class SummaryHeaderView: UICollectionReusableView, NibForName { | |
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { | |
super.apply(layoutAttributes) | |
guard let layoutAttributes = layoutAttributes as? OverlayAlphaLayoutAttributes else { | |
return | |
} | |
temperature.alpha = layoutAttributes.headerOverlayAlpha | |
weatherDesc.alpha = layoutAttributes.headerOverlayAlpha | |
} | |
} |
SummaryHeaderView에 SubViews alpha 속성을 지정
마무리
우리는 Custom Layout 의 강력한 기능을 학습하였습니다!
이제부터 Sticky 에 대해 겁낼 필요가 없겠죠?
전체소스는 여기에서 확인할 수 있습니다.
소중한 시간 내어서 읽어주셔서 감사합니다
'iOS' 카테고리의 다른 글
ABI 란? (1) | 2019.09.30 |
---|---|
IPA 추출 방법 (<= iOS 9) (0) | 2019.09.23 |
[번외편][이론] iOS 사용성 최대로 올려보자! UICollectionView Custom Layout (0) | 2019.09.09 |
왜 MVVM 을 사용할까요? (0) | 2019.09.03 |
RxCocoa Binder? ControlEvent? (0) | 2019.09.01 |