[UIKit] Collection View 검색 기능
Collection View에서 cell의 section을 검색하여 검색한 내용을 필터링해서 보여주는 기능을 만들어보자.
Search Collection View
이 프로젝트에서는 storyboard를 사용하지 않고 만들었다.
storyboard를 사용하지 않고 코드로 작성하는 방법은 아래의 링크에 정리해뒀다.
2023.11.05 - [스위프트] - [swift] storyboard없이 Navigation controller
또한 autolayout을 쉽게 구현하도록 하는 Snapkit 라이브러리를 사용하였다.
CocoaPod을 이용해 Snapkit 라이브러리를 설치해야 한다.
[swift] CocoaPod 설치와 사용법
iOS 및 macOS 애플리케이션에서 사용할 수 있는 라이브러리 및 코드 모듈을 공유하기 위한 패키지 관리 도구이다. CocoaPod 사용법에 대해 배워보자. CocoaPod CocoaPod이란? CocoaPod은 개발에서 사용하는
steelbeartaeng2.tistory.com
디렉터리 구조
- CollectionViewMain: collectionView의 Delegate/DataSource, 검색 로직
- CollectionViewHeader: section 헤더 설정
- CollectionViewMainCell: collectionViewCell 설정
- ImageData: 이미지 데이터 목록
주요 구성 요소
- collectionView: 이미지 데이터 표시
- collectionViewCell: CollectionView에 보여질 customCell
- collectionReusableView: 섹션 헤더 정의
- searchInputText: 검색 바
- seletedCellCountLabel: 선택된 이미지 항목의 수
- topBarView: 검색 바 및 선택된 항목 표시 label을 포함하는 상단 바 뷰
- ImageData: image data 정의
핵심 기능
1️⃣ 검색 및 스크롤:
- textfield에 입력된 텍스트를 기반으로 해당 섹션으로 스크롤
2️⃣ collectionView 데이터 정렬 및 헤더 표시:
- 섹션 별 title과 cell 표시
3️⃣ cell 선택 및 업데이트:
- 이미지 선택시 경계선 강조
- 선택된 항목 수에 따라 label 업데이트와 textfield 위치 동적으로 조절
CollectionViewController
UICollectionViewDelegate
collectionView의 동작과 이벤트를 커스터마이징할 수 있다.
- collectionView(_:didSelectItemAt:): 사용자가 셀을 탭했을 때 호출
- collectionView(_:willDisplay:forItemAt:): 셀이 화면에 나타나기 전에 호출
UICollectionViewDataSource
collectionView에 표시될 데이터와 각 셀의 구성을 제공한다.
- collectionView(_:numberOfItemsInSection:): 섹션의 아이템 개수 반환
- collectionView(_:cellForItemAt:): 각 아이템에 대한 셀을 반환
- collectionView(_:viewForSupplementaryElementOfKind: at:): 섹션의 보충요소 반환
- numberOfSections(in:): 컬렉션 뷰에 표시할 섹션의 총 수를 반환
UICollectionViewDelegateFlowLayout
주로 아이템의 크기, 간격등의 레이아웃 설정을 다루는 데 사용
- collectionView(_:layout:minimumLineSpacingForSectionAt:): 컬렉션 뷰의 섹션 간 수직으로 항목 간 최소 간격 설정
- collectionView(_:layout:minimumInteritemSpacingForSectionAt:): 컬렉션 뷰의 섹션 내 수평으로 항목 간의 최소 간격 설정
- collectionView(_:layout:sizeForItemAt:): 컬렉션 뷰의 항목 크기를 설정
- collectionView(_:layout:referenceSizeForHeaderInSection:): 섹션 헤더의 크기 설정
- collectionView(_:layout:insetForSectionAt:): 섹션 내부 여백을 설정
구현
1️⃣ image 저장
assets에 다음과 같이 색상 사진을 저장해준다.
다음과 같이 ImageData를 만들어 assets에 저장된 색상 사진을 적용하여 배열을 만든다.
- ImageData.swift
import UIKit
struct ImageData{
let imageName: String
let image: UIImage
let sectionName: String
}
extension ImageData{
static let list: [ImageData] = [
ImageData(imageName: "blue", image: UIImage(named: "blue")!, sectionName: "Section1"),
ImageData(imageName: "red", image: UIImage(named: "red")!, sectionName: "Section1"),
ImageData(imageName: "purple", image: UIImage(named: "purple")!, sectionName: "Section1"),
ImageData(imageName: "green", image: UIImage(named: "green")!, sectionName: "Section1"),
ImageData(imageName: "pink", image: UIImage(named: "pink")!, sectionName: "Section2"),
ImageData(imageName: "navy", image: UIImage(named: "navy")!, sectionName: "Section2"),
ImageData(imageName: "yellow", image: UIImage(named: "yellow")!, sectionName: "Section3")
]
}
2️⃣ 구성요소 설정
🔎 데이터 구조 정의
- sectionData: [String: [ImgaeData]] 이미지 데이터를 섹션별로 구분하여 저장
- selectedItems: [IndexPath: Bool] 선택된 cell인지 판단
custom Cell
custom cell은 tableview에서 정리해놨으니 참고바란다.
[swift] TableView + Custom Cell
iOS 애플리케이션 개발에서 중요한 컴포넌트 중 하나가 UITableView이다. 테이블 형식의 데이터를 표시하는데 사용하며 데이터를 목록 또는 그리드 형태로 표시할 수 있으며 주로 목록 형식으로 사
steelbeartaeng2.tistory.com
import UIKit
import SnapKit
class CollectionViewMainCell: UICollectionViewCell {
let cellImage: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .center
return imageView
}()
func initCell() {
addSubview(cellImage)
cellImage.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
collectionView와 custom cell을 아래와 같이 연결해줘야 화면에 잘 나오게 된다.
SectionHeader
섹션 헤더를 표현하기 위해 UICollectionReusableView를 상속받아 선언해 준다.
import UIKit
import SnapKit
class CollectionViewHeader: UICollectionReusableView {
let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 16)
label.textColor = .black
return label
}()
func initHeader() {
addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(50)
make.centerX.equalToSuperview()
}
}
}
ViewController에서 CollectionView에 섹션 헤더를 등록해보자.
- CollectionViewHeader.self : 등록할 View의 타입 지정
- forSupplementaryViewOfKind: 등록할 View의 종류, 위에서는 섹션 헤더를 나타내기 위해UICollectionView.elementKindSectionHeader를 사용
- withReuseIdentifier: 재사용을 위한 식별자 지정
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderView", for: indexPath) as! CollectionViewHeader
headerView.initHeader()
let sectionName = Array(sectionData.keys)[indexPath.section]
headerView.titleLabel.text = sectionName
return headerView
} else {
return UICollectionReusableView()
}
}
3️⃣ 이미지 섹션 별로 구분
organizeDataIntoSections() 는 ImageData.list에서 가져온 데이터를 섹션 별로 구분하기 위한 메서드이다.
저장된 sectionName에 따라 sectionData에 key, value 쌍으로 저장된다.
private func organizeDataIntoSections() {
for imageData in ImageData.list {
if sectionData[imageData.sectionName] == nil {
sectionData[imageData.sectionName] = []
}
sectionData[imageData.sectionName]?.append(imageData)
}
}
4️⃣ cell 선택
cell 선택 action
- delegate로 선택된 cell인지 아닌지 판단을 하고 selectedItems에 true/false 값을 저장한다.
- collectionView.reloadItems(at: [indexPath])로 collectionView를 reload하여 true/false에 따라 디자인을 설정한다.
extension CollectionViewMain: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
if selectedItems[indexPath] == true {
// Deselect
selectedItems[indexPath] = false
} else {
// Select
selectedItems[indexPath] = true
}
collectionView.reloadItems(at: [indexPath])
updateSelectedItemCount()
}
}
}
- collectionView가 reload될 때마다 seletedItems가 true/false 값에 따라 디자인을 다르게 적용해준다.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewMainCell", for: indexPath) as! CollectionViewMainCell
cell.initCell()
let sectionName = Array(sectionData.keys)[indexPath.section]
let imageData = sectionData[sectionName]?[indexPath.row]
cell.cellImage.image = imageData?.image
if let isSelected = selectedItems[indexPath], isSelected {
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 4
} else {
cell.layer.borderColor = UIColor.clear.cgColor
cell.layer.borderWidth = 0
}
return cell
}
선택된 항목 수에 따라 업데이트
selectedItems의 true/false 값으로 선택된 cell의 수를 확인한다.
선택된 cell의 개수에 따라 seletecItemCount의 text가 바뀐다.
func updateSelectedItemCount() {
let count = selectedItems.values.filter { $0 == true }.count
if count > 0{
seletecItemCount.text = "\(count) photo selected"
searchInputText.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(60)
make.height.equalTo(40)
make.width.equalTo(150)
make.leading.equalTo(view.snp.leading).offset(50)
}
}else{
seletecItemCount.text = ""
searchInputText.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(60)
make.height.equalTo(40)
make.width.equalTo(150)
make.centerX.equalToSuperview()
}
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()//즉시 업데이트
}
}
5️⃣ 검색 기능 구현
사용자가 textField에 입력한 텍스트와 sectionData의 key값과 일치한다면 해당 섹션으로 스크롤 되도록 한다.
extension CollectionViewMain: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let searchText = textField.text, !searchText.isEmpty {
for (section, _ ) in sectionData {
if section.lowercased() == searchText.lowercased() {//대소문자 구분없이
if let sectionIndex = Array(sectionData.keys).firstIndex(of: section) {
let indexPath = IndexPath(item: 0, section: sectionIndex)
collectionView.scrollToItem(at: indexPath, at: .top, animated: true)
}
}
}
}
textField.resignFirstResponder()//키보드 내리기
return true
}
}