Skip to content

Instantly share code, notes, and snippets.

@rdv0011
Last active May 6, 2021 02:54
Show Gist options
  • Save rdv0011/ca53ae6b4bfc5b400703acbaea4b22f4 to your computer and use it in GitHub Desktop.
Save rdv0011/ca53ae6b4bfc5b400703acbaea4b22f4 to your computer and use it in GitHub Desktop.
Swift Combine. Transform publisher output values and combine publishers together.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment