Search code examples
swiftrx-swift

RxSwift increase perfomance


This RxSwift code is running pretty slow, so can you, please, give me an advise on how to increase it performance?

This function is taking image resources and mapping them into redrawed images

func redrawChapter(pages: [Resource]) -> Single<[UIImage]> {
     return Observable
        .from(pages)
        .flatMap(getImage)
        // MARK: Detect synopsys
        .flatMap(recognizeText)
        // MARK: Translate synopsys
        .concatMap(translate)
        // MARK: Image redraw
        .concatMap(redrawImage)
        .toArray()
}

Im using kingfisher to download images

private func getImage(resource: Resource) -> Single<UIImage> {
    return Single.create { single in
        let disposables = Disposables.create()
        
        KingfisherManager.shared.retrieveImage(with: resource) { result in
            switch result {
            case .success(let value):
                single(.success(value.image))
            case .failure(let error):
                single(.failure(error))
            }
        }
        return disposables
    }
}

Im using apple vision to recognise text regions

private func recognizeText(image: UIImage) -> Single<(UIImage, [Synopsis])> {
    return Observable.of(image)
        .flatMap {
            Observable.combineLatest(
                Observable.just($0),
                // MARK: Apple vision to detect text regions
                self.imageProcessor.getRecognizedText(image: $0).asObservable()
            )
        }
        .asSingle()
}

And Moya for networking

private func translate(image: UIImage, synopsys: [Synopsis]) -> Maybe<(UIImage, [Synopsis])>{
    return Observable.from(synopsys)
        .flatMap {
            Observable.combineLatest(
                Observable.just($0.rect),
                self.translator.translate(text: $0.text).asObservable()
            )
        }
        .compactMap {
            return Synopsis(
                text: $0.1,
                rect: $0.0
            )
        }
        .toArray()
        .compactMap {
            return (image, $0)
        }
}

Image redraws using UIGraphics renderer

private func redrawImage(
    image: UIImage,
    synopsys: [Synopsis]
) -> Single<UIImage> {
    return Single.create { single in
        
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        
        guard let cgImage = image.cgImage else {
            return Disposables.create()
        }
        
        let size = image.size
        
        let bounds = CGRect(
            origin: .zero,
            size: size
        )
        
            let final = UIGraphicsImageRenderer(
                bounds: bounds,
                format: format
            ).image { context in
                
                image.draw(in: bounds)
                
                for syn in synopsys {
                    
                    let backgroundColor = cgImage.averageColorOf(rect: syn.rect)
                    let textColor = backgroundColor.textColor()
                    
                    self.setupLabel(
                        text: syn.text,
                        backgroundColor: backgroundColor,
                        textColor: textColor,
                        bounds: syn.rect,
                        context: context.cgContext
                    )
                }
            }
            single(.success(final))
        
        return Disposables.create()
    }
}

I feel like there is another way to not to make this huge amount of observables, to make it work, and maybe somehow it will raise this functions performance. Appreciate you for help.


Solution

  • Here's an updated solution. Try it out and see how it goes:

    func redrawChapter(pages: [Resource]) -> Single<[UIImage]> {
        let image = Observable.from(pages)
            .flatMap(getImage(resource:))
    
        let translatedSynopses = image
            .flatMap(imageProcessor.getRecognizedText(image:))
            .flatMap { Single.zip($0.map(translate(synopses:))) }
    
        return Observable.zip(image, translatedSynopses)
            .observe(on: MainScheduler.instance) // if you need it, you may not.
            .compactMap(redrawImage(image:synopses:))
            .toArray()
    }
    
    func getImage(resource: Resource) -> Single<UIImage> {
        Single.create { observer in
            KingfisherManager.shared.retrieveImage(with: resource, completion: observer)
            return Disposables.create()
        }
        .map(\.image)
    }
    
    func translate(synopses: Synopsis) -> Single<Synopsis> {
        translator.translate(text: synopses.text)
            .map { Synopsis(text: $0, rect: synopses.rect) }
    }
    
    func redrawImage(image: UIImage, synopses: [Synopsis]) -> UIImage? {
        guard let cgImage = image.cgImage else { return nil }
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        let size = image.size
        let bounds = CGRect(origin: .zero, size: size)
    
        return UIGraphicsImageRenderer(bounds: bounds, format: format).image { context in
            image.draw(in: bounds)
            for syn in synopses {
                let backgroundColor = cgImage.averageColorOf(rect: syn.rect)
                let textColor = backgroundColor.textColor()
                setupLabel(
                    text: syn.text,
                    backgroundColor: backgroundColor,
                    textColor: textColor,
                    bounds: syn.rect,
                    context: context.cgContext
                )
            }
        }
    }
    

    What likely makes this faster? Because I removed the concatMaps it will perform multiple translations at once (I assume this is a network request.) As the translations for an image come in, it will process that image.

    One thing about this code (and yours) is that the image array emitted may be in a different order than the resources passed in. If that's going to be a problem, then adjustments will have to be made.