본문 바로가기
UIKit/기본

아이튠즈 API를 통해 영화 정보 얻어오기

by 밤새는탐험가 2024. 4. 26.

순서랑 코드만 간단히 적어본다. 

https://leral123-it.tistory.com/entry/%EA%B3%B5%EA%B3%B5-API%EB%A5%BC-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B0%96%EA%B3%A0-%EC%98%A4%EB%82%98-%EA%B3%B5%EA%B3%B5-API%EB%A5%BC-GET-%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

 

공공 API를 어떻게 갖고 오나? (공공 API를 GET 하는 방법)

먼저 스위프트로 API를 갖고 오는 코드 구현 URL 구조체 생성 URL은 옵셔널 바인딩 처리를 해야 한다. 주소는 아이튠즈 API 갖고 왔다. https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTu

leral123-it.tistory.com

 

API 주소와 같은 문자열은 별도의 파일을 만들어 관리한다. 

이때 주로 열거형, 구조체를 통해 구분한다. 

 

// MARK: API 주소 문자열 묶음
enum MovieApi {
    static let requestUrl = "https://itunes.apple.com/search?"
    static let mediaParam = "media=movie"
}

 

 

데이터를 갖고 오는 함수는 분리해서 작성한다. 

일전에 작성한 함수를 참조했다. 

 

네트워크에서 발생할 수 있는 에러를 정의한다. 

이 때 정의한 이유는 Result를 통해 데이터를 갖고 오기 위함이다

 

enum NetworkError: Error {
    case networkingError
    case dataError
    case parseError
}

 

 

네트워크를 요청하는 함수를 생성한다. 이를 통해 영화 데이터를 가져온다. 

// 네트워크 요청하는 함수 (영화 데이터 가져오기)
func fetchMovie(searchTerm: String, completion: @escaping NetworkCompletion) {
    let urlString = "\(MovieApi.requestUrl)\(MovieApi.mediaParam)&term=\(searchTerm)"
    print(urlString)

    performRequest(with: urlString) { result in
        completion(result)
    }
}

 

 

위에서 네트워크를 요청하는 함수 내에서 실제로 Request 하는 함수를 구현한다. 

여기서 error를 먼저 확인하는 이유는, 에러 발생 유무에 따라 코드를 계속 실행시킬지 말지 정하기 위함이다. 

// 실제 Request하는 함수 (비동기 실행 ▶️ 클로저 방식으로 끝난 시점을 전달 받도록 설계)
private func performRequest(with urlString: String, completion: @escaping NetworkCompletion) {
    print(#function)
    guard let url = URL(string: urlString) else { return }

    let session = URLSession(configuration: .default)

    let task = session.dataTask(with: url) { (data, response, error) in
        if error != nil {
            print(error!.localizedDescription)
            completion(.failure(.networkingError))
            return
        }

        guard let safeData = data else {
            completion(.failure(.dataError))
            return
        }

        // 메서드 실행해서, 결과를 받는다.
        if let movies = self.parseJSON(safeData) {
            print("Parse 실행")
            completion(.success(movies))
        } else {
            print("Parse 실패")
            completion(.failure(.parseError))
        }

    }
    task.resume()
}

 

 

 

그리고 최종적으로 받아온 데이터를 확인하는 함수를 구현한다. 

 

private func parseJSON(_ movieData: Data) -> [Movie]? {

    // 성공
    do {
        // ⭐️ JSON Data -> MovieData 구조체
        let movieData = try JSONDecoder().decode(MovieData.self, from: movieData)
        return movieData.results
    // 실패
    } catch {
        print(error.localizedDescription)
        return nil
    }
}

 

 

ViewController 내부에 API 를 통해 받아올 데이터를 표시할 테이블 뷰를 하나 생성한다. 

 

테이블 뷰를 만들 때 순서는 다음과 같다. 

1. 테이블 뷰 변수를 하나 생성한다. 

2. 테이블을 사용하기 위해 dataSource, delegate 를 선언한다. 

3. 오토레이아웃을 설정한다. 

4. 테이블에 사용할 테이블 셀을 생성한다. 

5. dataSource, delegate 선언으로 필요한 함수를 구현한다. 

 

 

먼저 Constants 에 아래 코드를 생성한다.

// MARK: - 사용할 Cell 문자열 묶음
public struct cell {
    static let movieCellIdentifier = "MovieCell"
    static let movieCollectionViewCellIdentifier = "MovieCollectionViewCell"
    
    private init() {}
}

 

 

테이블 뷰를 선언한다. 

private let movieTableView = UITableView()

 

 

dataSource, delegate 선언 및 테이블 셀을 등록한다.

func setupMovieTableView() {
    movieTableView.dataSource = self
    movieTableView.delegate = self
    movieTableView.register(MovieTableViewCell.self, forCellReuseIdentifier: cell.movieCellIdentifier)
}

 

 

테이블에 대한 오토 레이아웃을 선언한다. 

func setupMovieTableViewConstraints() {
    view.addSubview(movieTableView)
    movieTableView.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activate([
        movieTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
        movieTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
        movieTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
        movieTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0)
    ])
}

 

 

