[SwiftUI] Download JSON & Combine

Junyoung Park·2022년 8월 20일
0

SwiftUI

목록 보기
23/136
post-thumbnail

Download JSON from API in Swift with Combine | Continued Learning #23

Download JSON & Combine

구현 목표

  • 비동기 처리를 하는 방법 중 하나인 Combine 프레임워크
  • 이벤트 처리 기반으로 비동기적 이벤트 처리하는 방법
  • 이스케이핑 클로저를 사용할 필요가 없음
  • Publisher: 시간에 따라 데이터 처리
  • Subscriber: Publisher 값의 변화를 감지

구현 태스크

  • Combine 프레임워크를 통해 이스케이핑 클로저에서 처리한 비동기적 데이터 처리를 손쉽게 처리한다.
  • Publisher : 퍼블리셔를 생성한다. URL에서 데이터를 받아올 것이기 때문에 데이터가 존재하는 곳이 곧 퍼블리셔.
  • Subscriber: 퍼블리셔를 '구독'하는 존재로 백그라운드 스레드에서 실행
  • Receiver: 백그라운드 스레드에서 다운로드한 데이터를 로컬로 옮길 때에는 메인 스레드에서 이루어져야 함
  • tryMap: 구독한 곳에서 데이터를 핸들링하는 클로저.
  • Sink: completion이 완료되었는지에 따라 성공/실패를 확인, 성공했다면 receiveValue를 받을 수 있다. 그렇지 않다면 failure가 에러를 throw할 것이다.
  • ReceiveValue: weak self로 받아서 강한 참조 사이클을 방지한다. 이때 receiver에서 호출한 메소드이기 때문에 자동으로 메인 스레드에서 작업
  • Store: 구독을 취소할 수 있다.

핵심 코드

   func getPosts() {
        guard let url = getUrl() else { return }

        let publisher = URLSession.shared.dataTaskPublisher(for: url)
        let subscriber = publisher.subscribe(on: DispatchQueue.global(qos: .background))
        let receiver = subscriber.receive(on: DispatchQueue.main)
        
        receiver
            .tryMap(handleOutput)
            .decode(type: [PostModel].self, decoder: JSONDecoder())
//            .replaceError(with: []): Replace Error with default value
            .sink { (completion) in
                switch completion {
                case .finished:
                    print("SUCCESS")
                case .failure(let error):
                    print("FAILURE")
                    print(error.localizedDescription)
                }
            } receiveValue: { [weak self] (returnedPosts) in
                self?.posts = returnedPosts
            }
            .store(in: &cancellables)
    }
    
    private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            throw URLError(.badServerResponse)
        }
        return output.data
    }

소스 코드

import SwiftUI
import Combine

struct PostModel: Identifiable, Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

class DownloadWithCombineViewModel: ObservableObject {
    @Published var posts = [PostModel]()
    var cancellables = Set<AnyCancellable>()
    
    init() {
        getPosts()
    }
    
    func getPosts() {
        guard let url = getUrl() else { return }

        let publisher = URLSession.shared.dataTaskPublisher(for: url)
        let subscriber = publisher.subscribe(on: DispatchQueue.global(qos: .background))
        let receiver = subscriber.receive(on: DispatchQueue.main)
        
        receiver
            .tryMap(handleOutput)
            .decode(type: [PostModel].self, decoder: JSONDecoder())
//            .replaceError(with: []): Replace Error with default value
            .sink { (completion) in
                switch completion {
                case .finished:
                    print("SUCCESS")
                case .failure(let error):
                    print("FAILURE")
                    print(error.localizedDescription)
                }
            } receiveValue: { [weak self] (returnedPosts) in
                self?.posts = returnedPosts
            }
            .store(in: &cancellables)
    }
    
    private func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
        guard
            let response = output.response as? HTTPURLResponse,
            response.statusCode >= 200 && response.statusCode < 300 else {
            throw URLError(.badServerResponse)
        }
        return output.data
    }
    
    
    
    private func getUrl() -> URL? {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return nil }
        return url
    }
}


struct DownloadWithCombineBootCamp: View {
    @StateObject private var viewModel = DownloadWithCombineViewModel()
    var body: some View {
        List {
            ForEach(viewModel.posts) { post in
                VStack(alignment: .leading, spacing: 20) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.body)
                        .foregroundColor(.gray)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }
}
  • Combine 프레임워크는 비동기 데이터 처리를 담당하는 매우 편리한 방법 중 하나이지만, iOS 13 버전 이후부터 지원한다는 주의사항이 있다.
profile
JUST DO IT

0개의 댓글