티스토리 뷰

이미 블로그에 CollectionView를 가지고 pinterest 형식으로 layout을 만드는 방법을 포스팅 했지만 해당 포스팅은 storyboard와 코드를 조합해서 만들었고, 이번에는 온전히 코드만 가지고 만드는 방법에 대해서 작성한다. 앞서 포스팅 한 UITableView와 유사한 부분이 많은데, CollectionView는 안에 들어가는 cell의 크기와 위치를 정하기가 좀 더 자유롭다.

 

 

 

Layout

자유로운 cell의 위치와 크기는 UICollectionViewLayout이라는 Layout 객체로 관리할 수 있다. pinterest layout은 고정된 width와 유동적인 height를 가지는 cell을 일정한 간격으로 띄워서 화면에 보여주며, 불규칙한 모자이크 또는 지그재그로 놓인 벽돌을 보는 것 같은 느낌을 준다.

 

cell의 크기는 내용에 따라 달라지고 간격은 일정하다.

 

pinterest의 경우 휴대폰에서는 두 줄로 보이지만 가로가 더 넓은 pc에서 보면 window의 넓이에 따라 여러 줄로 표현된다. 마찬가지로 두 줄 뿐 아니라 여러 줄로 cell을 나열할 수 있는 layout을 만들어 보자. cell은 좌측 하단에 title을 가지고 small, medium, large 이렇게 세가지 크기를 가지며, 모서리가 둥근 사각형으로 그린다.

 

 

 

UICollectionViewCell

UITableView와 마찬가지로 Cell을 가지고 Collection을 구성한다. 테스트를 위해서 사용할 CollectionViewCell을 만들어 보자.

 

// MyCollectionViewCell.swift 
class MyCollectionViewCell: UICollectionViewCell {
	lazy var titleLabel: UILabel = {
		let titleLabel = UILabel()
		titleLabel.font = .boldSystemFont(ofSize: 24)
		return titleLabel
	}()

	override init(frame: CGRect) {
		super.init(frame: frame)
		contentView.backgroundColor = .white
		contentView.layer.cornerRadius = 15
		contetnView.layer.masksToBounds = true

		contentView.addSubview(titleLabel)
	}
}

 

모서리를 둥글게 그리기 위해서 layer 값을 조정했고, titleLabel은 나중에 외부에서 text를 수정할 수 있도록 빼 두었다. constraint를 설정하는 코드는 반복되는 부분이 너무 많아서 생략했다.

 

 

 

UICollectionViewController

이번에는 UIViewController를 상속받지 않고 UICollectionViewController를 바로 상속 받아서 새로운 Controller를 만들었다. 이렇게 만든 Controller는 collectionView를 기본적으로 가지고 있다. UITableViewController와 마찬가지로 Cell이라는 아이템을 가지고 있어서 이 Cell을 register하여 사용할 수 있다.

// MyCollectionViewController.swift
class MyCollectionViewController: UICollectionViewController {
	override func viewDidLoad() {
		super.viewDidLoad()

		collectionView.delegate = self
		collectionView.dataSource = self
		collectionView.register(MyCollectionViewCell.self, withReuseIdentifier: reuseIdentifier)
	}
}

 

delegate는 collectionView에서 발생한 이벤트를 처리할 수 있는 protocol을 제공하고, dataSource는 collectionView를 구성하기 위한 section과 cell의 갯수와 cell 인스턴스를 질의하는 protocol을 제공한다. MyCollectionViewController의 경우 UICollectionViewController를 바로 상속 받았기 때문에 따로 extension으로 확장하지 않고, class 안에서 이 protocol을 정의해서 사용할 수 있다.

 

 

 

CellData

정해진 개수로 cell을 초기화 하도록 해도 되지만 data를 가지고 처리하는 것이 재사용에 좋기 때문에 data를 초기화하고 dataSource에서 이 data를 사용해서 collectionView를 만들 것이다. 먼저 data로 사용할 struct를 만든다.

 

// MyCollectionViewController.swift
struct CellInfo {
	enum Type: Int {
		case small
		case medium
		case large
	}
	var title: String
	var type: Type
}

 

title로 사용할 String과 크기를 저장할 수 있도록 만들었다. 이 struct를 가지고 더미 데이터를 만들어서 사용할 것이다.

 

// MyCollectionViewController.swift
var cellInfos: [CellInfo] = []

cellInfos.append(CellInfo(title: "Red", type: .small)
cellInfos.append(CellInfo(title: "Green", type: .large)
cellInfos.append(CellInfo(title: "Blue", type: .medium)
// ...

 

 

 

DataSource

dataSource는 collectionView가 몇 개의 section을 사용할 지, section에 들어있는 cell의 개수는 몇 개인지 등을 질의한다. 위에서 만든 더미 데이터를 가지고 값을 반환해 준다. cell 객체도 질의하는데 이 때 더미 데이터에 있는 title String을 사용해서 titleLabel을 초기화 할 수 있다.

 

// MyCollectionViewController.swift
// MARK: DataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
	return 1
}

override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
	return cellInfos.count
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
	let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)

	if myCell = cell as? MyCollecionViewCell, indexPath.item < cellInfo.count {
		myCell.titleLabel.text = cellInfo[indexPath.item].title
		return myCell
	}

	return cell
}

 

