티스토리 뷰
Customizing Collection View Layouts
flow의 셀 크기를 변경하거나 모자이크 스타일을 구현
Overview
간단한 그리드에 UICollectionView셀을 배치하려면 UICollectionViewFlowLayout을 직접 사용할 수 있습니다.
더 여러가지를 표현하려면 UICollectionViewLayout를 상속하여 고급 레이아웃을 만들 수 있습니다.
이 샘플 앱은 두 가지 커스텀 레이아웃 서브클래스를 보여줍니다.
1. ColumnFlowLayout
UICollectionViewFlowLayout은좁은 화면을 위한 목록 형식으로 셀을 정렬하거나 넓은 화면을 위한 그리드로 셀을 정렬하는 서브클래스입니다.
아래 “For a Simple Grid, Size Cells Dynamically,”를 참고하세요.
2. MosaicLayout
UICollectionViewLayout는 모자이크 스타일처럼 전형적이지 않은 그리드를 배치하는 서브클래스입니다.
아래 “For a Complex Grid, Define Cell Sizes Explicitly,” 를 참고하세요.
이 앱은 사람 목록을 보여주는 플로우 레이아웃을 사용하는 Friends 뷰 컨트롤러가 열립니다.
원하는 셀을 누르면 피드 뷰 컨트롤러로 이동하는데, 피드 뷰 컨트롤러는 모자이크 레이아웃을 사용하여 사용자의 사진 라이브러리에서 사진을 표시합니다.
클라우드 아이콘을 누르면 항목을 삽입, 삭제, 이동, 리로드를 하기 위한 애니메이션이 표시됩니다.
자세한 내용은 아래 “Perform Batch Updates,” 을 참조하십시오.
컬렉션 뷰에서 pull-to-refresh를 이용하면 데이터가 리셋됩니다.
For a Simple Grid, Size Cells Dynamically
ColumnFlowLayout은 컬렉션 뷰의 크기를 사용하여 셀 너비를 결정하는 UICollectionViewFlowLayout 의 서브클래스입니다.
하나의 셀이 수평으로 딱 맞으면 셀은 컬렉션 뷰의 전체 너비만큼 배열합니다.
그렇지 않으면 여러 셀이 일정한 너비로 표시됩니다.
아이폰을 세로모드로 보면 하나의 셀이 보입니다.
가로 모드나 아이패드에서는 그리드 형태로 보여줍니다.
prepare()를 사용해서 디바이스의 사용가능한 화면 너비를 계산하고 그에 따라 itemSize 프로피터를 설정합니다.
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let availableWidth = collectionView.bounds.inset(by: collectionView.layoutMargins).width
let maxNumColumns = Int(availableWidth / minColumnWidth)
let cellWidth = (availableWidth / CGFloat(maxNumColumns)).rounded(.down)
self.itemSize = CGSize(width: cellWidth, height: cellHeight)
self.sectionInset = UIEdgeInsets(top: self.minimumInteritemSpacing, left: 0.0, bottom: 0.0, right: 0.0)
self.sectionInsetReference = .fromSafeArea
}
For a Complex Grid, Define Cell Sizes Explicitly
UICollectionViewFlowLayout 대신에 UICollectionViewLayout의 서브클래스로 더 많은 커스터마이즈를 할 수 있습니다.
모자이크 레이아웃은 UICollectionViewLayout서브클래스로 임의의 개수의 셀을 다른 사이즈와 비율로 보여줄 수 있습니다.
피드 뷰 컨트롤러에서 모자이크 레이아웃으로 사용자의 사진 라이브러리에 있는 이미지를 보여줍니다.
셀은 4가지 유형 중 하나로 구성됩니다.
Calculate Cell Dimensions
prepare()메서드는 레이아웃이 무효화될 때마다 호출됩니다.
이 메서드를 override해서 모든 셀의 위치와 크기 및 전체 레이아웃의 총 치수를 계산합니다.
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
// 캐시된 이미지 리셋
cachedAttributes.removeAll()
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
// 컬렉션뷰의 모든 아이템에:
// - 속성 준비하기
// - cachedAttributes array에 속성 저장하기
// - contentBounds 와 attributes.frame 결합하기
let count = collectionView.numberOfItems(inSection: 0)
var currentIndex = 0
var segment: MosaicSegmentStyle = .fullWidth
var lastFrame: CGRect = .zero
let cvWidth = collectionView.bounds.size.width
while currentIndex < count {
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: 200.0)
var segmentRects = [CGRect]()
switch segment {
case .fullWidth:
segmentRects = [segmentFrame]
case .fiftyFifty:
let horizontalSlices = segmentFrame.dividedIntegral(fraction: 0.5, from: .minXEdge)
segmentRects = [horizontalSlices.first, horizontalSlices.second]
case .twoThirdsOneThird:
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3.0), from: .minXEdge)
let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge)
segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
case .oneThirdTwoThirds:
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3.0), from: .minXEdge)
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
}
// 계산된 프레임에 대한 레이아웃 속성값 만들고 캐시하기
for rect in segmentRects {
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
attributes.frame = rect
cachedAttributes.append(attributes)
contentBounds = contentBounds.union(lastFrame)
currentIndex += 1
lastFrame = rect
}
// 다음 세그먼트 스타일 정하기
switch count - currentIndex {
case 1:
segment = .fullWidth
case 2:
segment = .fiftyFifty
default:
switch segment {
case .fullWidth:
segment = .fiftyFifty
case .fiftyFifty:
segment = .twoThirdsOneThird
case .twoThirdsOneThird:
segment = .oneThirdTwoThirds
case .oneThirdTwoThirds:
segment = .fiftyFifty
}
}
}
}
Provide the Content Size
컬렉션뷰의 사이즈를 제공하여 collectionViewContentSize 프로퍼티를 override하기
override var collectionViewContentSize: CGSize {
return contentBounds.size
}
Define the Layout Attributes
layoutAttributesForElements(in:)를 override해서 기하학적 영역에 대한 레이아웃 특성을 정의하세요.
컬렉션 뷰는 geometric region에 의한 querying으로 알려진 아이템을 보여주기 위해서 이 function을 주기적으로 호출합니다.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributesArray = [UICollectionViewLayoutAttributes]()
// 쿼리 내에 있는 셀을 찾습니다.
guard let lastIndex = cachedAttributes.indices.last,
let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
// Starting from the match, loop up and down through the array until all the attributes
// have been added within the query rect.
for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
guard attributes.frame.maxY >= rect.minY else { break }
attributesArray.append(attributes)
}
for attributes in cachedAttributes[firstMatchIndex...] {
guard attributes.frame.minY <= rect.maxY else { break }
attributesArray.append(attributes)
}
return attributesArray
}
또한 layoutAttributesForItem(at:).을 구현하여 특정 아이템의 레이아웃 특성을 제공합니다.
컬렉션 뷰는 이 함수를 주기적으로 호출하여 인덱스 경로로별 쿼리라고 하는 특정 아이템을 표시합니다.
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
이러한 function이 자주 호출되기 때문에 앱의 성능에 영향을 줄 수 있습니다.
이러한 코드를 최대한 효율적으로 만들려면 가능한 예제 코드를 따라하세요.
Handle Bounds Changes
shouldInvalidateLayout(forBoundsChange:) 는 컬렉션 뷰의 모든 bounds 변경 또는 크기나 원점이 변경될 때마다 호출됩니다.
이 function은 스크롤 도중에도 자주 호출됩니다.
기본 구현에서 false를 반환하고, 사이즈나 원점이 바뀌면 true를 반환합니다.
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
return !newBounds.size.equalTo(collectionView.bounds.size)
}
이 샘플은 선형검색 대신 최적의 성능을 위해 주어진 바운드 영역에 각각의 요소를 위해 필요한 속성을 layoutAttributesForElements(in:)내에서 이진 검색을 수행합니다.
Perform Batch Updates
컬렉션 뷰의 오른쪽 상단 버튼을 누르면 삽입, 삭제, 이동, 리로드 애니메이션 작업을 동시에 일괄 업데이트합니다.
performBatchUpdates(_:completion:) 호출 내에서 모든 삽입, 삭제, 이동, 리로드 작업이 동시에 애니메이션 됩니다.
이 샘플에서 배치 업데이트는 PersonUpdate 객체의 배열을 처리해서 만들어집니다.
각 객체는 다음과 같이 하나의 업데이트를 캡슐화합니다.
- Person 객체를 삽입하고 인덱스를 삽입
- 인덱스를 삭제
- 인덱스를 이동
- 인덱스를 리로드
첫번째, 리로드 작업은 셀 이동이랑 관련이 없으므로 애니메이션 없이 수행됩니다.
// 셀 이동이 없으므로 애니메이션 없이 리로드됩니다.
UIView.performWithoutAnimation {
collectionView.performBatchUpdates({
for update in remoteUpdates {
if case let .reload(index) = update {
people[index].isUpdated = true
collectionView.reloadItems(at: [IndexPath(item: index, section: 0)])
}
}
})
}
다음으로, 다음 작업이 애니메이션됩니다.
// 다른 모든 업데이트가 같이 애니메이션됩니다.
collectionView.performBatchUpdates({
var deletes = [Int]()
var inserts = [(person:Person, index:Int)]()
for update in remoteUpdates {
switch update {
case let .delete(index):
collectionView.deleteItems(at: [IndexPath(item: index, section: 0)])
deletes.append(index)
case let .insert(person, index):
collectionView.insertItems(at: [IndexPath(item: index, section: 0)])
inserts.append((person, index))
case let .move(fromIndex, toIndex):
// Updates that move a person are split into an addition and a deletion.
collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
to: IndexPath(item: toIndex, section: 0))
deletes.append(fromIndex)
inserts.append((people[fromIndex], toIndex))
default: break
}
}
// 삭제
for deletedIndex in deletes.sorted().reversed() {
people.remove(at: deletedIndex)
}
// 삽입
let sortedInserts = inserts.sorted(by: { (personA, personB) -> Bool in
return personA.index <= personB.index
})
for insertion in sortedInserts {
people.insert(insertion.person, at: insertion.index)
}
// 업데이트 버튼은 리스트에 사람이 있을때만 가능합니다.
navigationItem.rightBarButtonItem?.isEnabled = !people.isEmpty
})
'iOS' 카테고리의 다른 글
[iOS] DispatchQueue 알아보기 (0) | 2021.01.18 |
---|---|
[iOS] Dispatch 프레임워크 알아보기 (0) | 2021.01.18 |
[iOS] UICollectionView Compositional Layout 알아보기 (0) | 2021.01.15 |
[iOS] UICollectionViewLayout 알아보기 (0) | 2021.01.14 |
[iOS] UICollectionViewFlowLayout 알아보기 (0) | 2021.01.14 |