스파르타 코딩 클럽 - iOS 스타터 6기/최종 팀 프로젝트 - 잇츠오케이

62. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #4

seongpil Heo 2025. 6. 11. 23:41

  🎯  Trouble Shooting

[ 1. 테이블 뷰 구분선 가출 ]

 

UIView로 구분선을 만들어서 TableView Cell 맨 아래 부분에 표시되도록 구현했는데

셀을 위아래로 스크롤하다가 돌아오면 위 쪽 Cell에 구분선이 사라지는 문제가 발생했다.

 

이전에도 프로젝트를 진행할 때 팀원분 중 한 분에게 이런 문제가 발생했던 기억이 있는데

셀 재사용 처리의 문제인 줄 알고

// 셀 재사용 시 상태 초기화
override func prepareForReuse() {
    super.prepareForReuse()
}

 

이 메서드를 작성했는데도 구분선이 계속 사라지는 문제가 있었다.

 

구글링도 하고 Perplexity 한테도 물어보면서 문제를 해결했다.

문제는 AutoLayout이었다.

separatorView.snp.makeConstraints { make in
    // make.top.equalTo(openNowLabel.snp.bottom).offset(26)
    make.bottom.equalToSuperview() // 해당 코드로 구분선 사라짐 해결
    make.leading.trailing.equalToSuperview()
    make.height.equalTo(2)
}

 

나는 구분선의 위치를 top을 통해서 잡고 있었는데

구분선의 위치를 bottom.equalToSuperView()로 수정하면서 문제를 해결했다.


  👨🏻‍💻  오늘의 작업 

[ 1. 매장  카운드 라벨과 정렬 버튼 AutoLayout을 비율로 변경 ]

// 매장 카운드
storeCountLabel.snp.makeConstraints { make in
    make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(316)
    make.leading.equalToSuperview().offset(20)
}

 

처음 AutoLayout을 잡았을 때 아이폰 13 mini를 기준으로 safeAreaLayoutGuide의 top으로부터 316만큼 떨어지게 AutoLayout을 잡았다. 그러나 이것의 문제는 다른 기기에서 (화면이 더 크거나 작은 경우) 내가 원하는 sortButton의 리스트들이 아래로 보이게 하는 동작이 안될 수도 있다는 문제가 있다.

 

따라서 이것을 각 기기의 safeAreaHeight의 비율로 Autolayout을 잡으면 어떤 기기에서도 비슷한 위치에 있기 때문에

내가 원하는 동작을 구현할 수 있다.

 

storeCountLabel.snp.makeConstraints { make in
    let safeAreaHeight = self.view.safeAreaLayoutGuide.layoutFrame.height
    // iPhone 13 mini : safeAreaHeight: 812.0
    // 원래 원했던 offset인 316을 812로 나눠서 비율 찾기 → 0.3891625... 
    // 비율을 0.389로 설정

    make.top.equalTo(self.view.safeAreaLayoutGuide.snp.top).offset(safeAreaHeight * 0.389)
    make.leading.equalToSuperview().offset(20)
}

 

각 기기마다 Autolayout을 설정할 때 기기 별 safeAreaHeight를 구하고 원하던 위치의 비율인 0.389를 곱해서

storeCountLabel의 top을 설정한다.

 

그러면 모든 기기에서 내가 원했던 위치에 라벨과 정렬버튼이 위치하게 되며,

원했던 정렬 버튼의 리스트들이 아래로 표시가 구현된다

 

 

[ 2. TableView + RxDataSource 리펙토링 ]

2.1 테이블 뷰 셀 등록

tableView.register(DetailTableViewCell.self, 
           	 forCellReuseIdentifier: DetailTableViewCell.identifier)

 

2.2 테이블 뷰 바인딩

private func bindTableView() {
    let dataSource = RxTableViewSectionedAnimatedDataSource<StoreSection>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: DetailTableViewCell.identifier, for: indexPath) as! DetailTableViewCell
            cell.configureView(with: item)
            return cell
        }
    )

    storeInfos
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)

	// 애니메이션 확인을 위한 테이블 뷰 클릭시 이벤트
    tableView.rx.itemSelected // 웹뷰 모달이 될 예정 Action
        .withUnretained(self)
        .bind { vc, indexPath in
            var test = vc.storeInfos.value
            let random = Int.random(in: 1...1000)
            test[0].items.insert(.init(displayName: "애니메이션 테스트 \(random)", formattedAddress: "ㅇㅇㅇㅇ", latitude: 33.44, longitude: 44.55, rating: 4.5, googleMapsURI: "ㅇㅇㅇㅇㅇ", userRatingCount: random, photosNames: ["ㄴㅇㄹㄴㅇ"]), at: indexPath.row)
            vc.storeInfos.accept(test)
        }.disposed(by: disposeBag)
}

 

2.3 데이터소스 작업

import Foundation
import RxDataSources

struct StoreInfo: Equatable {
    let displayName: String
    let formattedAddress: String
    let latitude: Double
    let longitude: Double
    let rating: Double
    let googleMapsURI: String
    let userRatingCount: Int
    let photosNames: [String]
}

extension StoreInfo: IdentifiableType {
    var identity: String { return displayName } // 고유 식별자로 가게명 사용
}

struct StoreSection {
    var items: [StoreInfo]
}

extension StoreSection: AnimatableSectionModelType {
    typealias Item = StoreInfo
    typealias Identity = String
    
    var identity: String { "default" }
    init(original: StoreSection, items: [StoreInfo]) {
        self = original
        self.items = items
    }
}

 

2.4 DetailTableViewCell에서 configureView() 작업

func configureView(with storeInfo: StoreInfo) {
    storeNameLabel.text = storeInfo.displayName
    rateLabel.text = "\(storeInfo.rating)"
    userRateCountLabel.text = "(\(storeInfo.userRatingCount))"
    addressLabel.text = storeInfo.formattedAddress
//        openNowLabel.text = 
}