I have built a simplified combine pipeline in my Xcode Playground, to make car objects [CarWithImage]
from an array of cars, [Car]
. That seems to work fine. But I would like the pipeline to check each car object for imageString
, and if it isn't nil fetch it with the function getImage(_:)
. I have commented that code out, because I get the error type of expression is ambiguous without more context
and I don't know how to fix that. I would also like to introduce a delay in the pipeline to more realistically simulate a network download of cars and images, and set the CarWithImage
image property to nil if the image fetching fails.
I have a Xcode Playground repository on GitHub where you can test out my code. First page is with the original class, the second page is with trying out compactMap: Cars Playground
The code will run in Xcode Playground:
import UIKit
import Combine
struct Car {
let name: String
let imageString: String?
}
struct CarWithImage {
let name: String
let image: UIImage?
}
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
// if let imageString = car.imageString {
// getImage(imageString)
// .flatMap { image in
// return Just(CarWithImage(name: car.name, image: image))
// }
// }
return Just(CarWithImage(name: car.name, image: nil))
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String) -> AnyPublisher<UIImage, Error> {
Just(UIImage(systemName: string)!)
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
let carClass = CarClass()
carClass.getVehicles()
.sink(receiveCompletion: { print($0)}) { cars in
cars.forEach { car in
let haveImage = car.image != nil
let string = haveImage ? "and it have an image" : ""
print("The car is", car.name, string)
}
}
// This is just to check that the getImage function works
carClass.getImage("car")
.sink(receiveCompletion: { print($0)}) { image in
print("Got image", image)
}
After suggestion to use compactMap, I have modified the class, but now I only get cars when the car have an image:
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "bolt.car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
self.getImage(car.imageString)
.compactMap { $0 }
.flatMap { image in
return Just(CarWithImage(name: car.name, image: image))
}
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
CodePudding user response:
To simulate the delay you can use the Delay Publisher:
func getImage(_ string: String) -> AnyPublisher<UIImage, Error> {
Just(UIImage())
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.delay(for: .seconds(1), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
To check for a nil imageString you can use the CompactMap Publisher. The tricky part is if you get the image in FlatMap, you also need to pass the Car down the pipeline so that you have access to it's name in order to construct the CarWtihImage. You can use the Zip Publisher for just that.
Note that you need to limit the concurrent number of Publishers with FlatMap or you won't get the delay that you were after.
typealias CarAndImagePublisher = Publishers
.Zip<AnyPublisher<Car, Error>, AnyPublisher<UIImage, Error>>
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.compactMap { car -> Car? in
return car.imageString != nil ? car : nil
}
.flatMap(maxPublishers: .max(1)) { car -> CarAndImagePublisher in
guard let imageString = car.imageString else { fatalError() }
print(imageString)
return Publishers.Zip(
Just(car).setFailureType(to: Error.self).eraseToAnyPublisher(),
getImage(imageString)
)
}
.map { value -> CarWithImage in
print(value.0.name)
return CarWithImage(name: value.0.name, image: value.1)
}
.collect()
.eraseToAnyPublisher()
}
CodePudding user response:
I finally got the code working the way I wanted. It is in Cars4
page of my Playground. If a Car have a property with an image string, the CarWithImage
is populated with the image, else the image property is set to nil. To prove that the cars have image or not, we print out at the end of the Playground the name of the cars. The following shows the working code:
import UIKit
import Combine
struct Car {
let name: String
let imageString: String?
}
struct CarWithImage {
let name: String
let image: UIImage?
}
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "bolt.car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
self.getImage(car.imageString)
.flatMap { image -> Just<CarWithImage> in
guard let image = image else { return Just(CarWithImage(name: car.name, image: nil)) }
return Just(CarWithImage(name: car.name, image: image))
}
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
let carClass = CarClass()
carClass.getVehicles()
.sink(receiveCompletion: { print($0)}) { cars in
cars.forEach { car in
let haveImage = car.image != nil
let string = haveImage ? "and it have an image" : ""
print("The car is", car.name, string)
}
}
CodePudding user response:
Here's an option. This one will emit all the CarWithImage's as a single Publisher containing an Array of objects.
func example(cars: [Car]) -> AnyPublisher<[CarWithImage], Never> {
zip(cars.map { car in
getImageOrNil(car.imageString)
.map { CarWithImage(name: car.name, image: $0) }
})
.eraseToAnyPublisher()
}
func getImageOrNil(_ string: String?) -> AnyPublisher<UIImage?, Never> {
return getImage(string)
.catch { _ in Just(nil).eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
func zip<S, Output, Failure>(_ array: S) -> AnyPublisher<[Output], Failure>
where S: Collection, S.Element: Publisher, S.Element.Output == Output, S.Element.Failure == Failure {
guard let first = array.first else {
return Just([]).setFailureType(to: Failure.self).eraseToAnyPublisher()
}
let rest = array.dropFirst()
return first.map { [$0] }.zip(zip(rest)).map { $0.0 $0.1 }.eraseToAnyPublisher()
}
After posting the above, I looked at your Cars4 code and that helped clean mine up. Using collect
instead of zip
.
func getImages(for cars: [Car]) -> AnyPublisher<[CarWithImage], Never> {
return cars.publisher
.flatMap { car in
getImageOrNil(car.imageString)
.map { CarWithImage(name: car.name, image: $0) }
}
.collect()
.eraseToAnyPublisher()
}