4

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
XY L
  • 25,431
  • 14
  • 84
  • 143
  • 1
    Did you check that `CGRectIsEmpty(bounds) == false`? – Eugene Dudnyk Oct 05 '20 at 01:35
  • A simpler approach might be to add an UIVisualEffectView as a subview of your label and just hide it whenever you need. You can attatch an UIBlurEffect to the UIVisualEffectView to get the desired effect – Pastre Oct 05 '20 at 01:36
  • 1
    @Pastre I tried to put the label behind a UIVisualEffectView however it does not look as good as a blurred created via CoreImage. – XY L Oct 05 '20 at 02:21
  • @EugeneDudnyk i just checked the bounds and it is empty after the label initialization. can you share with me if what's the right place to get the context? – XY L Oct 05 '20 at 02:25
  • You shouldn't even draw anything if the bounds are empty. Label takes no space on the screen in this case, and it will get the non-empty bounds later. Just don't create the context until it gets the bounds. – Eugene Dudnyk Oct 05 '20 at 02:28
  • @EugeneDudnyk thanks for your response, can you share with me how can I know when the view will get its bounds, so I can call `blur` there. – XY L Oct 05 '20 at 02:35
  • @Metropolis maybe override the `layoutSubviews()` method and there call `blur()` – Pastre Oct 05 '20 at 03:00
  • @Pastre it causes an infinity loop by adding `self.blur()` inside `layoutSubviews` – XY L Oct 05 '20 at 03:07
  • @Metropolis In your view controller, you can override `viewDidLayoutSubviews` and there call `view.blur()` – Pastre Oct 05 '20 at 03:18

2 Answers2

9

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.

class BlurredLabel: UILabel {
    
    var isBlurring = false {
        didSet {
            setNeedsDisplay()
        }
    }

    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)
        layer.isOpaque = false
        layer.needsDisplayOnBoundsChange = true
        layer.contentsScale = UIScreen.main.scale
        layer.contentsGravity = .center
        isOpaque = false
        isUserInteractionEnabled = false
        contentMode = .redraw
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    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()
    }
}
Eugene Dudnyk
  • 5,553
  • 1
  • 23
  • 48
  • Sir, can you have any suggestions for UITextView for the above code? – Kishan Bhatiya Oct 20 '20 at 05:19
  • @KishanBhatiya if you use text view just to show links, you can use BlurredLabel instead, and set `attributedText` to it like it is described here https://stackoverflow.com/questions/1256887/create-tap-able-links-in-the-nsattributedstring-of-a-uilabel – Eugene Dudnyk Oct 20 '20 at 09:19
  • thank you for the quick response, Using text view I'm not showing any link but attributed string. how can we do this with the text view? you can [check this](https://stackoverflow.com/questions/64428968/blur-uitextviews-text) – Kishan Bhatiya Oct 20 '20 at 13:14
  • @KishanBhatiya Text view uses private sub view to display text, and that’s the view that should be blurred in your scenario. I don’t think you can achieve it without using private apis – Eugene Dudnyk Oct 20 '20 at 13:54
  • Using `VisualEffectView()` i can but the whole text view, not text view's text and it also shows edges as you can see in my question mentioned above in the comment. Have you any suggestions or thoughts for `VisualEffectView()` then it will be helpful – Kishan Bhatiya Oct 20 '20 at 13:58
  • @KishanBhatiya no, I can’t help here because it requires research of the possibilities or building text view from scratch – Eugene Dudnyk Oct 20 '20 at 15:12
  • Exactly what I was looking for. Thank you @EugeneDudnyk – tuttu47 Mar 16 '22 at 09:49
  • How do you unblur at runtime in this case? – yaozhang Oct 30 '22 at 16:10
  • @yaozhang Set `label.isBlurring = false` – Eugene Dudnyk Oct 30 '22 at 20:47
0

Transformed @EugeneDudnyk answer to UIView extension so it can be used also with TextView.

extension UIView {
    struct BlurableKey {
        static var blurable = "blurable"
    }
    
    func blur(radius: CGFloat) {
        guard superview != nil else { return }
        
        UIGraphicsBeginImageContextWithOptions(CGSize(width: frame.width, height: frame.height), false, 1)
        layer.render(in: UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard
            let blur = CIFilter(name: "CIGaussianBlur"),
            let image = image
        else {
            return
        }
  
        blur.setValue(CIImage(image: image), forKey: kCIInputImageKey)
        blur.setValue(radius, forKey: kCIInputRadiusKey)
        
        let ciContext  = CIContext(options: nil)
        let boundingRect = CGRect(
            x:0,
            y: 0,
            width: frame.width,
            height: frame.height
        )
        
        guard
            let result = blur.value(forKey: kCIOutputImageKey) as? CIImage,
            let cgImage = ciContext.createCGImage(result, from: boundingRect)
        else {
            return
        }
                
        let blurOverlay = UIImageView()
        blurOverlay.frame = boundingRect
        blurOverlay.image = UIImage(cgImage: cgImage)
        blurOverlay.contentMode = .left
        
        addSubview(blurOverlay)
     
        objc_setAssociatedObject(
            self,
            &BlurableKey.blurable,
            blurOverlay,
            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
        )
    }
    
    func unBlur() {
        guard
            let blurOverlay = objc_getAssociatedObject(self, &BlurableKey.blurable) as? UIImageView
        else {
            return
        }
        
        blurOverlay.removeFromSuperview()
        
        objc_setAssociatedObject(
            self,
            &BlurableKey.blurable,
            nil,
            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
        )
    }
    
    var isBlurred: Bool {
        return objc_getAssociatedObject(self, &BlurableKey.blurable) is UIImageView
    } 
}
Shalugin
  • 1,092
  • 2
  • 10
  • 15