Search code examples
iosswiftuikitcore-graphicscore-animation

Blurry CALayer When Using PDF Vector Image as Content


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:

enter image description here


Solution

  • 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:

    scaled image