iOS/UIKIT

category 표현하기

밤새는 탐험가89 2024. 7. 2. 15:34

 

구현 내용

  • 카테고리 내의 부분을 누르면 눌린 카테고리 색은 진하게 변하고, 그 밑에 밑줄이 생긴다. 
  • 카테고리 부분은 컬렉션 뷰로 구현되어 있다. 

 

구현 방법

1. categoryCell.swift 파일을 생성한다. 

import UIKit

class CategoryCell: UICollectionViewCell {
    
    // MARK: - Variables
    static let identifier = "CategoryCell"
    
    // MARK: - UI Components
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textAlignment = .center
        label.backgroundColor = .systemGray
        label.layer.cornerRadius = 8
        label.layer.masksToBounds = true
        label.font = UIFont.systemFont(ofSize: 16, weight: .bold)
        return label
    }()
    
    private let underlineView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .systemOrange
        return view
    }()
    
    // MARK: - Life Cycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(titleLabel)
        addSubview(underlineView)
        configureConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Functions
    func configure(with title: String, isSelected: Bool) {
        titleLabel.text = title
        titleLabel.textColor = isSelected ? .label : .gray
        underlineView.isHidden = !isSelected
    }
    
    // MARK: - UI Constraints
    private func configureConstraints() {
        let titleLabelConstraints = [
            titleLabel.topAnchor.constraint(equalTo: topAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
            titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
        ]
        
        let underlineViewConstraints = [
            underlineView.leadingAnchor.constraint(equalTo: leadingAnchor),
            underlineView.trailingAnchor.constraint(equalTo: trailingAnchor),
            underlineView.bottomAnchor.constraint(equalTo: bottomAnchor),
            underlineView.heightAnchor.constraint(equalToConstant: 2)
        ]
        
        NSLayoutConstraint.activate(titleLabelConstraints)
        NSLayoutConstraint.activate(underlineViewConstraints)
    }
}

 

2. CategoryMenuViewDelegate.swift 파일 생성한다.

import Foundation

protocol CategoryMenuViewDelegate: AnyObject {
    func categoryMenuView(_ categoryMenuView: CategoryMenuCell, didselectedItemAt index: Int)
}

 

3. CategoryMenuCell.swift 파일 생성한다.

import UIKit

class CategoryMenuCell: UIView {
    
    // MARK: Variables
    private let categories = ["Attractions", "Hotels", "Foods", "Shoppings", "Events"]
    private var selectedCategoryIndex = 0
    weak var delegate: (CategoryMenuViewDelegate)?
    
    // MARK: UI Components
    private lazy var categoryCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = 15
        layout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
        
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.backgroundColor = .clear
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.register(CategoryCell.self, forCellWithReuseIdentifier: CategoryCell.identifier)
        return collectionView
    }()
    
    // MARK: Life Cycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(categoryCollectionView)
        categoryCollectionView.delegate = self
        categoryCollectionView.dataSource = self
        configureConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: UI Constraints
    private func configureConstraints() {
        let categoryCollectionViewConstraints = [
            categoryCollectionView.topAnchor.constraint(equalTo: topAnchor),
            categoryCollectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
            categoryCollectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
            categoryCollectionView.bottomAnchor.constraint(equalTo: bottomAnchor)
            // categoryCollectionView.heightAnchor.constraint(equalToConstant: 50)
        ]
        NSLayoutConstraint.activate(categoryCollectionViewConstraints)
    }
}


// MARK: - Extensions
extension CategoryMenuCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return categories.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier, for: indexPath) as! CategoryCell
        cell.configure(with: categories[indexPath.item], isSelected: indexPath.item == selectedCategoryIndex)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let category = categories[indexPath.item]
        let width = category.size(withAttributes: [.font: UIFont.systemFont(ofSize: 16, weight: .bold)]).width + 20
        return CGSize(width: width, height: collectionView.bounds.height - 10)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        selectedCategoryIndex = indexPath.item
        collectionView.reloadData()
        delegate?.categoryMenuView(self, didselectedItemAt: indexPath.item)
    }
}

 

  • extensions 부분은 UICollectionView를 통해 카테고리를 표시하고, 각 카테고리 셀의 크기와 셀 선택 시의 동작을 정의한다.
  • 이 함수는 컬렉션 뷰에 표시할 항목의 개수를 반환한다. 여기서는 categories 배열의 개수를 반환하여, 배열에 있는 항목 수만큼 셀을 표시한다.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return categories.count
}

 

  • 이 함수는 컬렉션 뷰의 각 셀을 구성한다.
  • dequeueReusableCell(withReuseIdentifier:for:)를 사용하여 재사용 가능한 셀을 가져오는데, 여기서는 CategoryCell 타입으로 캐스팅한다.
  • cell.configure(with:isSelected:) 메서드를 호출하여 셀을 구성한다. categories[indexPath.item]로 해당 인덱스의 카테고리를 설정하고, indexPath.item == selectedCategoryIndex를 통해 현재 선택된 셀인지 여부를 전달한다.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCell.identifier, for: indexPath) as! CategoryCell
    cell.configure(with: categories[indexPath.item], isSelected: indexPath.item == selectedCategoryIndex)
    return cell
}

 

  • 이 함수는 각 셀의 크기를 결정한다.
  • categories[indexPath.item]로 현재 인덱스의 카테고리를 가져온다.
  • 카테고리 문자열의 폭을 계산하기 위해 size(withAttributes:) 메서드를 사용하고, 여기에 추가적인 여백(20)을 더한다.
  • 셀의 높이는 컬렉션 뷰의 높이에서 10을 뺀 값으로 설정한다.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let category = categories[indexPath.item]
    let width = category.size(withAttributes: [.font: UIFont.systemFont(ofSize: 16, weight: .bold)]).width + 20
    return CGSize(width: width, height: collectionView.bounds.height - 10)
}

 

  • 이 함수는 사용자가 셀을 선택했을 때 호출된다.
  • selectedCategoryIndex를 선택된 인덱스로 업데이트한다.
  • collectionView.reloadData()를 호출하여 컬렉션 뷰를 다시 로드하고 선택된 셀의 상태를 반영한다.
  • delegate?.categoryMenuView(self, didselectedItemAt:)를 호출하여 델리게이트에게 선택된 항목의 인덱스를 알린다.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    selectedCategoryIndex = indexPath.item
    collectionView.reloadData()
    delegate?.categoryMenuView(self, didselectedItemAt: indexPath.item)
}

 

 

4. 포로토콜 관련해서 함수를 구체적으로 선언한다.

class HomeViewController: UIViewController, CategoryMenuViewDelegate {
    
    func categoryMenuView(_ categoryMenuView: CategoryMenuCell, didselectedItemAt index: Int) {
        print("index - \(index)")
    }
    ...