viewDidLoad() 메서드 내에 아래 함수를 구현한다. 

추가로 초기 화면에 나오게 할 데이터 함수를 구현한다. 

override func viewDidLoad() {
        super.viewDidLoad()
        
        setupDatas()
        setupMovieTableView()
        setupMovieTableViewConstraints()
        setupSearchBar()
        setupNaviBar()
    }
    
    
    // MARK: - 데이터 셋업
    func setupDatas() {
        networkManager.fetchMovie(searchTerm: "marvel") { result in
            print(#function)
            switch result {
            case.success(let movieDatas):
                self.movieArray = movieDatas
                print(movieDatas)
                DispatchQueue.main.async {
                    self.movieTableView.reloadData()
                }
            case.failure(let error):
                print(error.localizedDescription)
            }
        }
    }

 

 

테이블 셀을 구현한다. 

 

import UIKit

final class MovieTableViewCell: UITableViewCell {
    
    var imageUrl: String? {
        didSet {
            loadImage()
        }
    }
    
    // MARK: - UI 설정
    var movieMainImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    var stackView: UIStackView = {
        let sv = UIStackView()
        sv.axis = .vertical
        sv.distribution = .fill
        sv.alignment = .fill
        sv.spacing = 3
        return sv
    }()
    
    var movieNameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16, weight: .bold)
        label.numberOfLines = 1
        return label
    }()
    
    var movieDescription: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14, weight: .regular)
        label.numberOfLines = 3
        return label
    }()
    
    var movieReleasedDate: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 12, weight: .bold)
        label.numberOfLines = 1
        return label
    }()
    
    // 셀이 재사용되기 전에 호출되는 메서드 
    override func prepareForReuse() {
        super.prepareForReuse()
        
        self.movieMainImageView.image = nil
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: .default, reuseIdentifier: reuseIdentifier)
        setupStackView()
        setConstraints()
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // stackView에 영화제목, 감독, 설명 포함
    func setupStackView() {
        self.contentView.addSubview(movieMainImageView)
        self.contentView.addSubview(stackView)
        
        stackView.addArrangedSubview(movieNameLabel)
        stackView.addArrangedSubview(movieDescription)
        stackView.addArrangedSubview(movieReleasedDate)
    }
    
    
    // MARK: - 제약 조건 설정
    func setConstraints() {
        movieMainImageViewConstraints()
        // movieDescriptionConstraints()
        setStackViewConstraints()
    }
    
    // 이미지 제약조건 설정
    func movieMainImageViewConstraints() {
        NSLayoutConstraint.activate([
            movieMainImageView.heightAnchor.constraint(equalToConstant: 100),
            movieMainImageView.widthAnchor.constraint(equalToConstant: 100),
            movieMainImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 5),
            movieMainImageView.centerYAnchor.constraint(equalTo: self.contentView.centerYAnchor)
        ])
    }
    
    func movieDescriptionConstraints() {
        movieDescription.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            movieDescription.heightAnchor.constraint(equalToConstant: 18)
        ])
    }
    
    // 스택뷰 제약조건 설정
    func setStackViewConstraints() {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            stackView.leadingAnchor.constraint(equalTo: movieMainImageView.trailingAnchor, constant: 5),
            stackView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -5),
            stackView.topAnchor.constraint(equalTo: movieMainImageView.topAnchor),
            stackView.bottomAnchor.constraint(equalTo: movieMainImageView.bottomAnchor)
        ])
    }
    
    // URL ===> 이미지를 셋팅하는 메서드
    private func loadImage() {
        
        guard let urlString = self.imageUrl, let url = URL(string: urlString) else { return }
        
        DispatchQueue.global().async {
            guard let data = try? Data(contentsOf: url) else { return }
            guard urlString == url.absoluteString else { return }
            
            DispatchQueue.main.async {
                self.movieMainImageView.image = UIImage(data: data)
            }
        }
    }
}

 

 

최종적으로 대리자 선언에 따른 함수를 구현한다. 

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.movieArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = movieTableView.dequeueReusableCell(withIdentifier: cell.movieCellIdentifier, for: indexPath) as? MovieTableViewCell else { return MovieTableViewCell() }
        
        cell.imageUrl = movieArray[indexPath.row].artworkUrl100
        
        cell.movieNameLabel.text = movieArray[indexPath.row].movieName
        cell.movieDescription.text = movieArray[indexPath.row].shortDescription
        cell.movieReleasedDate.text = movieArray[indexPath.row].releaseDateString
        
        cell.selectionStyle = .none
        return cell
        
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 120
    }
}