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일 경우엔 빈 문자열로 대체