영화 예매 앱 만들기 - 4

maxminseok·2024년 12월 19일
1

상영 중인 영화와 상영 예정인 영화 두 가지로 나눠 검색 및 탐색을 할 수 있도록 segmentedControl 기능 추가하였다.

SearchView에 segmentedControl 추가

protocol textFieldDelegate: AnyObject {
    func searchingMovie(_ input: String)
    func pressReturnKey()
    func didChangeSegment(index: Int) // 추가
}
  • 기존에 있던 textFieldDelegate에 didChangeSegment 매서드 추가
  • 뷰에 정의한 segmentedControl을 컨트롤러에서 처리하도록 함

기존에 SearchView에서 텍스트 필드 이벤트 처리를 위해 정의해 놓은 프로토콜에 새로운 메서드를 추가하였다. 프로토콜 이름이 textFieldDelegate라 조금 맞지 않지만, 이름은 알맞게 변경하면 될 것이다.

매개변수인 index값은 segmentedControl의 메뉴가 상영중, 상영예정 두가지기 때문에 0 아니면 1이 넘어가게 될 것이다.

   // SearchView에 추가
   
    private let height = UIScreen.main.bounds.height // UI 제약사항을 동적으로 주기 위한 뷰높이 계산
    
    weak var delegate: textFieldDelegate?
   
    // segmented Control
    private let segmentedControl: UISegmentedControl = {
        let control = UISegmentedControl(items: ["상영중", "상영예정"])
        control.selectedSegmentIndex = 0
        control.backgroundColor = .systemGray6
        control.selectedSegmentTintColor = .white
        control.setTitleTextAttributes([.font: UIFont.boldSystemFont(ofSize: 16)], for: .selected)
        control.setTitleTextAttributes([.font: UIFont.systemFont(ofSize: 15)], for: .normal)
        control.addTarget(self, action: #selector(segmentChanged), for: .valueChanged)
        return control
    }()
    
		// MARK: - UI 셋업
    
    private func setupUI() {
        backgroundColor = .white
        [
            searchTextField,
            movieCollectionView,
            segmentedControl
        ].forEach { addSubview($0) }
        
        searchTextField.snp.makeConstraints {
            $0.top.equalToSuperview().offset(height / 6)
            $0.leading.trailing.equalToSuperview().inset(16)
            $0.height.equalTo(44)
        }
        
        segmentedControl.snp.makeConstraints {
            $0.top.equalTo(searchTextField.snp.bottom).offset(16)
            $0.leading.trailing.equalToSuperview().inset(16)
            $0.height.equalTo(40)
        }
        
    // segmentedControl 메뉴 선택 처리 메서드
    @objc func segmentChanged() {
        delegate?.didChangeSegment(index: segmentedControl.selectedSegmentIndex)
    }
  • segmentedControl 선언
  • .setTitleTextAttributes를 사용해 클릭 되었을 때와 기본 상태의 폰트 속성을 정함
  • delegate로 Controller에서 실제 처리를 하도록 위임

segmentedControl을 클릭할 때 해당 이벤트를 컨트롤러가 처리해야 하므로 delegate 패턴을 통해 컨트롤러로 이벤트를 전달하였다.


SearchViewController에 프로토콜 채택

class SearchViewController: UIViewController {

    // 세그먼트 선택 메뉴 저장할 변수, 초기값은 .nowPlaying
    private var selectedCategory = NetworkManager.URLEndpointSet.nowPlaying
    
    // 세그먼트에 따른 파라미터, 기본값은 common
    private var selectedParameter = NetworkManager.URLParameterSet.common
    
    // 검색 키워드 저장할 변수
    private var searchKeyword: String = ""
    
    // 기존 코드
}

extension SearchViewController: textFieldDelegate {
    
    // 기존 코드
    
    // 추가된 코드
    // Segmented Control 선택 메서드 
    func didChangeSegment(index: Int) {
        selectedCategory = (index == 0) ? .nowPlaying : .upcoming
        selectedParameter = (index == 0) ? NetworkManager.URLParameterSet.common : NetworkManager.URLParameterSet.secondPage
        
        fetchMovieData(selectedCategory, parameter: selectedParameter)
    }
}
extension SearchViewController {
    
    private func fetchMovieData(_ endPoint: NetworkManager.URLEndpointSet, parameter: URLParameters) {
        Task { [weak self] in
            do {
                // 1. MovieDataManager를 호출해 서버에서 영화 데이터 가져오기
                let movieData: MovieData = try await NetworkManager.shared.fetchData(
                    endpoint: endPoint,  // 'nowPlaying' 엔드포인트를 사용하여 현재 상영 중인 영화 목록을 요청
                    parameters: parameter
                )
                
                var tempMovies = [MovieDataSource]()    // 임시로 데이터를 저장할 배열
                
                // 2. 가져온 영화 목록을 순회하며 각 영화에 대한 이미지 데이터를 요청
                for movie in movieData.results {
                    let posterPath = movie.posterPath   // 영화의 포스터 이미지 경로
                    let image = try await ImageManager.shared.fetchImage(
                        from: posterPath ?? "",
                        size: .w342
                    )
                    // MovieDataSource 구조체로 영화 데이터와 이미지를 묶어 저장
                    let movieSource = MovieDataSource(movieData: movie, image: image)
                    tempMovies.append(movieSource)  // // 임시 배열에 추가
                }
                
                // 3. 가져온 데이터를 메인 데이터 소스와 검색 결과 배열에 할당
                self?.movieDataSource = tempMovies  // 컬렉션 뷰에 표시할 배열 업데이트
                // 저장된 검색 키워드로 필터링
                if let keyword = self?.searchKeyword, !keyword.isEmpty {
                    self?.searchMovies = tempMovies.filter { $0.movieData.title.contains(keyword) }
                } else {
                    self?.searchMovies = tempMovies // 초기 화면에 데이터를 표시
                }
                
                // 4. UI 업데이트는 메인 스레드에서 실행
                DispatchQueue.main.async {
                    self?.searchView.reloadCollectionView() // 컬렉션 뷰 리로드
                }
            } catch {
                // 5. 데이터 가져오기 실패 시 에러 메시지 출력
                print("데이터 불러오기 실패")
            }
        }
    }
}
  • 삼항연산자로 매개변수의 값을 확인 후 selectedCategory의 값을 .nowPlaying 또는 .upcoming으로 설정
  • 같은 방식으로 selectedParameter의 값을 NetworkManager.URLParameterSet.common 또는 NetworkManager.URLParameterSet.secondPage로 설정
  • fetchMovieData의 매개변수를 추가
  • 이에 따라 1,3번 내용 수정

아무 매개변수도 없던 fetchMovieData 메서드에 매개 변수를 추가하여 현재 상영중인 영화 데이터 목록과, 상영 예정인 영화 데이터 목록을 받아오도록 변경하였다.

그리고

self?.movieDataSource = tempMovies
self?.searchMovies = tempMovies

이던 3번 코드를 변경하여, 다른 탭을 눌러도 텍스트 필드에 입력한 검색 키워드가 유지되어 적용되도록 하였다.

이 코드를 처음엔 쓰지 않아서, 탭한 메뉴에만 검색이 적용되고 그 상태에서 다른 탭을 누르면 다시 영화 전체 목록이 나왔었다.


결과 화면

서버에서 현재 상영작과 상영 예정작에 같은 영화가 나오는 경우가 있어서, 우리 문제는 아니지만 결과 화면이 아름답지 않았다.

그래서 fetchMovieData 메서드의 매개변수에 parameter 코드를 추가한 것이다.

extension NetworkManager {
    enum URLParameterSet {
        static let empty: URLParameters = [:]
        static let common: URLParameters = ["language": "ko-KR", "page": "1"]
        static let secondPage: URLParameters = ["language": "ko-KR", "page": "2"] // 추가한 api 파라미터
        
        // 기존 코드
}

처음엔 현재 상영 중인 영화 목록과 상영 예정 영화 목록 둘 다 api 파라미터를 common으로 했다. 즉, 해당 목록의 1페이지만 가져오는 것이었다.

서버가 왜 상영 예정작과 상영 중인 영화 목록에 같은 영화가 섞이도록 반환 해주는지는 모르겠지만,

결과 화면에서 제대로 다른 api 요청임을 확인하기 위해 다른 페이지를 불러오는 secondPage 파라미터를 추가한 것이다.

fetchMovieData 메서드에 넣어준 parameter가 바로 이 secondPage를 저장한 프로퍼티인 것이다.

이제 사용자가 현재 상영중인 영화만 확인할 수 있는게 아니라, 상영 예정인 영화도 확인할 수 있게 되었다.

느낀점

MVC에 대해 확실히 배우는 느낌이다.

view와 Controller를 명확히 분리해 놓으니 새로운 기능을 추가하려고 할 때 어디에 추가해야 할지 확실히 알 수 있었다.
그리고 기능별로 메서드들도 분리해서 책임을 최소화 한 덕분에, 약간의 코드 수정 및 재사용만으로도 기능을 추가할 수 있었다.

덕분에 '현재 상영 중인 영화만 확인할 수 있는게 아니라, 상영 예정인 영화도 확인할 수 있게 하자'는 이야기가 나온지 한 시간여 만에 기능을 추가할 수 있었다.

불과 2~3주 전만 해도 이거 하나 하는데 꼬박 하루가 걸렸을 것이라고 생각한다.

나 뿐만 아니라 다른 팀원들도 다같이 코드 컨벤션을 지키고, 코드 리뷰도 진행하면서 리뷰하기 편하도록 주석도 달고 PR에 설명도 꼼꼼히 적어준 덕분에 기능을 추가하거나 변경하는게 무섭고 힘들기보단 흥미로운 경험이 될 수 있었다.

0개의 댓글