$30 off During Our Annual Pro Sale. View Details »

The Great iOS Concurrency Bake-Off - Swift Usergroup Netherlands, Amsterdam, February 2019

The Great iOS Concurrency Bake-Off - Swift Usergroup Netherlands, Amsterdam, February 2019

An Instant Replay™ of my concurrency talk from mobOS!

Abstract:
Callback Hell is a problem affecting developers of all stripes trying to write asynchronous code that's easy to follow. iOS developers don't yet have much in the way of built-in options to reduce callback hell, but there are several libraries which purport to help. In this session, you'll get a look at what problems are being solved by RxSwift and PromiseKit, as well as a look at an intriguing functional technique to avoid callback hell without libraries. And you'll learn more about how to answer the question: Which of these hammers is the most appropriate for my particular nail?

I will also make a ton of references to the Great British Bake Off, which you should watch because it is delightful.

Code:
https://github.com/designatednerd/OperationComparison

Original slides (they're pretty damned similar):
https://speakerdeck.com/designatednerd/the-great-ios-concurrency-bake-off-mobos-cluj-napoca-romania-february-2019

Ellen Shapiro
PRO

February 28, 2019
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

  1. The Great iOS Concurrency
    ! "
    Bake-Off
    !
    Swift Usergroup NL
    !
    | Amsterdam | February 2019
    @DesignatedNerd | bakkenbaeck.no | justhum.com

    View Slide

  2. What is Concurrency?

    View Slide

  3. Concurrency
    vs.
    Parallelism

    View Slide

  4. Metaphor
    Time!

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. View Slide

  19. View Slide

  20. View Slide

  21. View Slide

  22. View Slide

  23. View Slide

  24. View Slide

  25. View Slide

  26. Concurrency:
    "One thing should do these tasks at as
    close to the same time as possible."

    View Slide

  27. View Slide

  28. View Slide

  29. View Slide

  30. View Slide

  31. View Slide

  32. View Slide

  33. View Slide

  34. View Slide

  35. Parallelism:
    "Many things should do all these identical
    mini-tasks at the same time."

    View Slide

  36. View Slide

  37. View Slide

  38. View Slide

  39. View Slide

  40. View Slide

  41. View Slide

  42. !

    View Slide

  43. Taking the result of an asynchronous function

    View Slide

  44. Taking the result of an asynchronous function
    Giving it to another asynchronous function

    View Slide

  45. Taking the result of an asynchronous function
    Giving it to another asynchronous function
    (and another and another)

    View Slide

  46. The Problem:
    !
    Callback Hell

    View Slide

  47. Teh Codez!
    https://github.com/designatednerd/
    OperationComparison

    View Slide

  48. As a user, I want to see
    a profile picture
    when viewing a profile.

    View Slide

  49. As a developer, I want a
    list of profiles with icons to not
    drop frames when scrolling.

    View Slide

  50. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(failureCompletion: @escaping (Error) -> Void,
    successCompletion: @escaping (User) -> Void)
    static func fetchImage(for user: User,
    failureCompletion: @escaping (Swift.Error) -> Void,
    successCompletion: @escaping (UIImage) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Swift.Error) -> Void,
    successCompletion: @escaping (UIImage) -> Void)

    View Slide

  51. Not Horrible...
    self.activityIndicator.startAnimating()
    FakeAPI.fetchUser(failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error fetching user: \(error)"
    },
    successCompletion: { [weak self] user in
    guard let self = self else { return }
    self.operationLabel.text = "Fetching image..."
    })

    View Slide

  52. Not ideal...
    self.activityIndicator.startAnimating()
    FakeAPI.fetchUser(failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error fetching user: \(error)"
    },
    successCompletion: { [weak self] user in
    guard let self = self else { return }
    self.operationLabel.text = "Fetching image..."
    RealAPI.fetchImage(for: user,
    failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error fetching image: \(error)"
    },
    successCompletion: { [weak self] image in
    guard let self = self else { return }
    self.operationLabel.text = "Resizing image..."
    })
    })

    View Slide

  53. ...Does anyone have a ?
    self.activityIndicator.startAnimating()
    FakeAPI.fetchUser(failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error fetching user: \(error)"
    },
    successCompletion: { [weak self] user in
    guard let self = self else { return }
    self.operationLabel.text = "Fetching image..."
    RealAPI.fetchImage(for: user,
    failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error fetching image: \(error)"
    },
    successCompletion: { [weak self] image in
    guard let self = self else { return }
    self.operationLabel.text = "Resizing image..."
    ImageResizer.resizeImage(image,
    to: self.imageView.frame.size,
    failureCompletion: { [weak self] error in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.operationLabel.text = "Error resizing image: \(error)"
    },
    successCompletion: { [weak self] resizedImage in
    guard let self = self else { return }
    self.activityIndicator.stopAnimating()
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete in \(self.formattedSecondsSince(start))!"
    })
    })
    })

    View Slide

  54. ¯\_(ϑ)_/¯
    Avoid UI interaction
    and don't care about retain cycles!

    View Slide

  55. ¯\_(ϑ)_/¯
    Avoid UI interaction
    and don't care about retain cycles!*
    * - not recommended

    View Slide

  56. FakeAPI.fetchUser(failureCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    RealAPI.fetchImage(for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    ImageResizer.resizeImage(image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { resizedImage in
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!"
    })
    })
    })

    View Slide

  57. Think Indent Different

    View Slide

  58. FakeAPI.fetchUser(
    errorCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    RealAPI.fetchImage(
    for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { resizedImage in
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!"
    })
    })
    })

    View Slide

  59. Try to break it
    a p a r t

    View Slide

  60. private func fetchUser() {
    FakeAPI.fetchUser(
    failureCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    self.fetchImage(for: user)
    })
    }
    private func fetchImage(for user: User) {
    RealAPI.fetchImage(
    for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    self.resizeImage(image, to: self.imageView.frame.size)
    })
    }
    private func resizeImage(_ image: UIImage, to size: CGSize) {
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { [weak self] resizedImage in
    guard let self = self else { return }
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!")
    })
    }

    View Slide

  61. The problem:
    Dependent functions

    View Slide

  62. private func fetchUser() {
    FakeAPI.fetchUser(
    failureCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    self.fetchImage(for: user)
    })
    }
    private func fetchImage(for user: User) {
    RealAPI.fetchImage(
    for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    self.resizeImage(image, to: self.imageView.frame.size)
    })
    }
    private func resizeImage(_ image: UIImage, to size: CGSize) {
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { [weak self] resizedImage in
    guard let self = self else { return }
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!")
    })
    }

    View Slide

  63. private func fetchUser() {
    FakeAPI.fetchUser(
    failureCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    self.fetchImage(for: user)
    })
    }
    private func fetchImage(for user: User) {
    RealAPI.fetchImage(
    for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    self.resizeImage(image, to: self.imageView.frame.size)
    })
    }
    private func resizeImage(_ image: UIImage, to size: CGSize) {
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { [weak self] resizedImage in
    guard let self = self else { return }
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!")
    })
    }

    View Slide

  64. FakeAPI.fetchUser(
    errorCompletion: { error in
    debugPrint("Error fetching user: \(error)")
    },
    successCompletion: { user in
    RealAPI.fetchImage(
    for: user,
    failureCompletion: { error in
    debugPrint("Error fetching image: \(error)")
    },
    successCompletion: { image in
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size,
    failureCompletion: { error in
    debugPrint("Error resizing image: \(error)")
    },
    successCompletion: { resizedImage in
    self.imageView.image = resizedImage
    debugPrint("Complete in \(self.formattedSecondsSince(start))!"
    })
    })
    })

    View Slide

  65. A slight help:
    The Result Type

    View Slide

  66. !
    Swift 5.0
    https://github.com/apple/swift/blob/master/
    stdlib/public/core/
    Result.swift

    View Slide

  67. Result.Swift
    public enum Result {
    /// A success, storing a `Success` value.
    case success(Success)
    /// A failure, storing a `Failure` value.
    case failure(Failure)
    }

    View Slide

  68. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(failureCompletion: @escaping (Error) -> Void,
    successCompletion: @escaping (User) -> Void)
    static func fetchImage(for user: User,
    failureCompletion: @escaping (Swift.Error) -> Void,
    successCompletion: @escaping (UIImage) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Swift.Error) -> Void,
    successCompletion: @escaping (UIImage) -> Void)

    View Slide

  69. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(completion: @escaping (Result) -> Void)
    static func fetchImage(for user: User,
    completion: @escaping (Result) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Result) -> Void)

    View Slide

  70. FakeAPI.fetchUser { userResult in
    switch userResult {
    case .success(let user):
    RealAPI.fetchImage(for: user) { imageResult in
    switch imageResult {
    case .success(let image):
    ImageResizer.resizeImage(image,
    to: self.imageView.frame.size) { resizedImageResult in
    switch resizedImageResult {
    case .success(let resizedImage):
    self.imageView.image = resizedImage
    debugPrint("Complete")
    case .failure(let error):
    debugPrint("Error resizing image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching user: \(error)")
    }
    }

    View Slide

  71. FakeAPI.fetchUser { userResult in
    switch userResult {
    case .success(let user):
    RealAPI.fetchImage(for: user) { imageResult in
    switch imageResult {
    case .success(let image):
    ImageResizer.resizeImage(image,
    to: self.imageView.frame.size) { resizedImageResult in
    switch resizedImageResult {
    case .success(let resizedImage):
    self.imageView.image = resizedImage
    debugPrint("Complete")
    case .failure(let error):
    debugPrint("Error resizing image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching user: \(error)")
    }
    }

    View Slide

  72. FakeAPI.fetchUser { userResult in
    switch userResult {
    case .success(let user):
    RealAPI.fetchImage(for: user) { imageResult in
    switch imageResult {
    case .success(let image):
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size) { resizedImageResult in
    switch resizedImageResult {
    case .success(let resizedImage):
    self.imageView.image = resizedImage
    debugPrint("Complete")
    case .failure(let error):
    debugPrint("Error resizing image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching user: \(error)")
    }
    }

    View Slide

  73. FakeAPI.fetchUser { userResult in
    switch userResult {
    case .success(let user):
    RealAPI.fetchImage(for: user) { imageResult in
    switch imageResult {
    case .success(let image):
    ImageResizer.resizeImage(
    image,
    to: self.imageView.frame.size) { resizedImageResult in
    switch resizedImageResult {
    case .success(let resizedImage):
    self.imageView.image = resizedImage
    debugPrint("Complete")
    case .failure(let error):
    debugPrint("Error resizing image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching image: \(error)")
    }
    }
    case .failure(let error):
    debugPrint("Error fetching user: \(error)")
    }
    }

    View Slide

  74. How do we chain operations which need
    each others' results
    clearly?

    View Slide

  75. How do we chain operations which need
    each others' results
    in an easy-to-follow way?

    View Slide

  76. How do we chain operations which need
    each others' results
    so we can figure out where something's
    going wrong?

    View Slide

  77. ! "
    Bake-Off!

    View Slide

  78. View Slide

  79. View Slide

  80. View Slide

  81. Three competitors

    View Slide

  82. Three competitors
    (out of way more than three)

    View Slide

  83. View Slide

  84. View Slide

  85. View Slide

  86. The Problem:
    !
    Callback Hell

    View Slide

  87. Three competitors

    View Slide

  88. Three competitors
    1. Operator Overloading

    View Slide

  89. Three competitors
    1. Operator Overloading
    2. PromiseKit

    View Slide

  90. Three competitors
    1. Operator Overloading
    2. PromiseKit
    3. RxSwift

    View Slide

  91. !
    Signature Challenge:

    View Slide

  92. !
    Signature Challenge:
    Making Code More Readable

    View Slide

  93. Competitor #1:
    Operator Overloading

    View Slide

  94. Competitor #1:
    Operator Overloading
    "We don't need no stinkin' libraries!"

    View Slide

  95. View Slide

  96. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  97. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  98. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  99. PEMDAS

    View Slide

  100. PEMDAS
    (parentheses, exponents, multiplication,
    division, addition, subtraction)

    View Slide

  101. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  102. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  103. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  104. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  105. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  106. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(completion: @escaping (Result) -> Void)
    static func fetchImage(for user: User,
    completion: @escaping (Result) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Result) -> Void)

    View Slide

  107. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(completion: @escaping (Result) -> Void)
    static func fetchImage(for user: User,
    completion: @escaping (Result) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Result) -> Void)

    View Slide

  108. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(completion: @escaping (Result) -> Void)
    static func fetchImage(for user: User,
    completion: @escaping (Result) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Result) -> Void)

    View Slide

  109. !
    Wrapper Methods
    private func resizeImageForImageView(_ image: UIImage,
    completion: @escaping (Swift.Result) -> Void) {
    ImageResizer.resizeImage(image, to: self.imageView.frame.size, completion: completion)
    }

    View Slide

  110. !
    Wrapper Methods
    private func resizeImageForImageView(_ image: UIImage,
    completion: @escaping (Swift.Result) -> Void) {
    self.operationLabel.text = "Resizing image (Custom Operator)..."
    ImageResizer.resizeImage(image, to: self.imageView.frame.size, completion: completion)
    }
    private func fetchUserImage(for user: User,
    completion: @escaping (Swift.Result) -> Void) {
    self.operationLabel.text = "Fetching image (Custom Operator)..."
    RealAPI.fetchImage(for: user, completion: completion)
    }

    View Slide

  111. self.activityIndicator.startAnimating()
    let chain = API.fetchUser
    --> fetchUserImage
    --> resizeImageForImageView
    chain { result in
    self.activityIndicator.stopAnimating()
    switch result {
    case .success(let image):
    self.imageView.image = image
    self.operationLabel.text = "Complete (Custom Operator)!"
    case .error(let error):
    self.operationLabel.text = "Error occurred (Custom Operator): \(error)"
    }
    }

    View Slide

  112. self.activityIndicator.startAnimating()
    let chain = API.fetchUser
    --> fetchUserImage
    --> resizeImageForImageView
    chain { result in
    self.activityIndicator.stopAnimating()
    switch result {
    case .success(let image):
    self.imageView.image = image
    self.operationLabel.text = "Complete (Custom Operator)!"
    case .error(let error):
    self.operationLabel.text = "Error occurred (Custom Operator): \(error)"
    }
    }

    View Slide

  113. self.activityIndicator.startAnimating()
    let chain = API.fetchUser
    --> fetchUserImage
    --> resizeImageForImageView
    chain { result in
    self.activityIndicator.stopAnimating()
    switch result {
    case .success(let image):
    self.imageView.image = image
    self.operationLabel.text = "Complete (Custom Operator)!"
    case .error(let error):
    self.operationLabel.text = "Error occurred (Custom Operator): \(error)"
    }
    }

    View Slide

  114. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  115. typealias ResultCompletion = (Result) -> Void
    infix operator -->: MultiplicationPrecedence
    func -->(_ firstFunction: @escaping (@escaping ResultCompletion) -> Void,
    _ secondFunction: @escaping (T, @escaping ResultCompletion) -> Void)
    -> (@escaping ResultCompletion) -> Void {
    return { completion in
    firstFunction { result in
    switch result {
    case .success(let item):
    secondFunction(item) { result2 in
    completion(result2)
    }
    case .error(let error):
    completion(.error(error))
    }
    }
    }
    }

    View Slide

  116. Pro: You Do It Yourself

    View Slide

  117. Con: You Do It Yourself

    View Slide

  118. Competitor #2:
    PromiseKit

    View Slide

  119. Competitor #2:
    PromiseKit
    "OK, fine. Maybe we do need a library."

    View Slide

  120. View Slide

  121. View Slide

  122. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser(completion: @escaping (Result) -> Void)
    static func fetchImage(for user: User,
    completion: @escaping (Result) -> Void)
    static func resizeImage(_ image: UIImage,
    to size: CGSize,
    failureCompletion: @escaping (Result) -> Void)

    View Slide

  123. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser() -> Promise
    static func fetchImage(for user: User) -> Promise
    static func resizeImage(_ image: UIImage,
    to size: CGSize) -> Promise

    View Slide

  124. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  125. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  126. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  127. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  128. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  129. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  130. self.activityIndicator.startAnimating()
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  131. View Slide

  132. Solution #3:
    RxSwift

    View Slide

  133. Solution #3:
    RxSwift
    "Use ALL the libraries!"

    View Slide

  134. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser() -> Promise
    static func fetchImage(for user: User) -> Promise
    static func resizeImage(_ image: UIImage,
    to size: CGSize) -> Promise

    View Slide

  135. User Data -> Icon Image -> Resized Icon Image
    static func fetchUser() -> Single
    static func fetchImage(for user: User) -> Single
    static func resizeImage(_ image: UIImage,
    to size: CGSize) -> Single

    View Slide

  136. View Slide

  137. Observable

    View Slide

  138. Observable
    "I could get many values from this,
    until it tells me things are complete
    or an error has occurred."

    View Slide

  139. Single

    View Slide

  140. Single
    "This will either
    succeed or fail once."

    View Slide

  141. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  142. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  143. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  144. View Slide

  145. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  146. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  147. FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  148. View Slide

  149. !
    Technical Challenge:

    View Slide

  150. !
    Technical Challenge:
    Handling Threading

    View Slide

  151. Operator Overloading
    private func fetchUserImage(for user: User,
    completion: @escaping (Result) -> Void) {
    self.operationLabel.text = "Fetching image (Custom Operator)..."
    API.fetchImage(for: user, completion: completion)
    }

    View Slide

  152. Operator Overloading
    private func fetchUserImage(for user: User,
    completion: @escaping (Result) -> Void) {
    self.operationLabel.text = "Fetching image (Custom Operator)..."
    DispatchQueue.global(qos: .background).async {
    API.fetchImage(for: user, completion: completion)
    }
    }

    View Slide

  153. Operator Overloading
    private func fetchUserImage(for user: User,
    completion: @escaping (Result) -> Void) {
    self.operationLabel.text = "Fetching image (Custom Operator)..."
    DispatchQueue.global(qos: .background).async {
    API.fetchImage(for: user, completion: completion)
    }
    }

    View Slide

  154. Operator Overloading
    private func fetchUserImage(for user: User,
    completion: @escaping (Result) -> Void) {
    self.operationLabel.text = "Fetching image (Custom Operator)..."
    DispatchQueue.global(qos: .background).async {
    API.fetchImage(for: user, completion: completion)
    }
    }

    View Slide

  155. View Slide

  156. PromiseKit
    FakeAPI.fetchUser()
    .then { user in
    RealAPI.fetchImage(for: user)
    }
    .then { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  157. PromiseKit
    FakeAPI.fetchUser()
    .then(on: .global(qos: .background)) { user in
    RealAPI.fetchImage(for: user)
    }
    .then(on: .global(qos: .background)) { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  158. PromiseKit
    FakeAPI.fetchUser()
    .then(on: .global(qos: .background)) { user in
    RealAPI.fetchImage(for: user)
    }
    .then(on: .global(qos: .background)) { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  159. PromiseKit
    FakeAPI.fetchUser()
    .then(on: .global(qos: .background)) { user in
    RealAPI.fetchImage(for: user)
    }
    .then(on: .global(qos: .background)) { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  160. PromiseKit 6

    View Slide

  161. PromiseKit 6

    View Slide

  162. PromiseKit 6

    View Slide

  163. PromiseKit 6

    View Slide

  164. PromiseKit 6

    View Slide

  165. PromiseKit 6

    View Slide

  166. View Slide

  167. View Slide

  168. PromiseKit 6

    View Slide

  169. PromiseKit
    FakeAPI.fetchUser()
    .then(on: .global(qos: .background)) { user in
    RealAPI.fetchImage(for: user)
    }
    .map { image in
    (image, self.imageView.frame.size)
    }
    .then(on: .global(qos: .background)) { tuple in
    ImageResizer.resizeImage(tuple.0, to: tuple.1)
    }
    .then(on: .global(qos: .background)) { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  170. PromiseKit
    FakeAPI.fetchUser()
    .then(on: .global(qos: .background)) { user in
    RealAPI.fetchImage(for: user)
    }
    .map { image in
    (image, self.imageView.frame.size)
    }
    .then(on: .global(qos: .background)) { tuple in
    ImageResizer.resizeImage(tuple.0, to: tuple.1)
    }
    .then(on: .global(qos: .background)) { image in
    ImageResizer.resizeImage(image, to: self.imageView.frame.size)
    }
    .ensure {
    self.activityIndicator.stopAnimating()
    }
    .done { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (PromiseKit)!"
    }
    .catch { [weak self] error in
    self?.operationLabel.text = "Error occurred (PromiseKit): \(error)"
    }

    View Slide

  171. RxSwift:

    View Slide

  172. RxSwift:
    !
    Scheduler

    View Slide

  173. It's not a queue!

    View Slide

  174. It's not a queue!
    (it's just way easier to think of it as one)

    View Slide

  175. observeOn:
    tell me what happened here

    View Slide

  176. RxSwift
    FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  177. RxSwift
    FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .observeOn(MainScheduler.instance)
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  178. RxSwift
    FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .observeOn(MainScheduler.instance)
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  179. subscribeOn:
    actually do stuff here

    View Slide

  180. RxSwift
    FakeAPI.rx.fetchUser()
    .flatMap { user in
    RealAPI.rx.fetchImage(for: user)
    }
    .flatMap { image in
    ImageResizer.rx.resizeImage(image, to: self.imageView.frame.size)
    }
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
    .observeOn(MainScheduler.instance)
    .subscribe(
    onSuccess: { resizedImage in
    self.imageView.image = resizedImage
    self.operationLabel.text = "Complete (RxSwift) in \(self.formattedSecondsSince(start))!"
    },
    onError: { error in
    self.operationLabel.text = "Error occurred (RxSwift): \(error)"
    }
    )
    .disposed(by: bag)

    View Slide

  181. View Slide

  182. Operator overloading:
    Hard mode

    View Slide

  183. PromiseKit:
    Perform on a queue you specify
    Call back on a pre-defined queue

    View Slide

  184. RxSwift:
    Always specify where to do stuff
    Always specify where to call back

    View Slide

  185. !
    Show-Stopper Challenge:

    View Slide

  186. !
    Show-Stopper Challenge:
    Multi-Function Concurrency

    View Slide

  187. Two functions at the same time
    Do something with both results

    View Slide

  188. Operator Overloading

    View Slide

  189. Operator Overloading

    View Slide

  190. View Slide

  191. PromiseKit: Same Type
    let userPromise1 = FakeAPI.fetchUser()
    let userPromise2 = FakeAPI.fetchUser()
    when(fulfilled: [userPromise1, userPromise2])
    .done { users in
    debugPrint("Users: \(users)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  192. PromiseKit: Same Type
    let userPromise1 = FakeAPI.fetchUser()
    let userPromise2 = FakeAPI.fetchUser()
    when(fulfilled: [userPromise1, userPromise2])
    .done { users in
    debugPrint("Users: \(users)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  193. PromiseKit: Same Type
    let userPromise1 = FakeAPI.fetchUser()
    let userPromise2 = FakeAPI.fetchUser()
    when(fulfilled: [userPromise1, userPromise2])
    .done { users in
    debugPrint("Users: \(users)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  194. PromiseKit: Same Type
    let userPromise1 = FakeAPI.fetchUser()
    let userPromise2 = FakeAPI.fetchUser()
    when(fulfilled: userPromise1, userPromise2)
    .done { user1, user2 in
    debugPrint("User1: \(user1), User2: \(user2)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  195. PromiseKit: Same Type
    let userPromise1 = FakeAPI.fetchUser()
    let userPromise2 = FakeAPI.fetchUser()
    when(fulfilled: userPromise1, userPromise2)
    .done { user1, user2 in
    debugPrint("User1: \(user1), User2: \(user2)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  196. PromiseKit: Different Types
    let userPromise1 = FakeAPI.fetchUser()
    let imagePromise = ImageResizer.resizeImage(UIImage(),
    to: self.imageView.frame.size)
    when(fulfilled: userPromise1, imagePromise)
    .done { user, image in
    debugPrint("User: \(user), image: \(image)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  197. PromiseKit: Different Types
    let userPromise1 = FakeAPI.fetchUser()
    let imagePromise = ImageResizer.resizeImage(UIImage(),
    to: self.imageView.frame.size)
    when(fulfilled: userPromise1, imagePromise)
    .done { user, image in
    debugPrint("User: \(user), image: \(image)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  198. PromiseKit: Different Types
    let userPromise1 = FakeAPI.fetchUser()
    let imagePromise = ImageResizer.resizeImage(UIImage(),
    to: self.imageView.frame.size)
    when(fulfilled: userPromise1, imagePromise)
    .done { user, image in
    debugPrint("User: \(user), image: \(image)")
    }
    .catch { error in
    debugPrint("Error: \(error)")
    }

    View Slide

  199. View Slide

  200. View Slide

  201. RxSwift

    View Slide

  202. View Slide

  203. Rx Marble Diagrams

    View Slide

  204. RxSwift

    View Slide

  205. RxSwift

    View Slide

  206. RxSwift

    View Slide

  207. RxSwift

    View Slide

  208. RxSwift

    View Slide

  209. View Slide

  210. View Slide

  211. View Slide

  212. JUST PICK ONE!

    View Slide

  213. !
    PromiseKit

    View Slide

  214. View Slide

  215. !
    PromiseKit
    FOR ME

    View Slide

  216. View Slide

  217. !
    Killing flies
    with a sledgehammer

    View Slide

  218. Sometimes you
    need
    a sledgehammer

    View Slide

  219. Most of the time
    you only need a
    fly swatter

    View Slide

  220. Obligatory Summary Slide

    View Slide

  221. Obligatory Summary Slide
    → Harder concurrency is it looks than.

    View Slide

  222. Obligatory Summary Slide
    → Concurrency is harder than it looks.

    View Slide

  223. Obligatory Summary Slide
    → Concurrency is harder than it looks.
    → You can try to handle it yourself, but it's going to
    be a difficult time sink.

    View Slide

  224. Obligatory Summary Slide
    → Concurrency is harder than it looks.
    → You can try to handle it yourself, but it's going to
    be a difficult time sink.
    → PromiseKit is great for most concurrency handling
    and readability

    View Slide

  225. Obligatory Summary Slide
    → Concurrency is harder than it looks.
    → You can try to handle it yourself, but it's going to
    be a difficult time sink.
    → PromiseKit is great for most concurrency handling
    and readability
    → RxSwift is a giant hammer...but sometimes you
    need a giant hammer.

    View Slide

  226. View Slide

  227. Choose Wisely

    View Slide

  228. Links!
    → Vincent Pradeilles' Function Composition Talk
    https://vimeo.com/292702159
    → John Sundell's Tasks post
    https://www.swiftbysundell.com/posts/task-based-
    concurrency-in-swift
    → RxMarbles Visualization
    https://rxmarbles.com

    View Slide

  229. !
    The Future!
    → Chris Lattner's Concurrency Manifesto
    https://gist.github.com/lattner/
    31ed37682ef1576b16bca1432ea9f782
    → Kotlin Coroutines: Async/Await + More
    https://kotlinlang.org/docs/reference/coroutines-
    overview.html

    View Slide