I'm trying to use a PDF vector image as the contents of a CALayer
, but when it is scaled above it's initial size of 15x13, it looks very blurry. I have 'Preserve Vector Data' turned on in my asset catalog for the image in question. Here is the code for my view, which draws an outer circle on one layer, and uses a second layer to display an image of a checkmark in the center of the view if the isComplete
property is set to true
.
@IBDesignable
public class GoalCheckView: UIView {
// MARK: - Public properties
@IBInspectable public var isComplete: Bool = false {
didSet {
setNeedsLayout()
}
}
// MARK: - Private properties
private lazy var checkImage: UIImage? = {
let bundle = Bundle(for: type(of: self))
return UIImage(named: "check_event_carblog_confirm", in: bundle, compatibleWith: nil)
}()
private var checkImageSize: CGSize {
let widthRatio: CGFloat = 15 / 24 // Size of image is 15x13 when circle is 24x24
let heightRatio: CGFloat = 13 / 24
return CGSize(width: bounds.width * widthRatio, height: bounds.height * heightRatio)
}
private let circleLayer = CAShapeLayer()
private let checkLayer = CALayer()
private let lineWidth: CGFloat = 1
// MARK: - View lifecycle
public override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2))
circleLayer.path = path.cgPath
// Layout check
checkLayer.frame = CGRect(
origin: CGPoint(x: bounds.midX - checkImageSize.width / 2, y: bounds.midY - checkImageSize.height / 2),
size: checkImageSize
)
checkLayer.opacity = isComplete ? 1 : 0
}
// MARK: - Private methods
private func setupView() {
// Setup circle layer
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor(named: "goal_empty", in: bundle, compatibleWith: nil)?.cgColor
layer.addSublayer(circleLayer)
// Setup check layer
checkLayer.contentsScale = UIScreen.main.scale
checkLayer.contentsGravity = .resizeAspect
checkLayer.contents = checkImage?.cgImage
layer.addSublayer(checkLayer)
}
}
This code results in the following display if I set the size of the view to 240x240:
I was able to create a workaround for this. I can check the expected size of my image in layoutSubviews
, and if it does not match the size of the UIImage
I can use a UIGraphicsImageRenderer
to create a new image that is scaled to the correct size. I created an extension of UIImage
to facilitate this:
extension UIImage {
internal func imageScaled(toSize scaledSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: scaledSize)
let newImage = renderer.image { [unowned self] _ in
self.draw(in: CGRect(origin: .zero, size: scaledSize))
}
return newImage
}
}
Now, my updated layoutSubviews
method looks like this:
public override func layoutSubviews() {
super.layoutSubviews()
// Layout circle
let path = UIBezierPath(ovalIn: bounds.insetBy(dx: lineWidth.mid, dy: lineWidth.mid))
circleLayer.path = path.cgPath
// Layout check
if let checkImage = checkImage, checkImage.size != checkImageSize {
checkLayer.contents = checkImage.imageScaled(toSize: checkImageSize).cgImage
}
let checkOrigin = CGPoint(x: bounds.midX - checkImageSize.midW, y: bounds.midY - checkImageSize.midH)
checkLayer.frame = CGRect(origin: checkOrigin, size: checkImageSize)
}
This results in a nice crisp image: