Search code examples
uiimageswift4drawingcore-graphicsios12

iOS Swift how to incrementally draw to a UIImage?


How can I use iOS core graphics to incrementally draw a large data set in a single image?

I have code which is ready to process the entire dataset at once (over 100,000 rectangles) and produces a single image. This is a very long running operation and I want this dataset to be incrementally drawn 1000 rectangles at a time, displaying these small image updates (like images downloaded from internet in the 90s)

My questions are: Would I keep the reference to the same context throughout the operation and simply add elements to it? - OR - Should I be capturing the current image using UIGraphicsGetImageFromCurrentImageContext() , then drawing it in a new context and drawing additional rectangles on top of it?

Bonus question - is this the right approach if I want to use multiple threads to append to the same image?

   let context = UIGraphicsGetCurrentContext()!

    context.setStrokeColor(borderColor)
    context.setLineWidth(CGFloat(borderWidth))
    for elementIndex in 0 ..< data.count {

        context.setFillColor(color.cgColor)
        let marker = CGRect(x: toX(elementIndex),
                            y: toY(elementIndex),
                            width: rectWidth,
                            height: rectHeight)

        context.addRect(marker)
        context.drawPath(using: .fillStroke)
    }
    // Save the context as a new UIImage
    let myImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    if let cgImage = myImage?.cgImage,
        let orientation = myImage?.imageOrientation {

        return UIImage(cgImage: cgImage, scale: 2, orientation: orientation)
    }

Solution

  • You should:

    • Dispatch the whole thing to some background queue;
    • periodically call UIGraphicsGetImageFromCurrentImageContext and dispatch the image view update to the main queue

    E.g., this will update the image view every ¼ second:

    DispatchQueue.global().async {
        var lastDrawn = CACurrentMediaTime()
    
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
    
        for _ in 0 ..< 100_000 {
            // draw whatever you want
    
            let now = CACurrentMediaTime()
            if now - lastDrawn > 0.25 {
                self.updateImageView()
                lastDrawn = now
            }
        }
    
        self.updateImageView()
    
        UIGraphicsEndImageContext()
    }
    

    Where:

    func updateImageView() {
        guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return }
    
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
    

    Thus:

    func buildImage(of size: CGSize) {
        DispatchQueue.global().async {
            var lastDrawn = CACurrentMediaTime()
    
            UIGraphicsBeginImageContextWithOptions(size, false, 0)
    
            for _ in 0 ..< 100_000 {
                self.someColor().setFill()
                UIBezierPath(rect: self.someRectangle(in: size)).fill()
    
                let now = CACurrentMediaTime()
                if now - lastDrawn > 0.25 {
                    self.updateImageView()
                    lastDrawn = now
                }
            }
    
            self.updateImageView()
    
            UIGraphicsEndImageContext()
        }
    }
    
    func updateImageView() {
        let image = UIGraphicsGetImageFromCurrentImageContext()
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
    
    func someRectangle(in size: CGSize) -> CGRect {
        let x = CGFloat.random(in: 0...size.width)
        let y = CGFloat.random(in: 0...size.height)
        let width = CGFloat.random(in: 0...(size.width - x))
        let height = CGFloat.random(in: 0...(size.height - y))
    
        return CGRect(x: x, y: y, width: width, height: height)
    }
    
    func someColor() -> UIColor {
        return UIColor(red: .random(in: 0...1),
                       green: .random(in: 0...1),
                       blue: .random(in: 0...1),
                       alpha: 1)
    }
    

    Yields:

    enter image description here

    Now, I’m not calling CoreGraphics directly, but you can and it will work the same.