순서랑 코드만 간단히 적어본다.
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
}
}
'UIKit > 기본' 카테고리의 다른 글
서치바로 원하는 검색어에 맞춰 공공 API 정보 얻어오기 (0) | 2024.04.20 |
---|---|
공공 API를 어떻게 갖고 오나? (공공 API를 GET 하는 방법) (0) | 2024.04.09 |
Filemanager로 CRUD 해보기 (1) | 2024.04.04 |
네비게이션 바 + 탭 바 적용 (0) | 2024.03.29 |
Drawing Cycle (+ Layout Cycle) (0) | 2024.03.28 |