QR Images not rendered on Widget after iOS 17

114 Views Asked by At

I had extension methods which I used to transform QR codes into images. Everything was working flawlessly until iOS 17 update. They are not rendered now at all.

Extension that I was using before iOS 17.

extension UIImage {
    convenience init?(qrcode string: String) {
        if let outputCIImage = CIImage.make(qrcode: string) {
            let context = CIContext()
            guard let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else { return nil }
            self.init(cgImage: cgImage)
            return
        }
        return nil
    }
        
    convenience init?(qrcode string: String, using color: UIColor) {
        if let outputCIImage = CIImage.make(qrcode: string)?.tinted(using: color) {
            let context = CIContext()
            guard let cgImage = context.createCGImage(outputCIImage, from: outputCIImage.extent) else { return nil }
            self.init(cgImage: cgImage)
            return
        }
        return nil
    }
}
    
extension CIImage {
    static func make(qrcode string: String) -> CIImage? {
        let data = string.data(using: .ascii)
        if let filter = CIFilter(name: "CIQRCodeGenerator") {
            filter.setValue(data, forKey: "inputMessage")
            filter.setValue("Q", forKey: "inputCorrectionLevel")
            let transform = CGAffineTransform(scaleX: 12, y: 12)
            return filter.outputImage?.transformed(by: transform)
        }
        return nil
    }
        
    var transparent: CIImage? {
        inverted?.blackTransparent
    }
        
    var inverted: CIImage? {
        guard let invertedColorFilter = CIFilter(name: "CIColorInvert") else { return nil }
            
        invertedColorFilter.setValue(self, forKey: "inputImage")
        return invertedColorFilter.outputImage
    }
        
    var blackTransparent: CIImage? {
        guard let blackTransparentFilter = CIFilter(name: "CIMaskToAlpha") else { return nil }
        blackTransparentFilter.setValue(self, forKey: "inputImage")
        return blackTransparentFilter.outputImage
    }
        
    func tinted(using color: UIColor) -> CIImage? {
        guard
            let transparentQRImage = transparent,
            let filter = CIFilter(name: "CIMultiplyCompositing"),
            let colorFilter = CIFilter(name: "CIConstantColorGenerator") else { return nil }
            
        let ciColor = CIColor(color: color)
        colorFilter.setValue(ciColor, forKey: kCIInputColorKey)
        let colorImage = colorFilter.outputImage
            
        filter.setValue(colorImage, forKey: kCIInputImageKey)
        filter.setValue(transparentQRImage, forKey: kCIInputBackgroundImageKey)
            
        return filter.outputImage
    }
        
    func combined(with image: CIImage) -> CIImage? {
        guard let combinedFilter = CIFilter(name: "CISourceOverCompositing") else { return nil }
        let centerTransform = CGAffineTransform(translationX: extent.midX - (image.extent.size.width / 2), y: extent.midY - (image.extent.size.height / 2))
        combinedFilter.setValue(image.transformed(by: centerTransform), forKey: "inputImage")
        combinedFilter.setValue(self, forKey: "inputBackgroundImage")
        return combinedFilter.outputImage!
    }
}

After some research I found this extension method

extension UIImage {
    static func make(qrcode string: String) -> UIImage? {
        guard let data = string.data(using: String.Encoding.ascii) else { return nil }
        var uiImage: UIImage?
        if let filter = CIFilter(
            name: "CIQRCodeGenerator",
            parameters: ["inputMessage": data, "inputCorrectionLevel": "Q"]
        ) {
            guard
                let outputImage = filter.outputImage,
                let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent)
            else {
                return nil
            }
            let scaleFactor: CGFloat = 12
            let size = CGSize(
                width: outputImage.extent.width * scaleFactor,
                height: outputImage.extent.height * scaleFactor
            )
            UIGraphicsBeginImageContext(size)
            if let context = UIGraphicsGetCurrentContext() {
                context.interpolationQuality = .none
                context.draw(cgImage, in: CGRect(origin: .zero, size: size))
                uiImage = UIGraphicsGetImageFromCurrentImageContext()
            }
            UIGraphicsEndImageContext()
        }
        return uiImage
    }
}

I've managed to make it work now, but I'm unable to make its background transparent. I believe this is impossible without transforming UIImage to CIImage. However, when I do that, the image won't render. What could be the issue?

1

There are 1 best solutions below

1
Jayant Badlani On

This is the updated code to achieve the desired high-quality, transparent background QR code image in iOS 17, hope it solves your query.

iOS 17 device screenshot

Updated Code:

extension UIImage {
    
    static func make(qrcode string: String, tint color: UIColor) -> UIImage? {
        guard let qrCIImage = string.qrCIImage else { return nil }
        
        if let tintedCIImage = qrCIImage.tinted(using: color),
           let cgImage = CIContext().createCGImage(tintedCIImage, from: tintedCIImage.extent) {
            return UIImage(cgImage: cgImage)
        }
        
        return nil
    }
}

extension String {
    
    var qrCIImage: CIImage? {
        guard let qrFilter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
        let qrData = data(using: String.Encoding.ascii)
        qrFilter.setValue(qrData, forKey: "inputMessage")
        
        let qrTransform = CGAffineTransform(scaleX: 12, y: 12)
        return qrFilter.outputImage?.transformed(by: qrTransform)
    }
    
    var transparentQRImage: CIImage? {
        return qrCIImage?.transparent
    }
}

extension CIImage {
    
    var transparent: CIImage? {
        return inverted?.blackTransparent
    }
    
    var inverted: CIImage? {
        guard let invertedColorFilter = CIFilter(name: "CIColorInvert") else { return nil }
        invertedColorFilter.setValue(self, forKey: "inputImage")
        return invertedColorFilter.outputImage
    }
    
    var blackTransparent: CIImage? {
        guard let blackTransparentFilter = CIFilter(name: "CIMaskToAlpha") else { return nil }
        blackTransparentFilter.setValue(self, forKey: "inputImage")
        return blackTransparentFilter.outputImage
    }
    
    func tinted(using color: UIColor) -> CIImage? {
        guard
            let transparentQRImage = transparent,
            let filter = CIFilter(name: "CIMultiplyCompositing"),
            let colorFilter = CIFilter(name: "CIConstantColorGenerator") else { return nil }
        
        let ciColor = CIColor(color: color)
        colorFilter.setValue(ciColor, forKey: kCIInputColorKey)
        let colorImage = colorFilter.outputImage
        
        filter.setValue(colorImage, forKey: kCIInputImageKey)
        filter.setValue(transparentQRImage, forKey: kCIInputBackgroundImageKey)
        
        return filter.outputImage
    }
}

How to use it:

class ViewController: UIViewController {
    
    @IBOutlet weak var imgview: UIImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .yellow
        
        // Generate a QR code for a sample string
        let sampleString = "Hello, QR Code!"
        if let qrCodeImage = UIImage.make(qrcode: sampleString, tint: UIColor.black) {
            imgview.image = qrCodeImage
        }
    }
}