Search code examples
iosswiftgoogle-places-apireactive-swift

Why can't I observe events in this ReactiveSwift-enabled network workflow?


I'm training myself on using ReactiveSwift for networking and a good use case for this seemed to fetch photos for a location from the Google Places API for iOS.

The flow is as follow:

  1. Get a list of GMSPlacePhotoMetadata from a google place ID
  2. For each metadata, fetch a picture
  3. Concatenates all the pictures as an array

I wrote code doing this workflow in the best ReactiveSwift way I could think of (cf. code below) but when I call my service, although all the API calls to the Google Places API are made, I do not get into the observing part. I feel like I'm missing something really basic from the framework and that I lost some observers along the way, but I can't put my finger where the problem is. Any help will be more than welcome.

My Service Code

import Foundation
import ReactiveSwift
import GooglePlaces

struct GooglePlacesPhotoService {
    func findPlacePictures(googlePlaceID: String) -> SignalProducer<[UIImage], DataStoreError> {
        return findPlacePicturesMetadata(googlePlaceID: googlePlaceID)
            .map { (metadata) -> SignalProducer<UIImage, DataStoreError> in
                debugPrint("Mapping metadata to SignalProducer for metadata: ", metadata)
                return self.findPlacePicture(metadata: metadata)
            } // After mapping, we have a SignalProducer of SignalProducer<UIImage>
            .flatten(.merge) // After flatening, we get a single SignalProducer<UIImage>
            .reduce([], { (imageArray: [UIImage], newImage: UIImage) -> [UIImage] in
                debugPrint("Merging another picture")
                return imageArray + [newImage]
            }) // Now we have an array of UIImage
    }

    private func findPlacePicturesMetadata(googlePlaceID: String) -> SignalProducer<GMSPlacePhotoMetadata, DataStoreError> {
        return SignalProducer<GMSPlacePhotoMetadata, DataStoreError> { observer, disposable in
            GMSPlacesClient.shared().lookUpPhotos(forPlaceID: googlePlaceID) { photos, error in
                guard error == nil else { return observer.send(error: .externalError(error!)) }
                guard let photos = photos else { return }

                photos.results.forEach { metadata in
                    debugPrint("Sending metadata value: ", metadata)
                    observer.send(value: metadata)
                }
            }
        }
    }

    private func findPlacePicture(metadata: GMSPlacePhotoMetadata) -> SignalProducer<UIImage, DataStoreError> {
        return SignalProducer<UIImage, DataStoreError> { observer, disposable in
            let screenSize = UIScreen.main.bounds.size
            let screenScale = UIScreen.main.scale

            let myCallback: GMSPlacePhotoImageResultCallback = { image, error in
                guard error == nil else {
                    print("ERROR: couln't load picture for metadata \(metadata)")
                    observer.send(error: .externalError(error!))
                    return
                }

                guard let image = image else {
                    print("ERROR: empty image returned")
                    observer.send(error: .unknownExternalError)
                    return
                }

                debugPrint("Got 1 picture from metadata: ", metadata)
                observer.send(value: image)
            }

            GMSPlacesClient.shared().loadPlacePhoto(metadata,
                                                    constrainedTo: screenSize,
                                                    scale: screenScale,
                                                    callback: myCallback)
        }
    }
}

My Observing Code

    googlePlaceIDProperty.signal
        .filter { $0.isPresent }
        .flatMap(.latest) { googlePlaceID in
            return GooglePlacesPhotoService().findPlacePictures(googlePlaceID: googlePlaceID!)
        }.observe { event in
            debugPrint("Signal event!") // I NEVER GET THERE
            switch event {
            case let .value(pictures):
                // Do stuff
            case let .failed(error):
                // Do stuff
            default:
                break
            }
    }

My logs

"Sending metadata value: " <GMSPlacePhotoMetadata: 0x60000645f4d0>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x60000645f4d0>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x60000645b1b0>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x60000645b1b0>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x60000645b0f0>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x60000645b0f0>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x600006459950>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x600006459950>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x60000644e730>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x60000644e730>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x60000645ef30>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x60000645ef30>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x6000066420a0>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x6000066420a0>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x600006448d60>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x600006448d60>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x600006642130>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x600006642130>
"Sending metadata value: " <GMSPlacePhotoMetadata: 0x6000066421f0>
"Mapping metadata to SignalProducer for metadata: " <GMSPlacePhotoMetadata: 0x6000066421f0>
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x60000645f4d0>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x60000645b1b0>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x60000645b0f0>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x600006459950>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x60000644e730>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x60000645ef30>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x6000066420a0>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x600006448d60>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x600006642130>
"Merging another picture"
"Got 1 picture from metadata: " <GMSPlacePhotoMetadata: 0x6000066421f0>
"Merging another picture"

Solution

  • The Event contract says that it only terminates with either a failure, completed, or interrupted event. So you need to make sure you call observer.sendCompleted() in your SignalProducer closures after all values have been sent.

    The documentation for reduce says it returns a "producer that sends the final result as self completes", which means the results will never be sent along until that completed event happens. Basically, there's no way for it to know that it has collected all of the results unless your SignalProducers explicitly send a completed event to indicate that they have no more values to send. It is well illustrated on this graph.

    So in your case, in findPlacePicture, you should call sendCompleted() after you got the result you expected, ie:

    observer.send(value: image)
    observer.sendCompleted() // <- That's the line to add.
    

    and in findPlacePicturesMetadata:

    photos.results.forEach { metadata in
        debugPrint("Sending metadata value: ", metadata)
            observer.send(value: metadata)
        }
    observer.sendCompleted() // <- That's the line to add.