为什么在调用UIGraphicsBeginImageContext后,UIGraphicsGetCurrentContext返回nil?

4

我正在跟随一个代码示例来制作模糊的UILabel,https://dev59.com/2FoV5IYBdhLWcg3wc-jo#62224908

我的要求是在标签初始化后使标签模糊,而不是在运行时调用blur方法。然而,当我尝试在标签初始化后调用blur时,从UIGraphicsGetCurrentContext返回的值为nil,因此出现了"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)

我尝试分别将代码添加到以下所有位置,现在可以提取上下文,但是它没有生成预期的模糊效果。

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

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

完整的代码片段:

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
    }
}

参考资料

更新

使用基于Eugene Dudnyk的答案。


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

1
你有检查过 CGRectIsEmpty(bounds) == false 吗? - Eugene Dudnyk
一个更简单的方法可能是将UIVisualEffectView作为标签的子视图添加,并在需要时隐藏它。您可以将UIBlurEffect附加到UIVisualEffectView上以获得所需的效果。 - Pastre
1
@Pastre 我尝试将标签放在UIVisualEffectView后面,但它看起来不如通过CoreImage创建的模糊效果好。 - XY Li
如果边界为空,您甚至不应该绘制任何内容。在这种情况下,标签在屏幕上不占用空间,并且稍后会获得非空边界。只需在获取边界之前不创建上下文即可。 - Eugene Dudnyk
@Metropolis 在你的视图控制器中,你可以重写 viewDidLayoutSubviews 并在其中调用 view.blur() - Pastre
显示剩余4条评论
2个回答

9
这里有一个更好的解决方案 - 而不是获取模糊图像,只需让标签自动模糊。
当您需要它模糊时,请设置。 此外,这种解决方案对性能更好,因为它重用相同的上下文并且不需要图像视图。
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()
    }
}

先生,您对上面的代码有任何UITextView的建议吗? - Kishan Bhatiya
@KishanBhatiya 如果你只是用TextView来显示链接,那么你可以使用BlurredLabel,然后像这里所描述的那样将attributedText设置为它 https://dev59.com/lHM_5IYBdhLWcg3wt1rD - Eugene Dudnyk
正是我所需要的。谢谢@EugeneDudnyk - tuttu47
在这种情况下,如何在运行时取消模糊? - yaozhang
@yaozhang 设置 label.isBlurring = false - Eugene Dudnyk
显示剩余4条评论

0

将@EugeneDudnyk的答案转换为UIView扩展,以便也可与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
    } 
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接