Search code examples
iosswiftuikitcore-image

Why does UIGraphicsGetCurrentContext return nil after UIGraphicsBeginImageContext


I am following a code example to make a blurred UILabel, https://stackoverflow.com/a/62224908/2226315.

My requirement is to make the label on blur after label initialization instead of calling the blur method at runtime. However, when I try to call blur after label gets initialized the value returned from UIGraphicsGetCurrentContext is nil hence having a "Fatal error: Unexpectedly found nil while unwrapping an Optional value"

UIGraphicsBeginImageContext(bounds.size)
print("DEBUG: bounds.size", bounds.size)
self.layer.render(in: UIGraphicsGetCurrentContext()!) // <- return nil
var image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
print("DEBUG: image image", image)

I tried adding the code in all the following places individually, the context can be fetched now however it does not generate the blur effect as expected.

override func layoutSubviews() {
    super.layoutSubviews()
    self.blur()
}

// OR
    
override func draw(_ rect: CGRect) {
    super.draw(rect)
    self.blur()
}

Full code snippet,

class BlurredLabel: UILabel {

    func blur(_ blurRadius: Double = 2.5) {        
        let blurredImage = getBlurryImage(blurRadius)
        let blurredImageView = UIImageView(image: blurredImage)
        blurredImageView.translatesAutoresizingMaskIntoConstraints = false
        blurredImageView.tag = 100
        blurredImageView.contentMode = .center
        blurredImageView.backgroundColor = .white
        addSubview(blurredImageView)
        NSLayoutConstraint.activate([
            blurredImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            blurredImageView.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }

    func unblur() {
        subviews.forEach { subview in
            if subview.tag == 100 {
                subview.removeFromSuperview()
            }
        }
    }

    private func getBlurryImage(_ blurRadius: Double = 2.5) -> UIImage? {
        UIGraphicsBeginImageContext(bounds.size)
        layer.render(in: UIGraphicsGetCurrentContext()!)
        guard let image = UIGraphicsGetImageFromCurrentImageContext(),
            let blurFilter = CIFilter(name: "CIGaussianBlur") else {
            UIGraphicsEndImageContext()
            return nil
        }
        UIGraphicsEndImageContext()

        blurFilter.setDefaults()

        blurFilter.setValue(CIImage(image: image), forKey: kCIInputImageKey)
        blurFilter.setValue(blurRadius, forKey: kCIInputRadiusKey)

        var convertedImage: UIImage?
        let context = CIContext(options: nil)
        if let blurOutputImage = blurFilter.outputImage,
            let cgImage = context.createCGImage(blurOutputImage, from: blurOutputImage.extent) {
            convertedImage = UIImage(cgImage: cgImage)
        }

        return convertedImage
    }
}

REFERENCE

UPDATE

Usage based on "Eugene Dudnyk" answer


definitionLabel = BlurredLabel()
definitionLabel.numberOfLines = 0
definitionLabel.lineBreakMode = .byWordWrapping
definitionLabel.textColor = UIColor(named: "text")
definitionLabel.text = "Lorem Ipsum is simply dummy text"
definitionLabel.clipsToBounds = false
definitionLabel.isBluring = true

Solution

  • Here is a better solution - instead of retrieving the blurred image, just let the label blur itself.

    When you need it to be blurred, set label.isBlurring = true. Also, this solution is better for performance, because it reuses the same context and does not need the image view.

    @IBDesignable
    class BlurredLabel: UILabel {
        @IBInspectable
        public var isBlurring: Bool = false {
            didSet {
                setNeedsDisplay()
            }
        }
    
        @IBInspectable
        public var blurRadius: Double = 2.5 {
            didSet {
                blurFilter?.setValue(blurRadius, forKey: kCIInputRadiusKey)
            }
        }
    
        lazy var blurFilter: CIFilter? = {
            let blurFilter = CIFilter(name: "CIGaussianBlur")
            blurFilter?.setDefaults()
            blurFilter?.setValue(blurRadius, forKey: kCIInputRadiusKey)
            return blurFilter
        }()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            configure()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            configure()
        }
    
        func configure() {
            layer.isOpaque = false
            layer.needsDisplayOnBoundsChange = true
            layer.contentsScale = UIScreen.main.scale
            layer.contentsGravity = .center
            isOpaque = false
            isUserInteractionEnabled = false
            contentMode = .redraw
        }
    
        override func display(_ layer: CALayer) {
            let bounds = layer.bounds
            guard !bounds.isEmpty && bounds.size.width < CGFloat(UINT16_MAX) else {
                layer.contents = nil
                return
            }
            UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, layer.contentsScale)
            if let ctx = UIGraphicsGetCurrentContext() {
                self.layer.draw(in: ctx)
            
                var image = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
                if isBlurring, let cgImage = image {
                    blurFilter?.setValue(CIImage(cgImage: cgImage), forKey: kCIInputImageKey)
                    let ciContext = CIContext(cgContext: ctx, options: nil)
                    if let blurOutputImage = blurFilter?.outputImage,
                       let cgImage = ciContext.createCGImage(blurOutputImage, from: blurOutputImage.extent) {
                        image = cgImage
                    }
                }
                layer.contents = image
            }
            UIGraphicsEndImageContext()
        }
    }