Quite often we encounter a problem of downloading files by links from the backend when writing apps. Let's take a look at how it might be achieved with Combine. The code below contains a dummy asymchronous task and transforms publisher's output values from a series of links to and an actual data items(images). A returned publisher might be used in a view model as a provider for UIImageView's for example.
import UIKit
import Combine
import WebKit
import PlaygroundSupport
enum CustomNetworkingError: Error {
case invalidServerResponse
init(error: URLError) {
// TODO: Implement initialization based on URLError properties
self = .invalidServerResponse
}
}
struct ItemLinks: Decodable {
let links: [URL]
}
let backgroundQueue: DispatchQueue = DispatchQueue(label: "backgroundQueue")
let backendURL = URL(string: "https://google.com")!
let response200 = HTTPURLResponse(url: URL(string: "https://google.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)!
let sampleImageSize = 25
let sampleImage = "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAIAAABLixI0AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpSIVB4uIKGSoLloQv3DUKhShQqgVWnUwuX5Ck4YkxcVRcC04+LFYdXBx1tXBVRAEP0BcXJ0UXaTE/yWFFjEeHPfj3b3H3TtAqJWYaraNAapmGYlYVEylV8XAK4IYRC+mMCIzU5+TpDg8x9c9fHy9i/As73N/jq5M1mSATySeZbphEW8QT29aOud94hAryBnic+JRgy5I/Mh1xeU3znmHBZ4ZMpKJeeIQsZhvYaWFWcFQiSeJwxlVo3wh5XKG8xZntVRhjXvyFwaz2soy12kOIIZFLEGCCAUVFFGChQitGikmErQf9fD3O36JXAq5imDkWEAZKmTHD/4Hv7s1cxPjblIwCrS/2PbHEBDYBepV2/4+tu36CeB/Bq60pr9cA2Y+Sa82tfAR0L0NXFw3NWUPuNwB+p502ZAdyU9TyOWA9zP6pjTQcwt0rrm9NfZx+gAkqav4DXBwCAznKXvd490drb39e6bR3w/1C3Lb1HPTfAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MMHgc5MuouxJ4AAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAACmUlEQVQ4y2P8//8/A2Vgw471efMiGRgYGCk068bt627VhgwMDKH6GUyUGPT9x/faaVUMDAxCbLK5cQXM0mqSh08d+v3rt6yULCMjI0lm7T28Z8a+FgYGhobwCbbmtgyyoewQ1D6t9dPnT/+JBj9+/vDN9pANZdePVnn/4f3///8Rfpy2vymzMfXVm1dEOur4mWMXXu1nYGAo9K8W4BdgYGBACa9DDzfUTar+/OUzQYOePHtSMC0NwrY0soQwmLY2ntYQtIAr2nZj8eotK/Eb9O/fv845be9+PWZgYDCX9lRWUIaapaupu6R1lZdGLFxpw7qcZy+e4TFr066NG6/Mg7ATfVKYmVmgZjEwMIiJiDXltcpx6yDC4twxXAa9evMKkjIhScHKxBouBQ0vMRGxzqwJcNHmZZWfPn/ENOjnr5/N0xrgXB/DEEioo5jFwMBgom9qLu0JYb/79XjakqmYZp25eBruOwYGhmC3UGRZhFnsbOxVSbXISeTG7etocZc9MRHONRBz1FLXxm4WAwODvrZBqH4GnNsyq/H7j+8Q9u/fv+smV0PiDgLK46vZ2dhxmsXExFSaWiHEJgtPbmu3rYGwF69duOcuIq3IceuYGZijhQB63pYQk5hfjtDTs67p3sN7b969mbSlE1lZhm8+KysrAbMYGBgMdY1qAybCI6F/Yc+MpdOQfcfAwGCmb4apEXv59enzR7tMEzT9cOCvkzSxZgoTExNhdzEwMPDx8md7leBKrvH+SZgG4TSLgYHBytgGq3ieS5OJvglWKZxmKcgqYBUPcA3EpQWnWdxc3EXubWiCZV5dKoqqJJvFwMAQ4BaEJmKmb45HPT6zFGQV/HWSUFOfJJlmMTAw2Braw9lCbLICfPx4FAMAqm4EUk4mLp0AAAAASUVORK5CYII="
let dummyLinks = ItemLinks(links: [URL(string: "google.com")!,
URL(string: "github.com")!,
URL(string: "facebook.com")!])
let liveView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: sampleImageSize, height: sampleImageSize * dummyLinks.links.count)))
let stackView = UIStackView()
stackView.axis = NSLayoutConstraint.Axis.vertical
func backendURLRequest(for endpoint: String) -> URLRequest {
URLRequest(url: backendURL.appendingPathComponent(endpoint))
}
func data(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
Future<(data: Data, response: URLResponse), URLError> { promise in
backgroundQueue.asyncAfter(deadline: .now() + 1) {
let imageData = Data(base64Encoded: sampleImage, options: .ignoreUnknownCharacters)!
promise(.success((data: imageData, response: response200)))
}
}
.eraseToAnyPublisher()
}
func getObject(for request: URLRequest) -> AnyPublisher<(object: ItemLinks, response: URLResponse), URLError> {
Future<(object: ItemLinks, response: URLResponse), URLError> { promise in
backgroundQueue.asyncAfter(deadline: .now() + 1) {
promise(.success((object: dummyLinks, response: response200)))
}
}
.eraseToAnyPublisher()
}
func handleHTTPResponseCode(_ response: URLResponse) throws {
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw CustomNetworkingError.invalidServerResponse
}
}
func dataPublishers(from links: [URL]) -> [AnyPublisher<Data, Error>] {
links.map {
data(for: URLRequest(url: $0)) // <-- (1) At this point we convert Output value from URL to Data by downloading images
.map { (arg0) -> Data in
let (data, _) = arg0
return data
}
.mapError { (error) -> Error in
CustomNetworkingError(error: error) // <-- (2) At this point we convert Error by checking URLError properties
}
.eraseToAnyPublisher()
}
}
/// This function does magic by converting an array of links into a publisher
func publisherSequence(from links: [URL]) -> AnyPublisher<Data, Error> {
Publishers.Sequence(sequence: dataPublishers(from: links)) // <-- At this point we convert an array of publishers to a sequence which is just AnyPublisher as a result
.flatMap { $0 }
.eraseToAnyPublisher()
}
func images(from endpoint: String) -> AnyPublisher<Data, Error> {
getObject(for: backendURLRequest(for: endpoint))
.tryMap { data, response -> (object: ItemLinks, response: URLResponse) in
try handleHTTPResponseCode(response)
return (object: data, response: response)
}
.flatMap { links, _ -> AnyPublisher<Data, Error> in
return publisherSequence(from: links.links)
}
.eraseToAnyPublisher()
}
let anyCancellable = images(from: "/links")
.subscribe(on: backgroundQueue)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (result) in
switch result {
case .finished: ()
case .failure(let error):
guard let error = error as? URLError else {
return
}
print(error)
}
stackView.translatesAutoresizingMaskIntoConstraints = false
liveView.addSubview(stackView)
stackView.centerXAnchor.constraint(equalTo: liveView.centerXAnchor).isActive = true
stackView.centerYAnchor.constraint(equalTo: liveView.centerYAnchor).isActive = true
//PlaygroundPage.current.finishExecution()
}) { (imageData) in
// <-- (3) We will get into this handler multiple times and create as many images as links we have because of a publisher sequence we have created earlier
stackView.addArrangedSubview(UIImageView(image: UIImage(data: imageData)))
}
PlaygroundPage.current.liveView = liveView
PlaygroundPage.current.needsIndefiniteExecution = true