[iOS] URLSession 더럽게 사용해보기

Charlie·2022년 10월 1일
0
post-custom-banner

네트워크 연습중

아무 생각 없이 더럽게 URLSession을 사용해보자.
데이터는 아래에서 Article 정보들을 가져와보자.

https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey=bb8e4fcfd4bc4ea4a1ba2b1b105a592f

구현

1. 받아올 데이터 확인하고 DTO 만들기

나중에 옵셔널처리를 안해서 또는 오탈자로 인해 디코딩 실패로 이어지는 경우가 허다하므로 꼼꼼히 확인하는게 정신건강에 좋다..
확인이 끝나면 그에 맞게 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
    }
}

2. 실제 사용할 모델 만들기

이번엔 실제로 우리가 사용할 모델을 만들어보자. DTO에는 저자, 타이틀, 설명 등등 많은 프로퍼티들이 있지만 테스트를 위해 일단 author, title, description, urlToImage를 가지는 모델을 만들었다. DTO때와 마찬가지로 Decodable (또는 Encodable) 프로토콜을 준수하도록 하자.

struct Article: Decodable {
    let author: String
    let title: String
    let description: String
    let urlToImage: String
}

3. URLSession을 통해 통신하기

모델을 다 만들었으면 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
            }
        }
    }

ViewController 전체 코드

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
    }
}

DetailViewController

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)
        }
    }
}
profile
Hello
post-custom-banner

0개의 댓글