아무 생각 없이 더럽게 URLSession을 사용해보자.
데이터는 아래에서 Article 정보들을 가져와보자.
나중에 옵셔널처리를 안해서 또는 오탈자로 인해 디코딩 실패로 이어지는 경우가 허다하므로 꼼꼼히 확인하는게 정신건강에 좋다..
확인이 끝나면 그에 맞게 DTO 모델을 만들자.
quicktype을 이용하면 쉽게 만들 수 있다.
DTO 모델을 만들 때에는 key에 오타가 없는지, 타입은 옵셔널으로 해야하는지 등을 잘 확인하고 Decodable 프로토콜을 준수하도록 만들어야한다. (때에 따라 Encodable)
struct ArticleDTO: Decodable {
var status: String?
var totalResults: Int?
var articles: [ArticleModel]?
struct ArticleModel: Decodable {
var source: Source
var author: String?
var title: String
var description: String?
var url: String
var urlToImage: String?
var publishedAt: String
var content: String?
}
struct Source: Decodable {
var id: String?
var name: String
}
}
이번엔 실제로 우리가 사용할 모델을 만들어보자. DTO에는 저자, 타이틀, 설명 등등 많은 프로퍼티들이 있지만 테스트를 위해 일단 author, title, description, urlToImage를 가지는 모델을 만들었다. DTO때와 마찬가지로 Decodable (또는 Encodable) 프로토콜을 준수하도록 하자.
struct Article: Decodable {
let author: String
let title: String
let description: String
let urlToImage: String
}
모델을 다 만들었으면 URLSession을 통해 데이터를 가져올 수 있다.
private func loadArticles(completion: @escaping (Result<[Article], Error>) -> Void) {
let urlString = "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=bb8e4fcfd4bc4ea4a1ba2b1b105a592f"
guard let url = URL(string: urlString) else {
print("URL 생성 실패")
return
}
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
do {
let dto = try JSONDecoder().decode(ArticleDTO.self, from: data)
completion(.success(self.convertToArticles(dto: dto)))
} catch {
print(error.localizedDescription)
completion(.failure(error))
}
}
}
task.resume()
}
우선 URL을 생성해주고 session은 shared로 설정해준다. 그리고 받아온 data를 디코딩하고 completion으로 넘겨주면 된다.
DTO를 내가 사용하는 모델로 변환해주기 위해 아래 메소드를 따로 구현해주었고
private func convertToArticles(dto: ArticleDTO) -> [Article] {
var articles: [Article] = []
dto.articles?.forEach({ article in
articles.append(Article(author: article.author ?? "nil", title: article.title, description: article.description ?? "nil", urlToImage: article.urlToImage ?? "nil"))
})
return articles
}
또한 받아온 데이터를 UITableView로 나타내도록 했기 때문에 데이터를 성공적으로 받아왔을 때 UITableView를 업데이트 할 수 있도록 completion을 사용해주었다.
새로고침 버튼을 눌렀을 때 아래 메소드를 호출하고 URLSession을 호출할 때 UITableView를 update하는 completion을 전달해주었다.
@objc private func updateButtonPressed(_ sender: UIButton) {
self.articles = []
self.loadArticles { result in
switch result {
case .success(let articles):
for _ in 0..<5 {
self.articles.append(articles.randomElement()!)
}
DispatchQueue.main.async {
self.articleTableView.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
return
}
}
}
import UIKit
final class MainViewController: UIViewController {
private lazy var articleTableView: UITableView = {
$0.register(ArticleTableViewCell.self, forCellReuseIdentifier: ArticleTableViewCell.identifier)
$0.dataSource = self
$0.delegate = self
return $0
}(UITableView())
private lazy var updateButton: UIButton = {
$0.addTarget(self, action: #selector(updateButtonPressed(_: )), for: .touchUpInside)
$0.setTitle("update", for: .normal)
return $0
}(UIButton(type: .system))
var articles: [Article] = []
override func viewDidLoad() {
super.viewDidLoad()
layout()
}
}
// MARK: - UI
extension MainViewController {
private func layout() {
self.view.backgroundColor = .systemBackground
self.view.addSubview(articleTableView)
self.view.addSubview(updateButton)
articleTableView.snp.makeConstraints { make in
make.top.equalTo(self.view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview().inset(100)
}
updateButton.snp.makeConstraints { make in
make.top.equalTo(articleTableView.snp.bottom).offset(30)
make.centerX.equalToSuperview()
make.width.equalTo(100)
make.height.equalTo(50)
}
}
}
// MARK: - UITableViewDataSource, UITableViewDelegate
extension MainViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
articles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ArticleTableViewCell.identifier, for: indexPath) as? ArticleTableViewCell else { return UITableViewCell() }
let article = self.articles[indexPath.row]
cell.titleLabel.text = article.title
cell.authorLabel.text = article.author
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = indexPath.row
let vc = DetailViewController()
vc.detailViewModel = articles[index]
self.navigationController?.pushViewController(vc, animated: true)
}
}
// MARK: - Interaction
extension MainViewController {
@objc private func updateButtonPressed(_ sender: UIButton) {
self.articles = []
self.loadArticles { result in
switch result {
case .success(let articles):
for _ in 0..<5 {
self.articles.append(articles.randomElement()!)
}
DispatchQueue.main.async {
self.articleTableView.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
return
}
}
}
private func loadArticles(completion: @escaping (Result<[Article], Error>) -> Void) {
let urlString = "https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=bb8e4fcfd4bc4ea4a1ba2b1b105a592f"
guard let url = URL(string: urlString) else {
print("URL 생성 실패")
return
}
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
if let error = error {
print(error.localizedDescription)
return
}
if let data = data {
do {
let dto = try JSONDecoder().decode(ArticleDTO.self, from: data)
completion(.success(self.convertToArticles(dto: dto)))
} catch {
print(error.localizedDescription)
completion(.failure(error))
}
}
}
task.resume()
}
private func convertToArticles(dto: ArticleDTO) -> [Article] {
var articles: [Article] = []
dto.articles?.forEach({ article in
articles.append(Article(author: article.author ?? "nil", title: article.title, description: article.description ?? "nil", urlToImage: article.urlToImage ?? "nil"))
})
return articles
}
}
import UIKit
import SnapKit
final class DetailViewController: UIViewController {
private lazy var titleLabel: UILabel = {
$0.numberOfLines = 0
return $0
}(UILabel())
private lazy var authorLabel: UILabel = {
return $0
}(UILabel())
private lazy var descriptionLabel: UILabel = {
$0.numberOfLines = 0
$0.textAlignment = .left
return $0
}(UILabel())
var detailViewModel: Article? {
didSet {
DispatchQueue.main.async {
self.titleLabel.text = self.detailViewModel?.title
self.authorLabel.text = self.detailViewModel?.author
self.descriptionLabel.text = self.detailViewModel?.description
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
layout()
}
}
// MARK: - UI
extension DetailViewController {
private func layout() {
self.view.backgroundColor = .systemBackground
self.view.addSubview(titleLabel)
self.view.addSubview(authorLabel)
self.view.addSubview(descriptionLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalTo(self.view.safeAreaLayoutGuide).offset(30)
make.leading.equalToSuperview().inset(30)
make.height.equalTo(40)
}
authorLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(30)
make.leading.equalToSuperview().inset(30)
make.height.equalTo(40)
}
descriptionLabel.snp.makeConstraints { make in
make.top.equalTo(authorLabel.snp.bottom).offset(30)
make.leading.trailing.equalToSuperview()
make.height.equalTo(200)
}
}
}