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!
}
}
1 Answer 1
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.
-
\$\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– Chris2018年07月06日 20:15:36 +00:00Commented 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– Chris2018年07月06日 20:16:57 +00:00Commented Jul 6, 2018 at 20:16