본문 바로가기
프로그래밍 팁/Swift

RxSwift로 간단한 파일 다운로더 만들기

by Archivers 2020. 11. 15.

 

 

RxCocoa에는 URLSession을 리액티브하게 사용할 수 있도록 만들어 주는 익스텐션이 포함되어 있습니다. 이를테면 public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)>라는 메소드를 사용해서 HTTPURLResponseData를 리액티브하게 받는 등, 편의를 위한 메소드들이 존재합니다. 하지만 익스텐션으로 제공되는 메소드에 파일을 다운로드 받는 메소드는 존재하지 않습니다. 따라서 이번 포스팅에서는 RxSwift를 활용해서 이를 간단하게 구현해 보겠습니다.

구현 방법

우선, URLSession의 리액티브 익스텐션을 정의하고 그 안에 메소드의 파라미터와 적절한 반환형을 정의합니다.

extension Reactive where Base: URLSession {
    func download(request: URLRequest) -> Observable<(response: HTTPURLResponse, localURL: URL)> {

    }
}

그 다음에는 비동기 작업을 리액티브 래핑하기 위한 Observable.create { ... } 메소드의 결괏값을 반환해 주도록 작성하고, 그 안에는 URLSessiondownloadTask 메소드를 호출하도록 만듭니다. downloadTask로 생성된 taskresume() 호출을 통해 생성 직후 세션이 시작되도록 만들고, 반환 값으로 Disposables.create(with: task.cancel)을 정의해 줌으로써 옵저버블이 디스포즈될 때 세션이 취소되도록 설정합니다.

extension Reactive where Base: URLSession {
    func download(request: URLRequest) -> Observable<(response: HTTPURLResponse, localURL: URL)> {
        return Observable.create { observer in
            let task = self.base.downloadTask(with: request) { localURL, response, error in

            }

            task.resume()

            return Disposables.create(with: task.cancel)
        }
    }
}

그 뒤, downloadTask에 전달되는 컴플리션 클로저를 채워야 합니다. 이 클로저 안에서는 호출 성공 시 observernext 이벤트를 방출하도록 한다거나, 호출 실패 시 error 이벤트를 방출하도록 해야 합니다. 이를 구현하면 최종적으로 다음과 같습니다.

extension Reactive where Base: URLSession {
    func download(request: URLRequest) -> Observable<(response: HTTPURLResponse, localURL: URL)> {
        return Observable.create { observer in
            let task = self.base.downloadTask(with: request) { localURL, response, error in
                guard let localURL = localURL else {
                    observer.on(.error(error ?? APIError.failure))
                    return
                }

                guard let httpResponse = response as? HTTPURLResponse else {
                    observer.on(.error(APIError.failure))
                    return
                }

                guard 200 ..< 300 ~= httpResponse.statusCode else {
                    observer.onError(APIError.failure)
                    return
                }

                observer.on(.next((httpResponse, localURL)))
                observer.on(.completed)
            }

            task.resume()

            return Disposables.create(with: task.cancel)
        }
    }
}

APIError.failureenum으로 미리 정의된 에러입니다. 예제에서는 모든 에러 상황에 대해 .failure 케이스를 전달하고 있는데, 이 부분은 필요에 맞게 바꿔서 쓰시면 됩니다. URLSessiondownloadTaskdataTask와는 달리 결과를 디스크에 저장합니다. 따라서 컴플리션 클로저에는 localURL이 존재하고 이를 next 이벤트에 보내서 적절히 사용하면 됩니다. next 이벤트를 전달한 뒤에는 completed 이벤트를 전달해서 옵저버블이 종료됨을 알림으로써 컴플리션 클로저 내의 작업이 마무리되게 됩니다.

위와 같이 정의된 리액티브 익스텐션을 사용하는 예시는 다음과 같습니다.

static func download(url: URL) -> Single<URL> {
    let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)

    return URLSession.shared.rx
        .download(request: request)
        .map { $0.localURL }
        .asSingle()
}

URLRequest 등은 상황에 맞게 변경해서 사용하면 됩니다. .asSingle()은 해당 옵저버블을 Single 타입의 trait로 바꾸기 위해서 사용되는데, 이는 값을 한 번만 받고 종료한다는 것을 해당 download 메소드를 사용하는 측에서 보다 확실히 인지할 수 있도록 추가한 것입니다.

댓글