CollectionView는 준비가 다 되었다. 본격적으로 layout을 만들 수 있게 되었다.

 

 

 

UICollectionViewLayout

CollectionViewLayout은 collectionView를 초기화 하거나 내용의 변화가 있을 때 마다 cell을 어디에 어떤 크기로 그릴지 관리한다. 이 정보를 UICollectionViewLayoutAttributes라고 하는데, 이 attributes를 더미 데이터를 가지고 초기화 해서 layout을 구현할 것이다.

만들고자 하는 pinterest layout은 세로로 스크롤 할 수 있는 layout이므로 필요한 변수들을 먼저 초기화 한다.

 

// MyCollectionViewLayout.swift
class MyCollectionViewLayout: UICollectionViewLayout {
	override var collectionViewContentSize: CGSize {
		get {
			return CGSize(width: contentWidth, height: contentHeight)
		}
	}

	var layoutAttributes: [UICollectionViewLayoutAttributes] = []

	var contentWidth: CGFloat {
		set { columnWidth = newValue / CGFloat(self.columnCount) }
		get { return self.columnWidth * CGFloat(self.columnCount) }
	}
	var contentHeight: CGFloat = 0
	var columnWidth: CGFloat = 0
	var columnCount: Int = 2
	var cellInset: UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
	var originY: [CGFloat] = []

}

 

각 변수들의 용도와 값은 바로 아래에서 나오니까 생략하고 넘어간다.

 

 

 

prepare

이곳에서 attributes를 초기화 할 수 있다.

 

// MyCollectionViewLayout.swift
override func prepare() {
	super.prepare()

	guard let controller = collectionView?.dataSource as? MyCollectionViewController,
				let collectionView = collectionView else {
		return
	}

	// offset
	originY = Array<CGFloat>.init(repeating: 0, count: columnCount)
	contentHeight = 0
	
	for section in 0..<collectionView.numberOfSections {
		for item in 0..<collectionView.numberOfItems(inSection: section) {
			let indexPath = IndexPath(item: item, section: section)
			let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)

			// find min column
			let column = minColumn(originY)
			
			let x = CGFloat(column) * columnWidth + cellInset.left
			let y = originY[column] + cellInset.top
			let width = columnWidth - cellInset.left - cellInset.right
			let height = cellHeight(type: controller.cellInfos[item].type)
			attributes.frame = CGRect(x: x, y: y, width: width, height: height)

			originY[column] += height + cellInset.bottom

			if contentHeight < originY[column] {
				contentHeight = originY[column]
			}
			
			layoutAttributes.append(attributes)
		}
	}

}

 

originY는 각 column의 가장 아래 좌표를 저장하고 있으며, 이 값이 가장 작은 곳에 새로운 cell을 추가해서 column을 확장할 수 있다. controller의 cellInfo를 바탕으로 cell height를 계산하며, 확장된 column의 높이에 따라 contentHeight를 증가시켜서 최종적으로 collectionView의 contentSize를 계산할 수 있도록 한다.

 

 

 

layoutAttributesForElements

attributes를 임의로 만들었기 때문에 이 중에서 화면에 보이는 부분만 그릴 수 있도록 반환해 주어야 한다.

 

// MyCollectionViewLayout.swift
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
	var visibleAttributes: [UICollectionViewLayoutAttributes] = []

	for attributes in layoutAttributes {
		if rect.intersects(attributes.frame) {
			visibleAttributes.append(attributes)
		}
	}

	return visibleAttributes
}

 

이제 다 끝났다. 위에서 만든 MyCollectionView, MyCollectionViewCell, MyCollectionViewLayout을 조합해서 결과를 출력해보자.

 

 

 

Initialize

storyboard에서 만들었다면 Layout class를 GUI로 설정할 수 있지만 storyboard의 사용을 지양하고 있기 때문에 코드로 초기화 하는 방법을 알아보자. UICollectionViewController는 다른 ViewController들과 달리 UICollectionViewLayout 객체를 필요로 한다. 우리는 각 ViewController와 ViewLayout을 상속받은 class를 작성했기 때문에 생성할 때 사용할 수 있다.

 

// SceneDelegate.swift
let layout = MyCollectionViewController()
layout.columnCount = 2
let controller = MyCollectionViewController(collectionViewLayout: layout)

window = UIWindow(frame: windowScene.coordinateSpace.bound)
window?.windowScene = windowScene
window?.rootViewController = controller
window?.makeKeyAndVisible()

 

그리고 CollectionViewController로 이동해서 contentWidth를 설정해준다.

 

// MyCollectionViewController.swift
override func viewDidLoad() {
	// ...
	guard let layout = collectionViewLayout as? MyCollectionViewLayout else {
		return
	}
	layout.contentWidth = collectionView.frame.width - collectionView.contentInset.left - collectionView.contentInset.right
}

 

 

 

Result

크기에 따라서 각 cell이 제자리를 찾아가는 것을 볼 수 있다.

 

비슷해 보이지만 "Three" cell이 small일 경우 "Four"가 더 짧은 첫번째 column 아래에 추가된다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함