iosswiftuiimagescaleuigraphicscontext

How to apply scale when drawing and composing UIImage


I have the following functions.

extension UIImage
{
    var width: CGFloat
    {
        return size.width
    }
    
    var height: CGFloat
    {
        return size.height
    }
    
    private static func circularImage(diameter: CGFloat, color: UIColor) -> UIImage
    {
        UIGraphicsBeginImageContextWithOptions(CGSize(width: diameter, height: diameter), false, 0)
        let context = UIGraphicsGetCurrentContext()!
        context.saveGState()

        let rect = CGRect(x: 0, y: 0, width: diameter, height: diameter)
        context.setFillColor(color.cgColor)
        context.fillEllipse(in: rect)

        context.restoreGState()
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        return image
    }
    
    private func addCentered(image: UIImage, tintColor: UIColor) -> UIImage
    {            
        let topImage = image.withTintColor(tintColor, renderingMode: .alwaysTemplate)
        let bottomImage = self
        
        UIGraphicsBeginImageContext(size)
        
        let bottomRect = CGRect(x: 0, y: 0, width: bottomImage.width, height: bottomImage.height)
        bottomImage.draw(in: bottomRect)
        
        let topRect = CGRect(x: (bottomImage.width - topImage.width) / 2.0,
                             y: (bottomImage.height - topImage.height) / 2.0,
                             width: topImage.width,
                             height: topImage.height)
        topImage.draw(in: topRect, blendMode: .normal, alpha: 1.0)
        
        let mergedImage = UIGraphicsGetImageFromCurrentImageContext()!
        
        UIGraphicsEndImageContext()
        
        return mergedImage
    }
}

They work fine, but how do I properly apply UIScreen.main.scale to support retina screens?

I've looked at what's been done here but can't figure it out yet.

Any ideas?


Solution

  • Accessing UIScreen.main.scale itself is a bit problematic, as you have to access it only from main thread (while you usually want to put a heavier image processing on a background thread). So I suggest one of these ways instead.

    First of all, you can replace UIGraphicsBeginImageContext(size) with

    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    

    The last argument (0.0) is a scale, and based on docs "if you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen."

    If instead you want to retain original image's scale on resulting UIImage, you can do this: after topImage.draw, instead of getting the UIImage with UIGraphicsGetImageFromCurrentImageContext, get CGImage with

    let cgImage = context.makeImage()
    

    and then construct UIImage with the scale and orientation of the original image (as opposed to defaults)

    let mergedImage = UIImage(
        cgImage: cgImage, 
        scale: image.scale, 
        orientation: image.opientation)