본문 바로가기
iOS/Combine

#9 Networking

by HaningYa 2020. 10. 28.
728x90

[출처: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)")
  })’

  1. resulting subscription을 가지고 있는게 중요합니다. 그렇지 않으면 바로 canceled 되어 request 가 영원히 실행되지 않습니다.
  2. URL 을 파라미터로 가지는 dataTaskPublihser(for:)의 overload를 사용합니다.
  3. 항상 error 상황을 처리해야한다. 특히 네트워크 connection은 실패하기 쉽습니다.
  4. 결과는 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()

  1. DataTaskPublisher를 만들어 .data 를 mapping 하고 mulicast operator 를 적용합니다.
    전달된 클로져는 반드시 적절한 type을 반환해야 합니다.
    아니면 존재하는 subject를 multicast(subject:)에 전달해도 됩니다.
    multicast 는 13장에서 더 자세하게 배우게 됩니다.
  2. publisher를 처음 구독하면 publisher 는 ConnectablePublisher이기 때문에 바로 동작하지 않습니다. 
  3. 두번째로 publisher 를 구독합니다.
  4. 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

 

Encoding and Decoding in Swift

In this tutorial, you’ll learn all about encoding and decoding in Swift, exploring the basics and advanced topics like custom dates and custom encoding.

www.raywenderlich.com

developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

 

Apple Developer Documentation

 

developer.apple.com

 

728x90

'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

댓글