2

I have an array of A objects. I would like to transform it into an array with objects of type B. But the tricky part is to download images in the meantime and do everything using RxSwift or ReactiveSwift. Do you have any tips how could I do it?

struct A {
  let name: String
  let imageURL: URL
  let thumbnailURL: URL
}

struct B {
  let name: String
  let image: UIImage?
  let thumbnail: UIImage?
}
Kris
  • 1,538
  • 2
  • 16
  • 27
  • Are you trying to display these images as they load asynchronously like in an a table view or do you just want to create a final array once they've all downloaded? It'd help to have a bit more context about the final goal here. – jjoelson May 07 '18 at 12:37

2 Answers2

4

So notwithstanding my comment about the context for this potentially mattering a great deal, here's how I would asynchronously convert [A] to [B] using ReactiveSwift. Note that I haven't had a chance to test this code, but it should get across the basic idea:

// This function takes an `NSURL` and creates an asynchronous `SignalProducer` to
// download an image from that URL or yields `nil` if there is an error.
func downloadImage(url: URL) -> SignalProducer<UIImage?, NoError> {
    let request = URLRequest(url: url)
    return URLSession.shared.reactive.data(with: request)
        .map { (data, response) -> UIImage? in UIImage(data: data) }
        .flatMapError { _ in SignalProducer<UIImage?, NoError>(value: nil) }
}

func convertAToB(_ a: A) -> SignalProducer<B, NoError> {
    let imgDownload = downloadImage(url: a.imageURL)
    let thumbDownload = downloadImage(url: a.thumbnailURL)
    return SignalProducer<UIImage?, NoError>.combineLatest(imgDownload, thumbDownload)
        .map { images in
            return B(name: a.name, image: images.0, thumbnail: images.1)
        }
}

func convertAllAsToBs(_ inputs: [A]) -> SignalProducer<[B], NoError> {
    return SignalProducer<A, NoError>(values: inputs)
        .flatMap(.concat, convertAToB)
        .collect()
}


let inputs: [A] = ...

convertAllAsToBs(inputs).startWithValues { outputs in
    // `outputs` is [B]
}

Edit:

To bring this answer to parity with @PhilippeC's RxSwift answer, here's a summary of what each ReactiveSwift operator is doing:

  • SignalProducer.init(values:) is the equivalent of RxSwift's Observable.from. It creates a producer that sends each value of the sequence as a separate event.
  • collect is the equivalent of RxSwift's toArray. It collects each value from the source producer and sends them along in a single array once the source producer completes.
  • flatMap starts the convertAToB producer for each incoming A, and merges the result according to to specified FlattenStrategy. In this case, I used .concat, which makes it equivalent to RxSwift's concatMap which concatenates each result and preserves the order as @PhilippeC describes in his answer.
jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • 1
    One remark though - `.concat` order preserving comes at a cost - you hold next request until you get a response from the current one. With `.merge` and manually sorting the results you can perform the image fetching concurrently. – Kamil.S Jul 30 '18 at 14:06
0

With RxSwift, this can be done using a couple of operators:

  • the Observable.from and toArray() operators that convert an Array to a sequence of Events, and back and forth.
  • the concatMap operator which transforms each individual A items, then concatenates each individual result into one observable sequence, and respects the original ordering.

Here is a Swift code:

// choose the implementation you prefer for this function
// N.B. : if using RxCocoa, prefer Single<UIImage?> as return type
func downloadImage(url: URL) -> Observable<UIImage?> {
    return URLSession.shared.rx
        .data(URL)
        .map { data in UIImage(data: data) }
        .catchErrorJustReturn(nil)
}

let arrayOfA: [A] = []; // your input array goes here.

let arrayOfB: Observable<[B]> =
Observable
    // Convert each array element to an item
    .from(arrayOfA)
    // concatMap preserves the order
    .concatMap { a in
        Observable.zip(downloadImage(a.imageURL), downloadImage(a.thumbnailURL))
            .map { image, thumbnail in
                B(name: a.name, image: image, thumbnail: thumbnail)
            }
    }
    .toArray()

 // do some stuff with the result: arrayOfB

At the end, you get the resulting array as an Observable<[B]> that is expected to a single event. To use it, just subscribe to this Observable, or bind it directly to your UI or your DataSource.

N.B. in case of a long array, I also suggest running the downloaded on a background queue: thanks to Rx, this can be easily done just by adding something like .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) after .toArray().

PhilippeC
  • 51
  • 1
  • 3