스파르타 코딩 클럽 - iOS 스타터 6기/최종 팀 프로젝트 - 잇츠오케이
75. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #17
seongpil Heo
2025. 6. 30. 23:11
🎯 Trouble Shooting
[ 1. page count 라벨의 위치 문제 ]
페이지 카운트 라벨을 플로팅 형태로 첫번째 섹션 위에 띄우려고 했는데
화면을 내리면 같이 따라 내려오거나, 이미지를 스크롤 할 때 해당 라벨을 가리는 문제 발생
※ 컬렉션 뷰 안에 컬렉션 뷰를 만들어서 해결된다고 튜터님께서 조언해주심
※ 또는 온보딩 페이지에 썼던 방법을 참고하면 된다고 하심
👨🏻💻 오늘의 작업
[ 1. Summary View 레이아웃 구현 ]
Summary View CollectionView Compositional Layout 작성
[Feat] #158 - CollectionView Compositional 기본 Layout 구현 by heopill · Pull Request #161 · uddt-ds/EatsOkay
📌 관련 이슈 closed: #158 📌 변경 사항 및 이유 Summary View 구현을 위해 기본이 되는 CollectionView Layout 구현 웹뷰 버튼에서 사용할 이미지 Asset 추가 📌 PR Point Compositional Layout 사용 📌 참고 사항 추
github.com
담당 섹션 레이아웃 구현
Summary View Controller 코드
//
// SummaryViewController.swift
// EatsOkay
//
// Created by 허성필 on 6/30/25.
//
import UIKit
import SnapKit
import RxSwift
class SummaryViewController: UIViewController {
let data = [1, 2, 3]
let data2 = [1]
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createCompositionalLayout())
collectionView.register(SectionOneViewCell.self, forCellWithReuseIdentifier: SectionOneViewCell.identifier)
collectionView.register(SectionTwoViewCell.self, forCellWithReuseIdentifier: SectionTwoViewCell.identifier)
collectionView.register(SectionThreeViewCell.self, forCellWithReuseIdentifier: SectionThreeViewCell.identifier)
collectionView.register(SectionFourViewCell.self, forCellWithReuseIdentifier: SectionFourViewCell.identifier)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "DefaultCell")
collectionView.register(CustomHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: CustomHeaderView.identifier)
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
private let photoPageLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "1 / 3",
font: UIFont.customFontForBody(weight: .w500),
color: .bgColor
)
label.backgroundColor = UIColor.customColor(hexCode: .neutral950).withAlphaComponent(0.6)
label.layer.cornerRadius = 13
label.clipsToBounds = true
label.textAlignment = .center
return label
}()
private let webViewButton: UIButton = {
let button = CustomButton(title: " 웹에서 보기")
button.setImage(UIImage(named: "WebIcon"), for: .normal)
button.setImage(UIImage(named: "WebIcon"), for: .highlighted)
button.imageView?.contentMode = .scaleAspectFill
return button
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.isHidden = false
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.isHidden = true
}
private func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
switch sectionIndex {
case 0:
// 첫번째 섹션 레이아웃 만들기
return self.createOneSection()
case 1:
// 두번째 섹션 레이아웃 만들기
return self.createTwoSection()
case 2:
// 세번째 섹션 레이아웃 만들기
return self.createThreeSection()
case 3:
// 네번째 섹션 레이아웃 만들기
return self.createFourSection()
default:
return self.createDefaultSectionLayout()
}
}
return layout
}
// 첫번째 섹션 - 성필
private func createOneSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.32948))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
// 두번째 섹션 - 성필
private func createTwoSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(0.3196))
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
return section
}
// 세번째 섹션 - 혜민
private func createThreeSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(40))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
// 네번째 섹션 - 기태
private func createFourSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(40))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
return section
}
// 기본 섹션 레이아웃 만들기
private func createDefaultSectionLayout() -> NSCollectionLayoutSection {
// 1. item Size 만들기
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
// 2. item을 만들기
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// 3.Group Size 만들기
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100))
// 4.group 만들기
let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
// 5.section 만들기
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .continuous
return section
}
private func setupView() {
view.backgroundColor = .customColor(hexCode: .bgColor)
[webViewButton, collectionView].forEach{
view.addSubview($0)
}
// button autoLayout 설정
webViewButton.snp.makeConstraints { make in
make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-10)
make.centerX.equalToSuperview()
make.horizontalEdges.equalToSuperview().inset(20)
make.height.equalTo(60)
}
// collectionView autoLayout 설정
collectionView.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview()
make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top)
make.bottom.equalTo(webViewButton.snp.top).offset(-10)
}
}
}
extension SummaryViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return data.count
} else {
return data2.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch indexPath.section {
case 0:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SectionOneViewCell.identifier, for: indexPath) as? SectionOneViewCell else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "DefaultCell", for: indexPath)
}
cell.backgroundColor = .clear
return cell
case 1:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SectionTwoViewCell.identifier, for: indexPath) as? SectionTwoViewCell else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "DefaultCell", for: indexPath)
}
return cell
case 2:
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SectionThreeViewCell.identifier, for: indexPath) as? SectionThreeViewCell else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "DefaultCell", for: indexPath)
}
cell.contentView.backgroundColor = .green
let text = "\(indexPath.section)_\(indexPath.item)"
cell.update(text: text)
return cell
default :
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SectionFourViewCell.identifier, for: indexPath) as? SectionFourViewCell else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "DefaultCell", for: indexPath)
}
cell.contentView.backgroundColor = .gray
let text = "\(indexPath.section)_\(indexPath.item)"
cell.update(text: text)
return cell
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 4
}
// 헤더 설정
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomHeaderView.identifier, for: indexPath) as? CustomHeaderView else {
return UICollectionReusableView()
}
if indexPath.section == 2 {
header.configure(with: "매장 특징")
} else if indexPath.section == 3 {
header.configure(with: "위치")
}
return header
}
return UICollectionReusableView()
}
}
첫번째 Section
//
// SectionOneViewCell.swift
// EatsOkay
//
// Created by 허성필 on 6/30/25.
//
import UIKit
import SnapKit
class SectionOneViewCell: UICollectionViewCell {
static let identifier = "SectionOneViewCell"
required init?(coder: NSCoder) {
super.init(coder: coder)
configureView()
}
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "DefaultImage")
imageView.contentMode = .scaleAspectFit
return imageView
}()
private func configureView() {
self.backgroundColor = UIColor.customColor(hexCode: .bgColor)
self.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.height.equalToSuperview()
}
}
func update(imageUri: String) {
}
}
두번째 Section
//
// SectionTwoViewCell.swift
// EatsOkay
//
// Created by 허성필 on 6/30/25.
//
import UIKit
import SnapKit
class SectionTwoViewCell: UICollectionViewCell {
static let identifier = "SectionTwoViewCell"
required init?(coder: NSCoder) {
super.init(coder: coder)
configureView()
}
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
private let regionLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "서울 강남구 • 일식당 ",
font: UIFont.customFontForBody(weight: .w500),
color: .neutral400
)
return label
}()
private let storeLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "식당이름입니다 ",
font: UIFont.customFontForHeader(weight: .w950),
color: .neutral950
)
return label
}()
private let rateImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "Star")
return imageView
}()
private let rateLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "4.8",
font: UIFont.customFontForHeader(weight: .w700),
color: .neutral950
)
return label
}()
private let dotLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "•",
font: UIFont.customFontForBody(weight: .w500),
color: .neutral950
)
return label
}()
private let reviewLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "리뷰 40개",
font: UIFont.customFontForSubtitle(weight: .w700),
color: .neutral950
)
return label
}()
private let separator = CustomSeparator(color: .neutral50)
private let timeImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "Time")
return imageView
}()
private let timeLabel: UILabel = {
let label = UILabel()
label.attributedText = AttributedStringManager.configureString(
text: "영업 종료 • 18:00에 영업 시작",
font: UIFont.customFontForBody(weight: .w600),
color: .neutral800
)
return label
}()
private let callButton: UIButton = {
var configuration = UIButton.Configuration.plain()
configuration.image = UIImage(named: "Call")
configuration.imagePlacement = .leading
configuration.imagePadding = 4
configuration.contentInsets = .init(top: 8, leading: 16, bottom: 8, trailing: 16)
configuration.attributedTitle = AttributedStringManager.configureString(
text: "전화",
font: .customFontForBody(weight: .w600),
color: .neutral700
)
configuration.background.strokeWidth = 1
configuration.background.strokeColor = UIColor.customColor(hexCode: .neutral100)
configuration.background.cornerRadius = 50
let button = UIButton(configuration: configuration)
return button
}()
private let secondSeparator = CustomSeparator(color: .neutral50)
private func configureView() {
[regionLabel, storeLabel, rateImageView, rateLabel, dotLabel, reviewLabel,
separator ,timeImageView, timeLabel, callButton, secondSeparator].forEach {
addSubview($0)
}
regionLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.leading.equalToSuperview().offset(20)
}
storeLabel.snp.makeConstraints { make in
make.top.equalTo(regionLabel.snp.bottom).offset(10)
make.leading.equalToSuperview().offset(20)
}
rateImageView.snp.makeConstraints { make in
make.leading.equalToSuperview().offset(20)
make.centerY.equalTo(rateLabel)
}
rateLabel.snp.makeConstraints { make in
make.top.equalTo(storeLabel.snp.bottom).offset(10)
make.leading.equalTo(rateImageView.snp.trailing).offset(4)
}
dotLabel.snp.makeConstraints { make in
make.centerY.equalTo(rateLabel)
make.leading.equalTo(rateLabel.snp.trailing).offset(4)
}
reviewLabel.snp.makeConstraints { make in
make.top.equalTo(storeLabel.snp.bottom).offset(10)
make.leading.equalTo(dotLabel.snp.trailing).offset(4)
}
separator.snp.makeConstraints { make in
make.horizontalEdges.equalToSuperview().inset(20)
make.top.equalTo(reviewLabel.snp.bottom).offset(20)
make.height.equalTo(2)
}
timeImageView.snp.makeConstraints { make in
make.centerY.equalTo(timeLabel)
make.leading.equalToSuperview().offset(20)
}
timeLabel.snp.makeConstraints { make in
make.top.equalTo(separator).offset(20)
make.leading.equalTo(timeImageView.snp.trailing).offset(4)
}
secondSeparator.snp.makeConstraints { make in
make.height.equalTo(8)
make.horizontalEdges.equalToSuperview()
make.top.equalTo(timeLabel.snp.bottom).offset(20)
}
callButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.trailing.equalToSuperview().inset(20)
}
}
func update(StoreInfo: [StoreInfo]) {
}
}