일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 러닝기록앱
- addannotation
- CoreLocation
- 서체관리자
- Required Reason API
- Startign Assignments
- Protocol
- swift
- dispatchsource
- WeatherManager
- RunningTimer
- MKMapItem
- 영문 개인정보처리방침
- 클로저의 캡슐화
- SwiftUI Boolean 값
- MKMapViewDelegate
- xcode로 날씨앱 만들기
- weatherKit
- Timer
- AnyObject
- font book
- UICollectionViewFlowLayout
- CLLocationManagerDelegate
- App Store Connect
- weak var
- Xcode
- 한국어 개인정보처리방침
- 단일 책임원칙
- 러닝타이머
- UIAlertAction
- Today
- Total
VesselWheel
CollectionView를 활용한 키오스크 메뉴화면 만들기(feat. 코드 베이스) 본문
들어가기 앞서,
평소 사용하던 키오스크 앱이나, 카페, 페스트 푸드 어플리케이션을 보면 내가 원하는 메뉴를 선택하기 위해 메뉴 그림, 가격을 나열해 둔 것을 볼 수 있다.
스타벅스 같은 경우, 위에서 아래로 메뉴가 나열되어 있어, 데이블뷰로 구현되어 있다.
하지만, 블로그에서 다루지는 않겠지만 다른 메뉴를 선택하고 주문하는 앱들을 보면 MD 상품이나 회원가입이 우선되어 원하는 메뉴를 찾기 위해서 여러번 클릭해야하는 경험을 해왔다.
이점에서 불편함을 경험하였고, 부분적이지만 collectionView로 해당 상품들을 직관적으로 볼 수 있도록 구현해 보았다.
MenuView 클래스에 구현된 CollectionView
//
// MenuView.swift
// SPABUCKS-Kiosk-iOS
//
// Created by JasonYang on 2023/12/29.
//
import UIKit
class MenuView: UIView {
// MARK: - Properties
private let orderView = OrderListView()
var dataSource: [SpabucksMenuItem] = []
var drinkItems: [SpabucksMenuItem] = [
SpabucksMenuItem(id: 0, name: "Caffè Americano", imageName: "americano", price: 5700),
SpabucksMenuItem(id: 1, name: "Caramel Macchiato", imageName: "caramel_macchiato", price: 5900),
SpabucksMenuItem(id: 2, name: "Flat White", imageName: "flat_white", price: 6000),
SpabucksMenuItem(id: 3, name: "Caffe Latte", imageName: "Caffe Latte", price: 8000)]
var foodItems: [SpabucksMenuItem] = [
SpabucksMenuItem(id: 0, name: "Croissant", imageName: "croissant", price: 5000),
SpabucksMenuItem(id: 1, name: "Sandwich", imageName: "sandwich", price: 6000),
SpabucksMenuItem(id: 2, name: "Salad", imageName: "salad", price: 6000),
SpabucksMenuItem(id: 3, name: "Scorn", imageName: "scorn", price: 5500)]
var merchandiserItems: [SpabucksMenuItem] = [
SpabucksMenuItem(id: 0, name: "Tumbler", imageName: "Tumbler", price: 5500),
SpabucksMenuItem(id: 1, name: "Pen", imageName: "Pen", price: 1500),
SpabucksMenuItem(id: 2, name: "Notebook", imageName: "Notebook", price: 12500),
SpabucksMenuItem(id: 3, name: "Tent", imageName: "Tent", price: 25000)]
// MARK: - Life Cycle
private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
override init(frame: CGRect) {
super.init(frame: frame)
setUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// test
// MARK: - Extensions
extension MenuView {
func showBeverageView() {
dataSource = drinkItems
collectionView.reloadData()
}
func showFoodMenuView() {
dataSource = foodItems
collectionView.reloadData()
}
func showMdMenuView() {
dataSource = merchandiserItems
collectionView.reloadData()
}
}
// MARK: - UICollectionViewDelegate
extension MenuView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Selected: \(dataSource[indexPath.row])")
let selectedData = dataSource[indexPath.row] // 선택된 셀의 데이터
self.orderView.getOrderItem(selectedData)
collectionView.deselectItem(at: indexPath, animated: true)
}
}
// MARK: - UICollectionViewDataSource
extension MenuView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MenuCell.collectionViewCellIdentifier, for: indexPath) as? MenuCell else {
return UICollectionViewCell()
}
cell.imageView.image = UIImage(named: dataSource[indexPath.row].imageName)
cell.nameLabel.text = dataSource[indexPath.row].name
cell.priceLabel.text = String("\(Int(dataSource[indexPath.row].price)) 원")
return cell
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension MenuView: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 165, height: 70)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 30
}
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
// return 30
// }
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: 400, height: 15)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: 400, height: 15)
}
}
extension MenuView {
private func setUI() {
heightAnchor.constraint(equalToConstant: 455).isActive = true
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: MenuCell.collectionViewCellIdentifier)
collectionView.backgroundColor = .white
addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: self.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15),
collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
collectionView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10)
])
}
}
MenuView에 생성된 프로퍼티 정의
//
// SpabucksMenuItem.swift
// SPABUCKS-Kiosk-iOS
//
// Created by Jason Yang on 12/27/23.
//
import Foundation
class SpabucksMenuItem : Eatable {
var id: Int
var name: String
var imageName: String
var price: Double
init(id: Int, name: String, imageName: String, price: Double) {
self.id = id
self.name = name
self.imageName = imageName
self.price = price
}
}
생선된 프로퍼티를 규약하는 프로토콜 Eatable
//
// Eatable.swift
// SPABUCKS-Kiosk-iOS
//
// Created by Jason Yang on 12/28/23.
//
import Foundation
protocol Eatable {
var id: Int { get set }
var name: String { get set }
var imageName: String { get set }
var price: Double { get set }
func displayInfo()
}
extension Eatable {
func displayInfo() {
print("\(id) | W \(name) | \(price)")
}
}
생성된 collectionView에 Cell의 layout을 정의하는 extension MenuCell
//
// MenuCell.swift
// SPABUCKS-Kiosk-iOS
//
// Created by Jason Yang on 12/28/23.
//
import UIKit
class MenuCell: UICollectionViewCell {
// MARK: - Properties
static let collectionViewCellIdentifier = "CollectionViewCellIdentifier"
// MARK: - UI Properties
let imageView = UIImageView()
let nameLabel = UILabel()
let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
viewCell()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func viewCell() {
backgroundColor = .white
imageView.contentMode = .scaleAspectFit
contentView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), // 위 여백
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
imageView.widthAnchor.constraint(equalToConstant: 50),
imageView.heightAnchor.constraint(equalToConstant: 50)
])
nameLabel.textAlignment = .left
nameLabel.numberOfLines = 2
nameLabel.lineBreakMode = .byWordWrapping
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
nameLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 5),
nameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 5),
])
priceLabel.textAlignment = .left
contentView.addSubview(priceLabel)
priceLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
priceLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 5),
priceLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5)
])
}
}
상기 코드는 코드베이스로 storyboard를 사용하지 않고 delegate pattern과 ViewController와의 상호작용을 통해 구현하였다.
- 스토리보드로 구현할 경우, 협업의 경우 github로 업무를 하기 때문에, 비대면의 특성상 동시에 commit-push하고 merge할 때에 코드 충돌이 발생하는 경우가 많다.
- 스토리보드로 구현할 경우에도 프로퍼티 별로 swift 파일을 연결하여서 작성하고, delegate 패턴을 활용하면 충돌은 상대적으로 막을 수 있다.
다른 팀원들과의 협업 간 폴더링, 깃 컨밴션, 코드 컨벤션에 대해 깊히있게 배울 수 있었다.
코드베이스를 위한 xcode 폴더링 화면
깃 컨밴션
Git Convention
1. Issue를 생성한다. // 작업의 단위, 번호 부여
2. Issue의 Feature Branch를 생성한다. // ex. feat/#이슈번호-기능 설명-닉네임
3. ~작업~ // Add - Commit - Push - Pull Request 의 과정
4. Pull Request가 작성되면 작성자 이외의 다른 팀원이 Code Review를 한다.
5. Code Review가 완료되고,
2명 이상 Approve 하면 Pull Request 작성자가 develop Branch로 merge 한다.
// Conflicts 방지
6. 팀원들에게 merge 사실을 알린다.
7. 브랜치를 삭제한다.
8. 다른 팀원들은 merge된 작업물을 pull하고 다시 각자 맡은 작업을 이어나간다.
- 이슈 생성
- 브랜치 파기
- 작업…
- Pull Request
- 브랜치 삭제
📌 브랜치 단위
- 브랜치 단위 = 이슈 단위 = PR단위
📌 브랜치명
feature/#이슈번호-간단기능설명-닉네임
브랜치 앞에 써주세여~
- feature
- network
- fix
- 뷰 단위로 크게 판다 (ui / 기능 구현 / 네트워크)
- ex) feat/#1-MainViewUI-joon
- feature/chore/fix
- feature/#7-MainViewUI-joon
- network/#4 - MypageAPI-joon
PR
Issue & Pull Request
- Issue 이름 : [종류] 작업명 ex) [Feat] Main View UI 구현
- template에 맞춰서 작성
- 담당자, 리뷰어지정, 라벨 추가, 프로젝트 추가 꼭 하기
- Pull Request : [종류] #이슈번호 작업명
- ex) [Feat] #13 Main View UI 구현
Commit Convention
- [Feat] : 새로운 기능 구현
- [Fix] : 버그, 오류 해결
- [Chore] : 코드 수정, 내부 파일 수정, 애매한 것들이나 잡일은 이걸로!
- [Add] : 라이브러리 추가, 에셋 추가
- [Del] : 쓸모없는 코드 삭제 (ex. 주석 삭제, 구조 변경 등)
- [Docs] : README나 WIKI 등의 문서 개정
- [Refactor] : 전면 수정이 있을 때 사용합니다
- [Setting] : 프로젝트 설정관련이 있을 때 사용합니다.
[Merge] - Pull Developfea1
코드 컨밴션
- 변수명 = 동사+명사
- 중괄호 한 칸 띄어쓰기!!!!!!!!
- 무조건~ 개행 시 공백 한 칸
- 변수명 축약어X 그냥 길~게 쓰세요
- : 이거 붙여쓰기 (삼항 연산자 제외)
- let number: Int = 1 (O) let number : Int = 1 (X 백준님이 사망하심)
- MARK 주석
- 사용 시 개행 공백 한 칸
class ViewController: UIViewController { // MARK: - Properties // MARK: - UI Properties ''' // MARK: - Life Cycle (init - Life Cycle - deinit ''' } extension ViewController { // MARK: - Layout // MARK: - @objc // MARK: - Private Methods } // MARK: - UITableView Delegate . . .
참고자료
- GitHub Pull Request를 통해 코드 리뷰하는 방법
https://devlog-wjdrbs96.tistory.com/231
- Life CyCle
https://zeddios.tistory.com/43
- Snipped
https://seons-dev.tistory.com/entry/iOS-Xcode-Snipped-사용법-자주쓰는-코드를-저장하자
추가로 알아야 할 사항
- StackView, delegate pattern, UIButton, 데이터소스를 별도 클래스로 구현하기
'Xcode Study' 카테고리의 다른 글
UICollectionView의 UICollectionViewFlowLayout (0) | 2024.01.02 |
---|---|
OrderListView와 MenuView 간의 데이터 연결(feat. 델리게이트, 약한참조) (0) | 2024.01.02 |
[Trouble Shooting] 뷰 클래스 간 데이터 연결하기 (0) | 2023.12.29 |
Starting Assignments 작성하기 (feat. 키오스크 앱 프로젝트) (0) | 2023.12.26 |
CollectionView (1) | 2023.12.14 |