스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

27. 스파르타 코딩 클럽 - 팀 프로젝트 3일차

seongpil Heo 2025. 4. 9. 23:03

 👥 팀 프로젝트 - 키오스크 앱 만들기

오늘은 4월 9일 팀 프로젝트 3일차이다.

오늘 작업해야 될 내용은 아래와 같다.

  1. 현재 Tabel View를 Label, Button이 추가된 Custom Table View로 변경하기
  2. 버튼 클릭 시 이벤트 처리하기
  3. Table View 아래에 Label을 추가해서 총 개수와 총금액 표시하기
  4. Figma에 작성된 제약조건과 동일하게 Xcode에서 UI 제약조건 설정하기

먼저 작업에 들어가기 전에 UITableView에 대해 알아보고 작업을 시작하겠다.

 

UITableView | Apple Developer Documentation

A view that presents data using rows in a single column.

developer.apple.com

 

Apple Developer Documentation에 따르면
단일 열에 행을 사용하여 데이터를 표시하는 뷰라고 설명되어 있다.

iOS의 테이블 뷰에는 세로로 스크롤 하는 콘텐츠 행이 하나의 열에 표시된다.
따라서 추가로 개발자가 스크롤 뷰를 사용하지 않아도 알아서 스크롤이 가능한 테이블 뷰로 구현된다.

추가적인 내용은 개발을 진행하면서 막히면 알아보는 것으로 하자...

 


 🧑‍💻 변경된 코드

1. 테이블 뷰에서 셀들의 구분선 제거

차이점이 느껴진다면 당신은 디자인적으로 예민할 것이다... 아마도...

 

테이블 뷰에서 셀들의 구분선을 삭제하는 방법은 엄청 간단하다.

tableView.separatorStyle = .none // 셀들의 구분선 제거

 

해당 코드를 작성하면 셀들의 구분선을 제거할 수 있다.

 

2. Button과 Label이 포함된 커스텀 테이블 뷰 만들기

제품의 품목이 담겨있는 커스텀 테이블 뷰를 만들고, 테이블 뷰 안에 플러스, 마이너스 버튼을 클릭하면

수량과 각 제품별로 총 금액이 표시되고, 테이블 뷰 아래에 있는 Label에는 전체 장바구니에 담은 개수와 총 금액이 표시되도록 구현했다.

 

버튼 이벤트를 테이블 뷰 안에서 처리한 뒤 밖으로 데이터를 넘겨주고 총 개수와 총 금액 라벨로 넘겨주는 부분이 조금 어려웠다.

 

//
//  OrderTable.swift
//  Oduk
//
//  Created by 허성필 on 4/8/25.
//

import Foundation
import UIKit
import SnapKit

class OrderTable: UIView {
    
    private let countLabel = UILabel()
    private let priceLabel = UILabel()
    var dataSource = [CustomCellModel]()
    
    private lazy var tableView: UITableView = {
        let tableView = UITableView()
        
        self.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.height.equalTo(99)
        }
        
        return tableView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupTableView()
        loadData()
        makeLabel()
    }
    
    // 스토리보드를 사용할 때 코드랑 연결해주는 부분?
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupTableView() {
        tableView.register(CustomCell.self, forCellReuseIdentifier: CustomCell.identifier)
        tableView.delegate = self
        tableView.dataSource = self
        tableView.separatorStyle = .none // 셀들의 구분선 제거
    }
    
    private func loadData() {
        dataSource.append(.init(leftLabel: "고독한 영라기", rightLabel: "500"))
        dataSource.append(.init(leftLabel: "수줍은 원시기", rightLabel: "500"))
        dataSource.append(.init(leftLabel: "배고픈 다서이", rightLabel: "500"))
        dataSource.append(.init(leftLabel: "정열의 성피리", rightLabel: "500"))
        tableView.reloadData()
        didUpdateCounts()
    }
    
    func makeLabel() {
        
        countLabel.font = UIFont(name: "GmarketSansMedium", size: 14)
        priceLabel.font = UIFont(name: "GmarketSansMedium", size: 14)
        
        [countLabel, priceLabel].forEach {
            self.addSubview($0)
        }
        
        countLabel.snp.makeConstraints { make in
            make.top.equalTo(self.snp.bottom).offset(5)
            make.leading.equalTo(170)
        }
        
        priceLabel.snp.makeConstraints { make in
            make.top.equalTo(self.snp.bottom).offset(5)
            make.leading.equalTo(countLabel.snp.trailing).offset(46)
        }
    }
}

extension OrderTable: UITableViewDelegate, UITableViewDataSource, CustomCellDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as? CustomCell ?? CustomCell()
        cell.delegate = self
        cell.indexPath = indexPath
        cell.bind(model: dataSource[indexPath.row])
        cell.backgroundColor = UIColor(
            red: 248/255.0,
            green: 248/255.0,
            blue: 248/255.0,
            alpha: 1.0
        )
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 33 // 테이블 뷰에서 보이고 싶은 셀의 수 = 3  테이블 뷰의 높이 = 99
        // 따라서 높이 ÷ 보이고 싶은 셀의 수  99 ÷ 3 = 33
    }
    
    func didChangeCount(to newCount: Int, at indexPath: IndexPath) {
        dataSource[indexPath.row].count = newCount
        didUpdateCounts() // 다시 계산해서 업데이트
    }
    
    func didUpdateCounts() {
        var totalCount = 0
        var totalPrice = 0
        
        for item in dataSource {
            let count = item.count
            let price = Int(item.rightLabel) ?? 0
            totalCount += count
            totalPrice += count * price
        }
        
        countLabel.text = "총 \(totalCount)개"
        priceLabel.text = "₩ \(totalPrice)원"
    }
}

 

