스파르타 코딩 클럽 - 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]) {
        
    }
}