새싹

URLSession에서 Generic 사용하기

Thor_yeom 2023. 9. 1. 18:58

API 통신 하면서 여러 API를 불러오게 되면 

각각 URLSession을 만들어 데이터를 불러와야 됐다...

 

그렇다면 한개의 URLSession만 만들어서 사용할 수 있는 방법은 없을까?

에서 출발한 포스팅입니다.

 

그럼 시작하겠습니다.  컬렉션 뷰를 사용해서 만들어보겠습니다

 

일단 VC를 깔끔하게 사용하기 위해서 

VC, View, Cell을 분리해주었습니다.
( 배운거 사용하고 싶었어요...)

 

  • Cell을 먼저 만듭니다.
import UIKit
import SnapKit

class FirstCell : UICollectionViewCell {
    
    let imageView = {
        let image = UIImageView()
        image.backgroundColor = .gray
        image.contentMode = .scaleToFill
        return image
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureView()
        setConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureView() {
       contentView.addSubview(imageView)
    }
    
    func setConstraints() {
        imageView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
}
  •  
  •  
  •  
  •  
  •  
  • View를 만들어 줍니다.
class FirstView: UIView {
    
    lazy var collectionView = {
        let view = UICollectionView(frame: .zero, collectionViewLayout: setCollectionViewFlowLayout())
        view.register(FirstCell.self, forCellWithReuseIdentifier: String(describing: FirstCell.self))
        return view
    }()
    
    private func setCollectionViewFlowLayout() -> UICollectionViewFlowLayout {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .vertical
        let spacing: CGFloat = 8
        let width = UIScreen.main.bounds.width - (spacing * 4)
        layout.itemSize = CGSize(width: width / 3, height: width / 3)
        layout.minimumLineSpacing = spacing
        layout.minimumInteritemSpacing = spacing
        layout.sectionInset = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing)
        return layout
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        configureView()
        setConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureView() {
        self.addSubview(collectionView)
    }
    
    func setConstraints() {
        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
}

 

  •  viewDidLoad()가 로드 되기 전에 loadView()에서 먼저 로드를 진행시킵니다.
    • 왜냐하면 FirstVC의 RootView에 먼저 커스텀 FirstView를 적용하기 위해서 입니다. <- 시점의 차이

 

  • 전체 코드 입니다.
//
//  ViewController.swift
//  URLSession+Generic
//
//  Created by 염성필 on 2023/09/01.
//

import UIKit

class FristViewController: UIViewController {
    
    let firstView = FirstView()
    
    override func loadView() {
        self.view = firstView
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        configureView()
    }
    
    func configureView() {
        firstView.collectionView.dataSource = self
        firstView.collectionView.delegate = self
        firstView.collectionView.register(FirstCell.self, forCellWithReuseIdentifier: String(describing: FirstCell.self))
    }
}

extension FristViewController: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: FirstCell.self), for: indexPath) as? FirstCell else { return UICollectionViewCell() }
        return cell
    }
}

 

 

확실히 VC에 다 넣은것보다 간결해졌쥬?

시뮬레이터 콜렉션 뷰

 

그렇다면 이제 네트워크 통신을 위해 

모델과 URLSesstion을 만들어 보겠습니다

 

 

첫번째로  모델 두개를 만들어 주겠습니다

  • unsplash 와 TMDB의 이미지 데이터 모델을 만들었습니다.

TMDBPhotoResult에 연산 프로퍼티로

따로 작성된 이유는TMDB에서는 이미지 query만 주기 때문에

baseURL을 추가해줬습니다.

 

 

 

일반적으로 API를 불러올 때 이렇게 작성합니다.

 

VC에 적용해보겠습니다

 

네 역시나 잘 나옵니다.

 

kingfisher를 사용하지 않고 직접적으로

Image에 비동기 작업을 적용해줬습니다.

 

여기까지는 어렵지 않게 다들 하셨죠?

밥 아저씨...

 

 

 

여기서 오늘의 본론이 나옵니다.

 

 

추가로 두번째 모델인 unsplash를 데이터 통신 하려면 
NetworkManager에 또 다른 callRequest 함수를 만들어줘야 할까요?

 

 

A : 당연 빠따루이지... 말모!!

 

만약에 

( 만약에 충 아니에요 ㅡ,ㅡ)

데이터 통신을 5개, 10개 해야된다면... 하나씩 다 만들어 줄건가요??

... ( 거 말씀이 너무 심한거 아니오)

 

해결 방법이 있습니다!

 

 

Generic에 대해 아시나요?

공식문서에 보면... 

핵심 포인트가 보입니다.

 

플렉시블 하고 함수를 재사용 가능하게 사용할 수있는 코드

제네릭은 Swift의 아주 강려크한 특징이다.

 

네 그렇습니다. Generic을 사용해서 한번 적용해보겠습니다

 

혹시 Generic에 대해 모르시는 분들은 개발자들에게 한 줄기 빛이 되어 주시는 소들이님의 블로그를 참고해보시길 바랍니다.

https://babbab2.tistory.com/136

 

