Home > Net >  Combine pipeline for object with image
Combine pipeline for object with image

Time:10-04

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()
}
  • Related