확신이 없는 코드를 치면서 좌절했던 시간을 지나, 이제는 왜 이 코드가 여기 있어야 하고 왜 저 코드가 저기 있어야하는지 조금씩 감을 잡아가는 단계가 된 것 같다. 패턴 공부의 중요성을 절실하게 깨달은 지난 며칠이었다. MVC
와 Delegate
패턴을 공부하면서, 객체의 역할과 책임을 잘 분리하고 서로 간 의존성을 최대한 낮추는 첫 번째 방법을 터득했다고 봐도 될 것 같다. 공부한 내용을 적용하며 코드를 이리저리 옮기는 리팩토링을 진행하고자 한다.
기존 rootview controller 코드
class KioskViewController: UIViewController {
private let titleView = TitleView()
private let categoryView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.cornerRadius = 20
return view
}()
private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
private lazy var menuView = MenuView()
private lazy var cartView = CartView(mananger: manager)
private let sumView = SumView()
private let footerView = FooterView()
// ... //
}
귀여운 로고와 꼬끼오스크
타이틀은 위의 코드에서 titleView
에 속하고, 허니시리즈, 레드시리즈, 교촌시리즈는 CategoryButton
이라는 커스텀 버튼으로 구현하였다. buttons
가 categoryView
의 subview
인데, categoryView
를 컨트롤러에서 정의하지 않고 따로 클래스를 분리하여 버튼들과 함께 옮겨보려 한다.
class CategoryView: UIView {
private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
override init(frame: CGRect) {
super.init(frame: .zero)
addSubViews(buttons)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
backgroundColor = .clear
layer.cornerRadius = 20
// ... //
}
}
class KioskViewController: UIViewController {
private let titleView = TitleView()
private let categoryView = CategoryView()
private lazy var menuView = MenuView()
private lazy var cartView = CartView(mananger: manager)
private let sumView = SumView()
private let footerView = FooterView()
// ... //
}
카테고리 뷰를 따로 클래스로 분리하고 버튼들을 subview
로 옮겨넣었더니 Controller
에 view
선언 코드가 훨씬 간결해졌다.
CategoryView
→Controller
→Model
우선 Controller
가 갖는 버튼 관련 코드들을 CategoryView
로 옮긴다.
before
class KioskViewController: UIViewController {
private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
private func setupCategoryView() {
buttons.forEach {
categoryView.addSubview($0)
$0.snp.makeConstraints {
$0.width.equalToSuperview().dividedBy(3)
$0.height.equalToSuperview()
$0.centerY.equalToSuperview()
}
$0.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
}
buttons[0].snp.makeConstraints {
$0.leading.equalToSuperview()
}
buttons[1].snp.makeConstraints {
$0.centerX.equalToSuperview()
}
buttons[2].snp.makeConstraints {
$0.trailing.equalToSuperview()
}
// 허니시리즈 버튼은 눌린 채로 시작
setButtonSelected(for: buttons[0])
}
@objc func categoryTapped(_ sender: CategoryButton) {
series = sender.series
menuView.collectionView.reloadData()
setButtonSelected(for: sender)
menuView.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
menuView.pageControl.numberOfPages = series.chickens.count / 4 + 1
menuView.pageControl.currentPage = 0
}
private func setButtonSelected(for button: UIButton) {
buttons.forEach {
$0.backgroundColor = .clear
$0.isSelected = false
}
button.backgroundColor = .appPrimary
button.isSelected = true
}
}
after
class KioskViewController: CategorySelectDelegate {
override func viewDidLoad() {
super.viewDidLoad()
categoryView.delegate = self
}
func updateCategory(_ series: ChickenSeries) {
// model update
self.series = series
menuView.collectionView.reloadData()
menuView.collectionView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
menuView.pageControl.numberOfPages = series.chickens.count / 4 + 1
menuView.pageControl.currentPage = 0
}
}
Controller
의 코드 양이 확연히 줄은 게 보인다. CategoryView
에 Delegation
을 적용한 내용도 가져와보겠다.
// 프로토콜을 선언하고 눌린 버튼이 무슨 시리즈의 버튼인지 컨트롤러에 전송하기 위한 함수를 선언한다
protocol CategorySelectDelegate: AnyObject {
func updateCategory(_ series: ChickenSeries)
}
class CategoryView: UIView {
// 순환 참조 방지를 위해 weak var로 선언
weak var delegate: CategorySelectDelegate?
private let buttons = [CategoryButton(.honey), CategoryButton(.red), CategoryButton(.kyochon)]
private func setup() {
// .. auto layout 코드 생략 .. //
buttons.forEach {
$0.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
}
// 허니시리즈 버튼은 눌린 채로 시작
setButtonSelected(for: buttons[0])
}
private func setButtonSelected(for button: UIButton) {
buttons.forEach {
$0.backgroundColor = .clear
$0.isSelected = false
}
button.backgroundColor = .appPrimary
button.isSelected = true
}
@objc func categoryTapped(_ sender: CategoryButton) {
// 눌린 버튼 ui 처리
setButtonSelected(for: sender)
// delegate를 통해 눌린 버튼 정보 전송
delegate?.updateCategory(sender.series)
}
}
여기까지 적용하고 나니, Controller
의 코드가 50줄
줄었다.
Model
→Controller
→CollectionView
위에서는 버튼 → 컨트롤러
방향으로 눌린 카테고리 정보가 전달됐다면, 이번에는 컨트롤러 → 컬렉션 뷰
방향으로 정보가 전달되어 컬렉션 뷰가 카테고리에 맞는 치킨들을 뷰에 그리도록 구현해보려 한다.
우선, Model
과의 직접적인 결합을 피하기 위해 Controller
에 CollectionView
의 DataSource
와 Delegate
코드를 다 보냈었는데, 그 코드를 subview
에 보낸 뒤 Delegate
를 정의하여 객체 간 통신을 구현해보도록 하겠다.
왼쪽에 블록 처리 된 친구들을 모두 오른쪽
MenuView
로 옮긴다.
컬렉션 뷰 DataSource : 화면에 그려야하는 치킨의 series 정보가 필요
→ Delegate에 ChickenSeries 타입을 리턴하는 함수를 정의하여 구현
protocol CategorySelectDelegate: AnyObject {
func updateCategory(_ series: ChickenSeries)
// 함수 추가
func getSeriesInfo() -> ChickenSeries
}
extension KioskViewController: CategorySelectDelegate {
func getSeriesInfo() -> ChickenSeries {
// model 데이터 전송
return series
}
}
extension MenuView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// delegate를 통해 받은 데이터를 collection view에 반영
guard let series = delegate?.getSeriesInfo() else { return 0 }
return series.chickens.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let series = delegate?.getSeriesInfo(),
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ChickenCell.identifier, for: indexPath) as? ChickenCell
else { return UICollectionViewCell() }
// delegate를 통해 받은 데이터를 collection view에 반영
cell.bind(series.chickens[indexPath.item])
return cell
}
}
컬렉션 뷰 Delegate : 사용자가 탭한 치킨 cell의 index 정보를 Controller에 전송 필요
→ Delegate에 파라미터로 index 값을 전달하는 함수 정의
// CategoryView.swift 파일 안에 있던 프로토콜 코드를 CategorySelectDelegate.swift로 분리했다.
protocol CategorySelectDelegate: AnyObject {
func updateCategory(_ series: ChickenSeries)
func getSeriesInfo() -> ChickenSeries
// 함수 정의
func didTapChickenCell(of index: Int)
}
extension MenuView: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// 탭 된 셀의 index를 delegate를 통해 전송
delegate?.didTapChickenCell(of: indexPath.item)
}
}
// controller에서 model과 소통하여 로직 처리
extension KioskViewController: CategorySelectDelegate {
func didTapChickenCell(of index: Int) {
let chicken = series.chickens[index]
if let index = manager.orders.firstIndex(where: { $0.menu == chicken }) {
manager.orders[index].count += 1
} else {
let newOrder = Order(menu: chicken)
manager.orders.append(newOrder)
}
}
}
여기까지 적용하고 나니, 리팩토링을 시작하기 전에 271
줄이었던 KioskViewController
의 코드가 199
줄로 줄었다. 리팩토링은 여기서 끝이 아니다. 지금은 카테고리와 메뉴 영역만 고친 것이다. 다른 영역에 적용할 것들이 많이 남았다. 앞으로 진행하는 모든 리팩토링을 글로 남기게 될지는 모르겠지만, 하여튼 이 프로젝트에서 손을 떼진 않으려 한다.
키오스크 .. 아직 놓아주지 않았군요.. 대단해요 분명 더 성장하실고에요