//
//  CustomCell.swift
//  Oduk
//
//  Created by 허성필 on 4/9/25.
//

import UIKit
import SnapKit

protocol CustomCellDelegate: AnyObject {
    func didChangeCount(to newCount: Int, at indexPath: IndexPath)
}

class CustomCell: UITableViewCell {
    
    weak var delegate: CustomCellDelegate?
    
    static let identifier = "CustomCell"
    private var count: Int = 1
    private var model: CustomCellModel?
    var indexPath: IndexPath? 
    
    let leftLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .left
        label.font = UIFont(name: "GmarketSansMedium", size: 13)
        return label
    }()
    
    lazy var minusButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "minus.circle.fill"), for: .normal)
        button.tintColor = UIColor(red: 253/255, green: 141/255, blue: 194/255, alpha: 1) // #FD8DC2
        button.addTarget(self, action: #selector(minusButtonTapped), for: .touchUpInside) // self 가 아닌 CustomCell.self인 이유
        return button
    }()
    
    let middleLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .center
        label.font = UIFont(name: "GmarketSansMedium", size: 14)
        return label
    }()
    
    lazy var plusButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "plus.circle.fill"), for: .normal)
        button.tintColor = UIColor(red: 253/255, green: 141/255, blue: 194/255, alpha: 1) // #FD8DC2
        button.addTarget(self, action: #selector(plusButtonTapped), for: .touchUpInside)
        return button
    }()
    
    let rightLabel: UILabel = {
        let label = UILabel()
        label.textAlignment = .right
        label.font = UIFont(name: "GmarketSansMedium", size: 13)
        return label
    }()
    
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        
        [leftLabel, minusButton, middleLabel, plusButton, rightLabel].forEach {
            contentView.addSubview($0)
        }
        
        setupConstraints()
        //        setupActions()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been impl")
    }
    
    private func setupConstraints() {
        leftLabel.snp.makeConstraints { make in
            make.leading.equalToSuperview().offset(19)
            make.centerY.equalToSuperview()
            make.width.equalTo(103)
            make.height.equalTo(21)
        }
        minusButton.snp.makeConstraints { make in
            make.leading.equalTo(leftLabel.snp.trailing).offset(31)
            make.centerY.equalToSuperview()
            make.height.equalTo(20)
        }
        
        middleLabel.snp.makeConstraints { make in
            make.leading.equalTo(minusButton.snp.trailing).offset(8)
            make.centerY.equalToSuperview()
            make.width.equalTo(20)
            make.height.equalTo(21)
        }
        
        plusButton.snp.makeConstraints { make in
            make.leading.equalTo(middleLabel.snp.trailing).offset(8)
            make.centerY.equalToSuperview()
            make.width.height.equalTo(20)
        }
        
        rightLabel.snp.makeConstraints { make in
            make.leading.equalTo(plusButton.snp.trailing).offset(27)
            make.centerY.equalToSuperview()
            make.width.equalTo(72)
            make.height.equalTo(21)
        }
    }

    
    // MARK: - Button Actions
    
    @objc private func minusButtonTapped() {
        print("마이너스 버튼 클릭")
        self.count -= 1
        model?.count = self.count
        middleLabel.text = String(count)
        if let rightValue = Int(model?.rightLabel ?? "0") {
            rightLabel.text = "\(rightValue * count)원"
        }
        if let index = indexPath {
            delegate?.didChangeCount(to: count, at: index)
        }
    }
    
    @objc private func plusButtonTapped() {
        print("플러스 버튼 클릭")
        self.count += 1
        model?.count = self.count
        middleLabel.text = String(count)
        if let rightValue = Int(model?.rightLabel ?? "0") {
            rightLabel.text = "\(rightValue * count)원"
        }
        if let index = indexPath {
            delegate?.didChangeCount(to: count, at: index)
        }
    }
    
}

// MARK: - Utils

extension CustomCell {
    public func bind(model: CustomCellModel) {
        self.model = model
        self.count = model.count
        leftLabel.text = model.leftLabel
        middleLabel.text = String(count)
        rightLabel.text = "\(Int(model.rightLabel) ?? 0 * model.count)원"
    }
}

 

//
//  CustomCellModel.swift
//  Oduk
//
//  Created by 허성필 on 4/9/25.
//

import UIKit

struct CustomCellModel {
    let leftLabel: String
    let rightLabel: String
    var count: Int = 1
}

 

OrderTabel 파일에서 테이블 뷰를 생성하고,

CustomCell 파일에서는 테이블 뷰 안에 들어갈 셀을 커스텀 하는 작업을 한다.

CustomCellModel에서는 struct를 이용하여 CustomCellModel을 정해두었다.

 

현재까지 Figma에서 작업한 제약 조건을 지정해줬고, 버튼 클릭 이벤트와 라벨 표시 작업까지 완료하였다.

 📝 내일 해야할 것

  1. 결제 / 취소 버튼 UI 합치기
  2. 데이터 로직 구현하기
  3. 발표 자료 준비하기