Project/HiddenGem

✅ UICollectionViewCell을 공용으로 사용하려면? (데이터 타입도 다를때)

밤새는 탐험가89 2025. 5. 27. 12:18

 

 

✅ 현재 셀을 보면 공용으로 사용되는 것을 확인할 수 있음

 

따라서 먼저 각 셀의 UI를 공용으로 사용할 수 있도록 하나의 클래스를 생성함

/// 상세페이지 내에 각 셀에 공용으로 사용될 UI
final class TitleValueView: UIView {
    
    // MARK: - UI Component
    private let titleLabel: UILabel = UILabel()
    private let valueLabel: BasePaddingLabel = BasePaddingLabel()
    
    
    init(title: String = "", value: String = "") {
        super.init(frame: .zero)
        setupUI()
        configure(title: title, value: value)
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    
    private func setupUI() {
        titleLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .title3).pointSize)
        titleLabel.numberOfLines = 0
        titleLabel.textColor = .label
        
        valueLabel.font = UIFont.preferredFont(forTextStyle: .body)
        valueLabel.numberOfLines = 3
        valueLabel.textColor = .label
        valueLabel.backgroundColor = .systemBackground
        valueLabel.layer.cornerRadius = 10
        valueLabel.clipsToBounds = true
        
        let stackView = UIStackView(arrangedSubviews: [titleLabel, valueLabel])
        stackView.axis = .vertical
        stackView.spacing = 8
        stackView.alignment = .fill
        //stackView.distribution = .fill
        
        addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            
            stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            stackView.topAnchor.constraint(equalTo: topAnchor),
            stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
            
        ])
    }
    
    func configure(title: String?, value: String?) {
        
        guard let title = title,
              let value = value else { return }
        
        titleLabel.text = title
        valueLabel.text = value
    }
    
}

 

 

여기서 중요한게 바로 configure() 메서드 부분이다. 

여기 데이터 타입을 보면 title: String?, value: String? 이걸로, 제목과 그에 맞는 값을 받아온다. 

 

그리고 UICollectionViewCell 타입의 클래스를 하나 만든다. 

class DetailCommonCell: UICollectionViewCell {
    
    
    // MARK: - Variable
    
    static let reuseIdentifier: String = "DetailCommonCell"
    
    
    // MARK: - UI Component

//    private let addressView: TitleValueView = TitleValueView()
//    private let overviewView: TitleValueView = TitleValueView()
    private let mainStackView: UIStackView = UIStackView()
    
    
    // MARK: - Init
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.backgroundColor = .systemGray6
        
        setupUI()
        
    }
    
    
    override func prepareForReuse() {
        super.prepareForReuse()
        mainStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
        
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    

    
//    private func setupUI() {
//        
//        mainStackView.axis = .vertical
//        mainStackView.spacing = 12
//        mainStackView.alignment = .fill
//        
//        [addressView, overviewView].forEach {
//            mainStackView.addArrangedSubview($0)
//        }
//        
//        
//        mainStackView.translatesAutoresizingMaskIntoConstraints = false
//        contentView.addSubview(mainStackView)
//        
//        NSLayoutConstraint.activate([
//            
//            mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
//            mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
//            mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
//            mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
//            
//        ])
//    }

    
    private func setupUI() {
        
        mainStackView.axis = .vertical
        mainStackView.spacing = 8
        mainStackView.alignment = .fill
        mainStackView.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(mainStackView)
        
        NSLayoutConstraint.activate([
            
            mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
            mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
            
        ])
    }
    

//    func configure(with item: CommonIntroItem) {
//        
//        let addressValue = item.addr1
//        let overviewValue = item.overview
//        
//        addressView.configure(title: "주소", value: addressValue)
//        overviewView.configure(title: "소개", value: overviewValue)
//    }
    
    func configure(with items: [(title: String, value: String?)]) {
        mainStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
        
        for item in items {
            let view = TitleValueView()
            view.configure(title: item.title, value: item.value)
            mainStackView.addArrangedSubview(view)
        }
        
    }
}

 

 

보면 mainStackView를 하나 만들어 놓고,

configure() 메서드 내에서 받아온 데이터를 추가하고 있다. 

여기서 그 위에서 생성한 TitleValueView()를 가지고 데이터에 따라 view를 mainStackView에 추가한다.

 

특히 configure() 메서드의 데이터 타입을 보면 [(title: String, value: String?)] 로 했다. 

 

이렇게 하면 장점이 바로 API를 통해 받아온 데이터의 타입이 다르다.

위의 형식으로 사용하면 데이터 각 API 데이터 타입과 상관없이 공용으로 사용 할 수 있다. 

 

추가로 각 데이터 타입을 configure() 타입에 맞게 변환해야한다. 

아래 함수를 통해서 각 API 데이터 타입을 변환해서 저장했다. 

/// CommonIntroItem 타입의 데이터를 [(String, String?)] 타입으로 변환
private func makeCommonInfoData(item: CommonIntroItem) -> [(String, String?)] {

    return [
        ("주소", item.addr1),
        ("소개", item.overview)
    ]
}    

/// IntroInfoItem 타입의 데이터를 [(String, String?] 타입으로 변환
private func makeIntroInfoData(item: IntroInfoItem) -> [(String, String?)] {

    let firstMenu = item.firstmenu?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
    let treatMenu = item.treatmenu?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""


    let cleanedFirstMenu = firstMenu.replacingOccurrences(of: "\\s*[/,]+\\s*", with: ", ", options: .regularExpression)
    let cleanedTreatMenu = treatMenu
            .replacingOccurrences(of: "\\s*[/,]+\\s*", with: ", ", options: .regularExpression)

    let totalMenu = [cleanedFirstMenu, cleanedTreatMenu]
        .filter { !$0.isEmpty }
        .joined(separator: ", ")

    let finalValue = totalMenu.isEmpty ? nil : totalMenu

    return [
        ("메뉴", finalValue),
        ("영업시간", item.opentimefood),
        ("휴무일", item.restdatefood),
        ("전화번호", item.infocenterfood)
    ]
}

 

 

let firstMenu = item.firstmenu?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""

 

🔍 동작 설명

  • item.firstmenu: 옵셔널 문자열
  • ?.trimmingCharacters(in: .whitespacesAndNewlines):
    • 옵셔널이 nil이 아니면, 문자열 앞뒤에 있는 공백( ), 탭(\t), 줄바꿈(\n, \r) 등을 제거함
  • ?? "": item.firstmenu가 nil일 경우엔 빈 문자열로 대체