[출처:www.raywenderlich.com/books/combine-asynchronous-programming-with-swift/v2.0]
프로그래머로써 networking 작업을 많이 하게됩니다.
- backend 와 통신
- fetching data
- pushing updates
- encoding and decoding JSON
combine은 이런 흔한 작업들을 declaratively 하게 수행할 수 있는 API 를 제공합니다.
이런 API 들은 현대 앱의 2가지 부분에 도움을 줍니다.
- URLSession
- JSON encoding and decoding through the Codable protocol
URLSession extensions
URLSession은 network data 전송 작업에 권장되는 방법입니다.
여러 configuration 이 가능한 현대적인 비동기 API를 제공하며 완전히 투명한 백그라운드를 지원한다? (무슨표현..?)
여러가지 operation들을 지원하는데
- URL 의 내용을 받는 Data transfer tasks
- URL 의 내용을 받아 file로 저장하는 Download tasks
- file 과 data를 URL로 업로드 하는 upload tasks
- 두개의 parties 사이에서 데이터를 스트리밍하는 Stream tasks
- 웹소켓을 연결하는 Websocket tasks
이 작업들중 첫번째인 data transfer task 만이 combine publisher 를 노출시킵니다.
Combine은 URLRequest 또는 URL만 받는 두 가지 변형이 있는 하나의 API를 사용하여 이러한 작업을 처리합니다.
guard let url = URL(string: "https://mysite.com/mydata.json") else {
return
}
// 1
let subscription = URLSession.shared
// 2
.dataTaskPublisher(for: url)
.sink(receiveCompletion: { completion in
// 3
if case .failure(let err) = completion {
print("Retrieving data failed with error \(err)")
}
}, receiveValue: { data, response in
// 4
print("Retrieved data of size \(data.count), response = \(response)")
})’
- resulting subscription을 가지고 있는게 중요합니다. 그렇지 않으면 바로 canceled 되어 request 가 영원히 실행되지 않습니다.
- URL 을 파라미터로 가지는 dataTaskPublihser(for:)의 overload를 사용합니다.
- 항상 error 상황을 처리해야한다. 특히 네트워크 connection은 실패하기 쉽습니다.
- 결과는 tuple 형태로 Data와 URLResponse 객체입니다.
보이는바와 같이 Combine은 URLSession.dataTask 위에 투명한 뼈대 publisher 추상화를 제공하고 클로저 대신 publisher 만 노출합니다.
Codable support
Codable 프로토콜은 현대적이고 강력한 Swift 만이 제공하는 encoding, decoding 메커니즘 입니다.
Foundation은 JSONEncoder 과 JSONDecoder를 통해 encoding to JSON 과 decoding from JSON을 지원합니다.
또한 PropertyListEncoder나 PropertyListDecoder를 사용할 수 있는데 network request 의 맥락에서는 덜 유용합니다.
이전 예제에서 JSON 을 다운받았습니다. 그걸 JSONDecoder 로 decode 하겠습니다.
let subscription = URLSession.shared
.dataTaskPublisher(for: url)
.tryMap { data, _ in
try JSONDecoder().decode(MyType.self, from: data)
}
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Retrieving data failed with error \(err)")
}
}, receiveValue: { object in
print("Retrieved object \(object)")
})
JSON을 tryMap 내에서 decode 시켜도 되지만 Combine은 decoding 할 때 쓰는 boilderplate 를 제공합니다.
.map(\.data)
.decode(type: MyType.self, decoder: JSONDecoder())
tryMap 오퍼레이터를 위 2줄로 바꿔줍니다.
불행히도 dataTaskPublisher(for:) 는 tuple 을 방출해서 decode(type:decoder:) map 을 사용해 결과값 중 data만 방출할 수 있도록 해야합니다.
유일한 장점은 tryMap 은 publisher를 세팅할때 JSONDecoder를 한번만 instantiate 시켜주면 되지만 tryMap 에서는 매번 생성해줘야 합니다.
Publishing network data to multiple subscribers
publisher를 subscribe 하면 publisher 는 일을 시작합니다.
network request 와 같은 경우 만약 multiple subscriber 가 result 를 원하면 publisher도 같은 request를 여러번 보내는 다는 의미입니다.
이 작업을 쉽게 만들 수 있는 연산자가 Combine 에는 부족합니다.
share operator 를 사용해도 좋지만 까다롭습니다. 왜냐하면 result 가 오기전 모든 subscribers 를 구독해야 되기 때문입니다.
caching 메커니즘을 사용하는 것 이외에 다른 해결책은 multicast() operator 를 사용하는 것 입니다.
multicast() operator 는 Subject를 통해 값을 publish 하는 ConnectablePublisher를 만듭니다.
ConnectablePublisher는 subject를 여러번 구독하는걸 가능하게 하고 다 준비가 된 다음 publisher의 connect() 메소드를 호출합니다.
let url = URL(string: "https://www.raywenderlich.com")!
let publisher = URLSession.shared
// 1
.dataTaskPublisher(for: url)
.map(\.data)
.multicast { PassthroughSubject<Data, URLError>() }
// 2
let subscription1 = publisher
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Sink1 Retrieving data failed with error \(err)")
}
}, receiveValue: { object in
print("Sink1 Retrieved object \(object)")
})
// 3
let subscription2 = publisher
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Sink2 Retrieving data failed with error \(err)")
}
}, receiveValue: { object in
print("Sink2 Retrieved object \(object)")
})
// 4
let subscription = publisher.connect()
- DataTaskPublisher를 만들어 .data 를 mapping 하고 mulicast operator 를 적용합니다.
전달된 클로져는 반드시 적절한 type을 반환해야 합니다.
아니면 존재하는 subject를 multicast(subject:)에 전달해도 됩니다.
multicast 는 13장에서 더 자세하게 배우게 됩니다. - publisher를 처음 구독하면 publisher 는 ConnectablePublisher이기 때문에 바로 동작하지 않습니다.
- 두번째로 publisher 를 구독합니다.
- publisher를 connect() 하면 바로 동작하며 subscriber 에게 값들을 방출할 것 입니다.
이 코드에서 request 는 단 한번만 요청했고 하나의 result 를 두개의 subscriber 와 함께 공유한 것을 볼 수 있습니다.
*subscription은 Cancellables에 저장하는걸 잊지마세요. 그렇지 않으면 current code scope 를 벗어날 때 deallocated되고 canceled 될 수 있습니다.
이러한 상황에 맞는 operator 를 Combine 은 제공하지 않기 때문에 이 과정은 약간 복잡하게 느껴집니다.
18장에서 더 나은 방법을 배우게 될 것 입니다.
Key points
- Combine은 dataTaskPublisher (for :)라는 dataTask(with : completionHandler :) 메서드에 Publisher 기반 추상화를 제공합니다.
- Codable을 준수하는 model에 대해 Data 값을 방출하는 publisher 에 내장된 decode operator 를 통해 decode 를 수행할 수 있습니다.
- 다수의 subscribers에 대해 subscription을 반복하는 operator 는 없기 때문에 이 행위를 ConnectablePublisher와 multicast operator 를 사용해 재창조할 수 있습니다.
import UIKit
import Combine
struct Contacts: Decodable {
let contacts : [Contact?]
}
struct Contact : Decodable {
let id : String?
let name : String?
let email : String?
let address : String?
let gender : String?
let phone : Phone?
}
struct Phone : Decodable {
let mobile : String?
let home : String?
let office : String?
}
func printResponse(_ object : Contacts ){
for index in 0..<object.contacts.count {
let item = object.contacts[index]
if let id = item?.id,let name = item?.name, let email = item?.email {
print(id, name, email)
}
}
print("")
}
guard let url = URL(string: "https://api.androidhive.info/contacts/") else {
fatalError("nil")
}
//let subscription = URLSession.shared
// .dataTaskPublisher(for: url)
// .sink(receiveCompletion: { completion in
// if case .failure(let err) = completion {
// print("Retrieving data failed with error \(err)")
// }
// }, receiveValue: { object in
// print(object)
// })
//let subscription2 = URLSession.shared
// .dataTaskPublisher(for: url)
// .tryMap { data, _ in
// try JSONDecoder().decode(Contacts.self, from: data)
// }
// .sink(receiveCompletion: { completion in
// if case .failure(let err) = completion {
// print("Retrieving data failed with error \(err)")
// }
// }, receiveValue: { object in
// for index in 0..<object.contacts.count {
// let item = object.contacts[index]
// if let id = item?.id,let name = item?.name, let email = item?.email {
// print(id, name, email)
// }
//
// }
// })
//let subscription3 = URLSession.shared
// .dataTaskPublisher(for: url)
// .map(\.data)
// .decode(type: Contacts.self, decoder: JSONDecoder())
// .sink(receiveCompletion: { completion in
// if case .failure(let err) = completion {
// print("Retrieving data failed with error \(err)")
// }
// }, receiveValue: { object in
// })
//
//
let publisher = URLSession.shared
// 1
.dataTaskPublisher(for: url)
.map(\.data)
.multicast { PassthroughSubject<Data, URLError>() }
// 2
let subscription4 = publisher
.decode(type: Contacts.self, decoder: JSONDecoder())
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Sink1 Retrieving data failed with error \(err)")
}
}, receiveValue: { object in
print("subscription4")
printResponse(object)
})
// 3
let subscription5 = publisher
.decode(type: Contacts.self, decoder: JSONDecoder())
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Sink2 Retrieving data failed with error \(err)")
}
}, receiveValue: { object in
print("subscription5")
printResponse(object)
})
// 4
//publisher.connect()
let subscription6 = publisher.connect()
//
//
//
// {
// "contacts": [
// {
// "id": "c200",
// "name": "Ravi Tamada",
// "email": "ravi@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c201",
// "name": "Johnny Depp",
// "email": "johnny_depp@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c202",
// "name": "Leonardo Dicaprio",
// "email": "leonardo_dicaprio@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c203",
// "name": "John Wayne",
// "email": "john_wayne@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c204",
// "name": "Angelina Jolie",
// "email": "angelina_jolie@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "female",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c205",
// "name": "Dido",
// "email": "dido@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "female",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c206",
// "name": "Adele",
// "email": "adele@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "female",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c207",
// "name": "Hugh Jackman",
// "email": "hugh_jackman@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c208",
// "name": "Will Smith",
// "email": "will_smith@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c209",
// "name": "Clint Eastwood",
// "email": "clint_eastwood@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c2010",
// "name": "Barack Obama",
// "email": "barack_obama@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c2011",
// "name": "Kate Winslet",
// "email": "kate_winslet@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "female",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// },
// {
// "id": "c2012",
// "name": "Eminem",
// "email": "eminem@gmail.com",
// "address": "xx-xx-xxxx,x - street, x - country",
// "gender" : "male",
// "phone": {
// "mobile": "+91 0000000000",
// "home": "00 000000",
// "office": "00 000000"
// }
// }
// ]
// }
링크
www.raywenderlich.com/3418439-encoding-and-decoding-in-swift
'iOS > Combine' 카테고리의 다른 글
#Chapter15 In Practice: Combine & SwiftUI (0) | 2020.11.02 |
---|---|
#12 Key-Value Observing (0) | 2020.10.30 |
"UI events are asynchronous" (0) | 2020.10.28 |
#3 Transforming Operators (1) | 2020.10.28 |
#2 Publishers & Subscribers (0) | 2020.10.27 |
댓글