Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Human interface guide
- Xcode
- Refactoring
- ribs
- combine
- swiftUI
- RxCocoa
- Clean Code
- UITextView
- map
- Observable
- MVVM
- HIG
- rxswift
- 스위프트
- collectionview
- 애니메이션
- uitableview
- tableView
- UICollectionView
- 리펙터링
- uiscrollview
- SWIFT
- 리펙토링
- clean architecture
- 클린 코드
- ios
- 리팩토링
- swift documentation
- Protocol
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 3. CollectionView (컬렉션 뷰) - custom layout (grid, pinterest 레이아웃 구현) 본문
iOS 응용 (swift)
[iOS - swift] 3. CollectionView (컬렉션 뷰) - custom layout (grid, pinterest 레이아웃 구현)
jake-kim 2021. 7. 20. 00:42
1. CollectionView (컬렉션 뷰) - UICollectionViewFlowLayout
2. CollectionView (컬렉션 뷰) - UICollectionViewFlowLayout을 이용한 CarouselView (수평 스크롤 뷰)
3. CollectionView (컬렉션 뷰) - custom layout (grid, pinterest 레이아웃 구현)
4. CollectionView (컬렉션 뷰) -실전 사용 방법 (FlowLayout, CustomLayout, binary search, cache)
CollectionView와 UICollectionViewLayout 개념 포인트
- prepare: 레이아웃 작업이 발생하려고 할 때 UIKit이 해당 메서드 호출
- collectionViewContentSize: contents의 너비와 높이를 반환
- layoutAttributesForElement(in:): 인수로 들어온 rect내부의 모든 항목에 대한 레이아웃 속성(UIColelctionViewLayoutAttributes)을 배열로 반환
- layoutAttributesForItem(at:): indexPath가 인수로 들어오면 레이아웃 정보를 제공
커스텀 UICollectionViewLayout 정의
- 높이를 요청하는 delegate protocol 정의
// MyLayout.swift
protocol MyLayoutDelegate: AnyObject {
func collectionView(_ collectionView: UICollectionView, heightForImageAtIndexPath indexPath: IndexPath) -> CGFloat
}
- MyLayout 클래스 정의
class MyLayout: UICollectionViewLayout {
weak var delegate: MyLayoutDelegate?
}
- 레이아웃을 구현하기 위해 필요한 property 작성
- cache: 컬렉션 뷰가 레이아웃 속성을 요청할 때 매번 다시 계산하지 않고 해당 cache를 이용하여 효율적으로 제공
- contentHeight, contentWidth: contentHeight은 cell이 추가될때마다 증가, contentWidth는 증가 > 감소 > 증가 > ...
- collectionViewContentSize: 크기를 계산하기 위해 이전 contentWidth와 contentHeight를 모두 사용
fileprivate var numberOfColumns: Int = 2
fileprivate var cellPadding: CGFloat = 6.0
fileprivate var cache: [UICollectionViewLayoutAttributes] = []
fileprivate var contentHeight: CGFloat = 0.0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0.0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
layout 계산
- 바로 다음에 위치할 cell의 위치를 구하기 위해서 xOffset, yOffset 계산
- prepare(): 컬렉션에서 항목이 추가되거나 제거 또는 방향이 바뀌는 경우에 호출
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
cache.removeAll()
// xOffset 계산
let columnWidth: CGFloat = contentWidth / CGFloat(numberOfColumns)
var xOffset: [CGFloat] = []
for column in 0..<numberOfColumns {
let offset = CGFloat(column) * columnWidth
xOffset += [offset]
}
// yOffset 계산
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let imageHeight = delegate?.collectionView(collectionView, heightForImageAtIndexPath: indexPath) ?? 0
let height = cellPadding * 2 + imageHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// cache 저장
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 새로 계산된 항목의 프레임을 설명하도록 확장
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
// 다음 항목이 다음 열에 배치되도록 설정
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
- rect에 따른 layoutAttributes를 얻는 메서드 재정의
- layoutAttributesForElement(in:): 인수로 들어온 rect내부 항목에 대한 레이아웃 속성 반환
- cache에 저장된게 있으면 해당 값 반환
- 참고) IndexPath로 인수가 들어오면 random access로 O(1)로 효율적으로 탐색할수 있지만, rect로 들어온 경우는 O(n)이 되면 비효율적이므로 O(logn)으로 접근 - binSearch를 이용하여 layoutAttributesForElements(in:) 효율적 사용 방법 참고
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache.filter { rect.intersects( $0.frame) }
}
- indexPath에 따른 layoutAttribute를 얻는 메서드 재정의
- layoutAttributesForItem(at:): 특정 indexPath에 대한 레이아웃 속성 반환
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
사용하는 쪽
- Model 정의
struct MyModel {
let color: UIColor
let commentString: String
let contentHeightSize: CGFloat
static func getMock() -> [Self] {
var datas: [MyModel] = []
let number = arc4random_uniform(30) // 0 ~ 29
for i in 0...number {
let red = CGFloat(arc4random_uniform(256))
let green = CGFloat(arc4random_uniform(256))
let blue = CGFloat(arc4random_uniform(256))
let alpha = CGFloat(drand48()) // 0 ~ 1
let color = UIColor.init(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha)
let tmpHeight = CGFloat(arc4random_uniform(500))
let imageHeightSize = tmpHeight < 50 ? 50 : tmpHeight
let myModel: MyModel = .init(color: color,
commentString: "\(i + 1) cell",
contentHeightSize: imageHeightSize)
datas += [myModel]
}
return datas
}
}
- CollectionViewCell 정의
class MyCell: UICollectionViewCell {
static var id: String {
return NSStringFromClass(Self.self).components(separatedBy: ".").last ?? ""
}
var myModel: MyModel? {
didSet { bind() }
}
lazy var containerView: UIView = {
let view = UIView()
return view
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
private func setupView() {
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
titleLabel.text = myModel?.commentString
containerView.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
titleLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
}
private func bind() {
containerView.backgroundColor = myModel?.color
titleLabel.text = myModel?.commentString
}
}
- CollectionView 사용 핵심
- UICollectionView(frame:collectionViewLayout:) 초기화: 커스텀 layout객체 생성하여 주입
class ViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: [MyModel] = []
override func viewDidLoad() {
super.viewDidLoad()
setupDataSource()
configure()
}
private func configure() {
let collectionViewLayout = MyLayout()
collectionViewLayout.delegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.layer.borderWidth = 1
collectionView.backgroundColor = .systemBackground
collectionView.contentInset = UIEdgeInsets(top: 23, left: 10, bottom: 10, right: 10)
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 10).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: 700).isActive = true
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(MyCell.self, forCellWithReuseIdentifier: MyCell.id)
}
private func setupDataSource() {
dataSource = MyModel.getMock()
}
}
extension ViewController: MyCollectionViewLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForImageAtIndexPath indexPath: IndexPath) -> CGFloat {
return dataSource[indexPath.item].contentHeightSize
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print(dataSource.count)
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCell.id, for: indexPath)
if let cell = cell as? MyCell {
cell.myModel = dataSource[indexPath.item]
}
return cell
}
}
* source code: https://github.com/JK0369/CollectionViewEx
* 참고:
- apple documentation: https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts/customizing_collection_view_layouts
- raywnderlich: https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest
'iOS 응용 (swift)' 카테고리의 다른 글
Comments