【问题标题】:Combine pipeline for object with image将对象的管道与图像相结合
【发布时间】:2021-10-03 07:24:40
【问题描述】:

我在我的 Xcode Playground 中构建了一个简化的组合管道,用于从一系列汽车 [Car] 中生成汽车对象 [CarWithImage]。这似乎工作正常。但我希望管道检查每个汽车对象的imageString,如果不是零,则使用函数getImage(_:) 获取它。我已将该代码注释掉,因为我收到错误 type of expression is ambiguous without more context 并且我不知道如何解决它。我还想在管道中引入延迟,以更真实地模拟汽车和图像的网络下载,如果图像获取失败,则将 CarWithImage 图像属性设置为 nil。

我在 GitHub 上有一个 Xcode Playground 存储库,您可以在其中测试我的代码。第一页是原始类,第二页是试用 compactMap:Cars Playground

代码将在 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)
    }

在建议使用compactMap后,我修改了类,但现在我只在汽车有图像时才得到汽车:

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

【问题讨论】:

标签: swift combine


【解决方案1】:

要模拟延迟,您可以使用延迟发布器:

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

要检查 nil imageString,您可以使用 CompactMap Publisher。棘手的部分是,如果您在 FlatMap 中获取图像,您还需要将 Car 传递到管道中,以便您可以访问它的名称以构造 CarWtihImage。您可以为此使用 Zip Publisher。

请注意,您需要使用 FlatMap 限制并发发布者的数量,否则您将不会得到您想要的延迟。

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

【讨论】:

  • 那很接近,但我仍然只得到一辆车(特斯拉)而不是沃尔沃,因为沃尔沃没有图像。我希望将两辆车都打印出来,即使其中一辆没有图像。我已将更新推送到我的 GitHub Playground,其中 Cars3 页面包含您的代码
  • 啊,我明白了。那么您当前的 getImage 方法将不够用。不知何故,您将不得不将 Optional 推送到下游而不是 UIImage。在这一点上,我很难理解为什么你会想要这样的东西,而不是为了满足一些好奇心。这似乎不切实际。
  • 这个想法是从 API 模拟下载汽车,并在下载的汽车的图像属性不为零时填充图像属性
  • Playground 上的 Cars2 页面在 getImage 函数中有一个可选的 UIImage 发布者
【解决方案2】:

我终于让代码按我想要的方式工作了。它在我的 Playground 的 Cars4 页面中。如果 Car 具有带有图像字符串的属性,则使用图像填充 CarWithImage,否则将图像属性设置为 nil。为了证明汽车是否有图像,我们在 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: "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)
        }
    }

【讨论】:

    【解决方案3】:

    这是一个选项。这会将所有 CarWithImage 作为包含对象数组的单个 Publisher 发出。

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

    在发布上述内容后,我查看了您的 Cars4 代码,这有助于清理我的代码。使用collect 而不是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()
    }
    

    【讨论】:

      猜你喜欢
      • 2020-10-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-15
      • 2012-08-29
      • 1970-01-01
      • 2012-08-14
      • 1970-01-01
      • 2018-11-09
      相关资源
      最近更新 更多