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:
GMSPlacePhotoMetadata
from a google place IDI 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.
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)
}
}
}
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
}
}
"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"
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.