Download JSON from API in Swift with Combine | Continued Learning #23
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 버전 이후부터 지원한다는 주의사항이 있다.