Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Declarative Networking with Combine –– iOS Conf SG 2020

Declarative Networking with Combine –– iOS Conf SG 2020

Ritesh Gupta

January 18, 2020
Tweet

More Decks by Ritesh Gupta

Other Decks in Programming

Transcript

  1. #DroidConBos @ragdroid
    Declarative Networking
    with Combine
    Ritesh Gupta
    iOS Conf SG 2020

    View Slide

  2. @_riteshhh
    fueled.com/ritesh
    twitter.com/@_riteshhh
    riteshhh.com
    github.com/riteshhgupta
    Ritesh Gupta

    View Slide

  3. @_riteshhh
    Combine
    It’s a new reactive framework by Apple
    which provides a declarative Swift API for
    processing values over time.

    View Slide

  4. @_riteshhh
    Pillars of Combine
    Publisher
    Just
    Fail
    Future
    DataTaskPublisher
    Map
    FlatMap
    Filter
    Catch
    Sink
    Assign
    AnySubscriber
    [Operators] Subscriber

    View Slide

  5. @_riteshhh
    Networking
    • URLSession
    • URLSessionDataTask
    • func dataTask(URL) -> URLSessionDataTask
    (without Combine)

    View Slide

  6. @_riteshhh
    Networking
    (with Combine)
    • URLSession
    • URLSession.DataTaskPublisher
    • func dataTaskPublisher(URL) -> DataTaskPublisher

    View Slide

  7. @_riteshhh
    DataTaskPublisher
    typealias DataTaskResult = (Data, URLResponse)
    public struct DataTaskPublisher : Publisher {
    /// The kind of values published by this publisher.
    public typealias Output = DataTaskResult
    /// The kind of errors this publisher might publish.
    public typealias Failure = URLError
    }

    View Slide

  8. @_riteshhh
    DataTaskPublisher
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )

    View Slide

  9. @_riteshhh
    DataTaskPublisher
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )

    View Slide

  10. @_riteshhh
    DataTaskPublisher
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    •Gateway timeout error
    •No internet error

    View Slide

  11. @_riteshhh
    DataTaskPublisher
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    •401: Invalid API Key
    •404: Not found

    View Slide

  12. @_riteshhh
    Validation
    • move response into correct stream
    (i.e. 4xx response into failure stream
    and 2xx into output/value stream)

    View Slide

  13. @_riteshhh
    Validation
    func validateResponse()

    View Slide

  14. @_riteshhh
    Validation
    func validateResponse() -> ValidatedPublisher
    enum ValidationError: Error {
    case error(Error)
    case jsonError(Data)
    }
    typealias ValidatedPublisher = AnyPublisher
    typealias DataTaskResult = (Data, URLResponse)

    View Slide

  15. @_riteshhh
    Validation
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher
    typealias IsValidCallback = (DataTaskResult) -> Bool
    typealias ValidatedPublisher = AnyPublisher
    typealias DataTaskResult = (Data, URLResponse)

    View Slide

  16. @_riteshhh
    Validation
    extension Publisher where Output == DataTaskResult {
    func validateResponse(isValid: IsValidCallBack)
    -> ValidatedPublisher
    }

    View Slide

  17. @_riteshhh
    Validation
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    }

    View Slide

  18. @_riteshhh
    Validation
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    }

    View Slide

  19. @_riteshhh
    Validation
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in }
    }

    View Slide

  20. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output/Success stream
    } else {
    // return the value in the Failure stream
    }
    }
    }

    View Slide

  21. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output/Success stream
    } else {
    // return the value in the Failure stream
    }
    }
    }

    View Slide

  22. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    }
    }
    }

    View Slide

  23. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    }
    }
    }

    View Slide

  24. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    return Fail(
    outputType: DataTaskResult.self,
    failure: ValidationError.jsonError(result.data)
    ).eraseToAnyPublisher()
    }
    }
    }

    View Slide

  25. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    return Fail(
    outputType: DataTaskResult.self,
    failure: ValidationError.jsonError(result.data)
    ).eraseToAnyPublisher()
    }
    }
    }

    View Slide

  26. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    return Fail(
    outputType: DataTaskResult.self,
    failure: ValidationError.jsonError(result.data)
    ).eraseToAnyPublisher()
    }
    }
    }

    View Slide

  27. @_riteshhh
    func validateResponse(isValid: IsValidCallback)
    -> ValidatedPublisher {
    return self
    .mapError { ValidationError.error($0) }
    .flatMap { (result) -> ValidatedPublisher in
    if isValid(result) {
    // return the value in the Output stream
    return Just(result)
    .setFailureType(to: ValidationError.self)
    .eraseToAnyPublisher()
    } else {
    // return the value in the Failure stream
    return Fail(
    outputType: Output.self,
    failure: ValidationError.jsonError(result.data)
    ).eraseToAnyPublisher()
    }
    }.eraseToAnyPublisher()
    }

    View Slide

  28. @_riteshhh
    func validateStatusCode(_ isValid: (Int) -> Bool) -> ValidatedPublisher {
    return validateResponse { (data, response) in
    let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
    return isValid(statusCode)
    }
    }
    func validateResponse(isValid: (DataTaskResult) -> Bool)
    -> ValidatedPublisher { ... }
    Validation

    View Slide

  29. @_riteshhh
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    Declarative Networking

    View Slide

  30. @_riteshhh
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .validateStatusCode { (200..<300).contains($0) }
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    Declarative Networking

    View Slide

  31. @_riteshhh
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .validateStatusCode { (200..<300).contains($0) }
    .validateResponse { !$0.data.isEmpty }
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    Declarative Networking

    View Slide

  32. @_riteshhh
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .validateStatusCode { (200..<300).contains($0) }
    .validateResponse { !$0.data.isEmpty }
    .mapErrorJson(to: ApiErrorResponse.self, decoder: decoder)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    Declarative Networking

    View Slide

  33. @_riteshhh
    let url = URL(string: "http://api.openweathermap.org/data/2.5")!
    let session = URLSession.shared
    session
    .dataTaskPublisher(for: url)
    .validateStatusCode { (200..<300).contains($0) }
    .validateResponse { !$0.data.isEmpty }
    .mapErrorJson(to: ApiErrorResponse.self, decoder: decoder)
    .mapValueJson(to: ApiValueResponse.self, decoder: decoder)
    .sink(
    receiveCompletion: { print("Completion/Failure: \($0)") },
    receiveValue: { print("Value: \($0)") }
    )
    Declarative Networking

    View Slide

  34. @_riteshhh
    Further reading
    • Declarative Networking with Combine ––
    riteshhh.com
    • Awesome Combine –– Github

    View Slide

  35. @_riteshhh
    Thank you!

    View Slide