Swift) 제네릭(Generic) 정복하기

안녕하세요 :) 소들입니다! 오늘의 두 번째 포스팅은 Generic에 대한 것입니다! 범용 타입이라구 하죠!! 어렵지 않은 문법이라 호딱 끝내 봅시다 :)) 모든 포스팅은 편의 말투로 합니다~!! 1. Generic이

babbab2.tistory.com

 

자. 그렇다면 저희 프로젝트에 어떻게 적용해보면 좋을까요?

파라미터 앞에 <T: Codable>을 적용해주고 해당 모델이 들어갔던 자리에 T를 넣어주면 됩니다.

 

Generic을 사용하실때 이것만 기억하시면 됩니다.

 

T: placeHolder - 그냥 플레이스홀더 임...

<T: ~ > : ~ 에는 타입이 들어감

 

 

처음과 비교해보면 TMDB의 자료형은 Codable로 되어있습니다.  고로 TMDB(T) : Codable  와 같은 의미입니다.

 

 

그렇다면 어떻게 적용하면 될까요??

해석을 해보면 " Codable 타입인건 알겠는데 그래서 뭘 넣어줄건데? 그냥 response만 넣으면 끝이 아니잖아? " 

(너가 generic을 선택한 결과물이다...견뎌라...)

 

자 그렇다면 어떻게 해줘야 할까요? 아주 쉽습니다.

 

TMDB의 자료형 Codable이었죠?

response의 자료형을 TMDB로 설정합니다.

 

그렇게 되면 response == TMDB와 같아집니다.

(와웅미...) 참 신기하죠? 역시 파워풀하네요

 

 

 

 

 

여기까지 EBS  수학 교재였다면...

 

 

 

 

 

주의 ❗️

지금부터 숨마쿰라우데 버젼 시작하겠습니다.

( 벌써...싫다..)

 

 

그렇다면... unsplash를 불러볼까요?

생각해야되는 부분이 제네릭으로 만든 함수를 보면 baseUrl을 받아와서 

URLSession에 적용시켜 주니까 열거형을 만들어서 human Error를 최대한 없애줍니다.

 

첫번째로 열거형을 먼저 만들어줍니다.

 

파라미터에 열겨형을 추가해줍니다.

switch 문을 통해서 들어오는 파라미터의 값에 따라 다른 url이 할당 받을 수 있도록 합니다.

 

이렇게 Network를 만들어주고 호출해주면 됩니다.

참 쉽죠? 

물론 unsplashList를 만들어줘야 되는 센스 다 아시죠?

var list: TMDB = TMDB(results: [])
var unsplashList: Photo = Photo(total: 0, total_pages: 0, results: [])

 

원하는 것만 보여주기 위해 주석 처리를 하면 됩니다.

 

 

 

자... 여기서 끝나면 아쉽겠죠?

(그...그만 이러다가 다 죽어 )

 

 

 

 

한번에 두 API를 불러봅시다.

 

어떻게 한 VC에서 두가지의 API를 보여줄 수 있을까요?

 

Collection Section 추가

 

현재까지는 한개의 section에서 해주었다면 section을 2개로 설정해줍니다.

 

그 다음은 section에 맞게 보여주고싶은 데이터를 나눠주기만 하면 됩니다.

   func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: FirstCell.self), for: indexPath) as? FirstCell else { return UICollectionViewCell() }
        
        
        if indexPath.section == 0 {
            let unsplashItem = unsplashList.results[indexPath.item].urls.thumb
            let unsplashurl = URL(string: unsplashItem)!
            // 비동기로 작업
            DispatchQueue.global().async {
                // 이미지 주소 Data화 시키기
                let data = try! Data(contentsOf: unsplashurl)
                
                // Main Thread에서 그려주기
                DispatchQueue.main.async {
                    // 데이터 통신 해서 image에 넣기
                    cell.imageView.image = UIImage(data: data)
                }
            }
        } else {
            let item = list.results[indexPath.item].imageUrl
            let url = URL(string: item)!
            // 비동기로 작업
            DispatchQueue.global().async {
                // 이미지 주소 Data화 시키기
                let data = try! Data(contentsOf: url)
                
                // Main Thread에서 그려주기
                DispatchQueue.main.async {
                    // 데이터 통신 해서 image에 넣기
                    cell.imageView.image = UIImage(data: data)
                }
            }
        }
        return cell
    }
    
}

 

 

흠... 코드가 조금 지저분한 느낌이 들죠? 리팩토링 해보겠습니다.

이렇게 줄여 볼 수 있겠네요

 

 

cellForRowAt 부분에 cell.setConfigure부분이 중복되긴 하네요...

여기도 한번 더 리팩토링 해보겠습니다.

( 적당히 해라... !!!)

 

 

section에 따라 들어오는 url을 담을 변수를 지정해줍니다.

section 별로 담긴 sectionImageurl을 

마지막에 담아주기만 하면 끝!

 

 

Section별로 이미지가 다른것을 볼 수 있습니다.

 

 

 

이상 먼저해주고 대신해주는 아무개 발자였습니다. 감사합니다.