4
\$\begingroup\$

I have made a helper struct in Swift that creates circular icons from a PNG image (a white icon on transparent background) and a background UIColor. These variables are user-definable in my app, and the created icons are used in notifications and Spotlight, for example.

The PNG images are named iconA.png, iconB.png etc. and are all a standard 100-points square. The default image is a fully transparent one called iconEmpty.png. I wanted to provide a CGSize argument so that I could use this method for other purposes as well, should the need arise.

It works as expected, but I am wondering how it might be improved or made more efficient, as I am not very experienced with drawing contexts.

import UIKit

struct IconMaker {

    func makeIcon(with color: UIColor, size: CGSize, icon: UIImage = UIImage(named: "iconEmpty.png")!) -> UIImage {

        // Make solid color background
        let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
        color.setFill()
        UIRectFill(rect)
        let backgroundImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        // Add other image (which has transparency)
        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
        backgroundImage.draw(in: rect, blendMode: .normal, alpha: 1)

        icon.draw(in: rect, blendMode: .normal, alpha: 1)

        let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        // Circular mask
        let cornerRadius = newImage.size.height/2
        UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)
        let bounds = CGRect(origin: CGPoint.zero, size: size)
        UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
        newImage.draw(in: bounds)
        let finalImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return finalImage!
    }
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)

can be simplified to

let rect = CGRect(origin: .zero, size: size)

The explicit type annotation in

let backgroundImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!

is not needed. In

UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale)

one can pass 0.0 as the last argument, that is also interpreted as the scale factor of the device’s main screen.

let cornerRadius = newImage.size.height/2
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()

can be simplified to

UIBezierPath(ovalIn: bounds).addClip()

(and that would work even for a non-quadratic frame).

Instead of creating and drawing into a bitmap-based graphics context three times, with two intermediate images, it suffices to use a single graphics context, and

  • Add a round clipping path,
  • fill with the given background color,
  • draw the given image,

all with the same context.

Instead of the fully transparent default icon "iconEmpty.png" you could make that parameter optional, and draw the icon only if an icon was provided by the caller.

Finally, the method does not access any properties of the IconMaker type. You can make it static, so that it can be called without creating an IconMaker instance:

let newIcon = IconMaker.makeIcon(...)

Putting it all together, the method could look like this:

static func makeIcon(with color: UIColor, size: CGSize, icon: UIImage? = nil) -> UIImage {

    // Create graphics context:
    let rect = CGRect(origin: .zero, size: size)
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)

    // Circular mask
    UIBezierPath(ovalIn: rect).addClip()

    // Make solid color background:
    color.setFill()
    UIRectFill(rect)

    if let icon = icon {
        // Add other image (which has transparency)
        icon.draw(in: rect, blendMode: .normal, alpha: 1.0)
    }

    // Create new image and release context:
    let newImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return newImage
}

Another option is to use the defer statement for releasing the graphics context:

static func makeIcon(with color: UIColor, size: CGSize, icon: UIImage? = nil) -> UIImage {

    // Create graphics context:
    let rect = CGRect(origin: .zero, size: size)
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
    defer { UIGraphicsEndImageContext() }

    // ...

    return UIGraphicsGetImageFromCurrentImageContext()!
}

The block passed to the defer statement is executed just before the function returns. Here it allows to get rid of the newImage variable. Generally it is a good approach to ensure that all acquired resources are released properly, no matter how program control leaves the function.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks so much for this. Your systematic approach is very easy to follow and there are loads of good ideas here. I particularly like using only one image context and making it a static method. \$\endgroup\$
    – Chris
    Jul 6, 2018 at 20:15
  • \$\begingroup\$ I never thought to make the icon optional, so that a transparent PNG wasn’t even necessary. Also, I’ve just spotted that my title calls this a helper class when it is in fact a struct! \$\endgroup\$
    – Chris
    Jul 6, 2018 at 20:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.