How to make a Self-sizing UiImageView?

74 views Asked by At

I have a need for a simple QR Code class that I can re-use. I have created the class and it works, however manually setting the size constraints is not desired because it needs to adjust its size based on the DPI of the device. Here in this minimal example, I just use 100 as the sizing calculation code is not relevant (set to 50 in IB). Also I will have multiple QR Codes in different positions, which I will manage their positioning by IB. But at least I hope to be able to set the width and height constraints in code.

The below code shows a QR code, in the right size (set at runtime), but when the constraints are set to horizontally and vertically center it, it does not. Again, I don't want the size constraints in the IB, but I do want the position constraints in the IB

import Foundation
import UIKit

@IBDesignable class QrCodeView: UIImageView {
    var content:String = "test" {
        didSet {
            generateCode(content)
        }
    }
    lazy var filter = CIFilter(name: "CIQRCodeGenerator")
    lazy var imageView = UIImageView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        imageView.frame = CGRect(x:0, y:0, width:100, height:100)
        frame = CGRect(x:frame.origin.x, y:frame.origin.y, width:100, height:100)

    }

    func setup() {
        //translatesAutoresizingMaskIntoConstraints = false
        generateCode(content)


        addSubview(imageView)
        layoutIfNeeded()

    }

    func generateCode(_ string: String) {
        guard let filter = filter,
        let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
            return
        }

        filter.setValue(data, forKey: "inputMessage")

        guard let ciImage = filter.outputImage else {
            return
        }
        let transform = CGAffineTransform(scaleX: 10, y: 10)
        let scaled = UIImage(ciImage: ciImage.transformed(by: transform))

        imageView.image = scaled
    }

}
1

There are 1 answers

4
DonMag On BEST ANSWER

I believe you're making this more complicated than need be...

Let's start with a simple @IBDesignable UIImageView subclass.

Start with a new project and add this code:

@IBDesignable
class MyImageView: UIImageView {

    // we'll use this later
    var myIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
    override var intrinsicContentSize: CGSize {
        return myIntrinsicSize
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setup()
        self.image = UIImage()
    }
    
    func setup() {
        backgroundColor = .green
        contentMode = .scaleToFill
    }

}

Now, in Storyboard, add a UIImageView to a view controller. Set its custom class to MyImageView and set Horizontal and Vertical Center constraints.

The image view should automatically size itself to 100 x 100, centered in the view with a green background (we're just setting the background so we can see it):

enter image description here

Run the app, and you should see the same thing.

Now, add it as an @IBOutlet to a view controller:

class ViewController: UIViewController {

    @IBOutlet var testImageView: MyImageView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        testImageView.myIntrinsicSize = CGSize(width: 300.0, height: 300.0)
    }
}

Run the app, and you will see a centered green image view, but now it will be 300 x 300 points instead of 100 x 100.

The rest of your task is pretty much adding code to set this custom class's .image property once you've rendered the QRCode image.

Here's the custom class:

@IBDesignable
class QRCodeView: UIImageView {
    
    // so we can test changing the QRCode content in IB
    @IBInspectable
    var content:String = "test" {
        didSet {
            generateCode(content)
        }
    }
    
    var qrIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
    override var intrinsicContentSize: CGSize {
        return qrIntrinsicSize
    }
    
    lazy var filter = CIFilter(name: "CIQRCodeGenerator")
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setup()
        generateCode(content)
    }
    
    func setup() {
        contentMode = .scaleToFill
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        generateCode(content)
    }
    
    func generateCode(_ string: String) {
        guard let filter = filter,
            let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
                return
        }
        
        filter.setValue(data, forKey: "inputMessage")
        
        guard let ciImage = filter.outputImage else {
            return
        }
        
        let scX = bounds.width / ciImage.extent.size.width
        let scY = bounds.height / ciImage.extent.size.height

        let transform = CGAffineTransform(scaleX: scX, y: scY)

        let scaled = UIImage(ciImage: ciImage.transformed(by: transform))
        
        self.image = scaled
        
    }
    
}

In Storyboard / IB:

enter image description here

And here's an example view controller:

class ViewController: UIViewController {
    
    @IBOutlet var qrCodeView: QRCodeView!
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // calculate your needed size
        //  I'll assume it ended up being 240 x 240
        qrCodeView.qrIntrinsicSize = CGSize(width: 240.0, height: 240.0)
    }
    
}

Edit

Here's a modified QRCodeView class that will size itself to a (physical) 15x15 mm image.

I used DeviceKit from https://github.com/devicekit/DeviceKit to get the current device's ppi. See the comment to replace it with your own (assuming you are already using something else).

When this class is instantiated, it will:

  • get the current device's ppi
  • convert ppi to pixels-per-millimeter
  • calculate 15 x pixels-per-millimeter
  • convert based on screen scale
  • update its intrinsic size

The QRCodeView (subclass of UIImageView) needs only position constraints... so you can use Top + Leading, Top + Trailing, Center X & Y, Bottom + CenterX, etc, etc.

@IBDesignable
class QRCodeView: UIImageView {
    
    @IBInspectable
    var content:String = "test" {
        didSet {
            generateCode(content)
        }
    }
    
    var qrIntrinsicSize: CGSize = CGSize(width: 100.0, height: 100.0)
    override var intrinsicContentSize: CGSize {
        return qrIntrinsicSize
    }
    
    lazy var filter = CIFilter(name: "CIQRCodeGenerator")
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setup()
        generateCode(content)
    }
    
    func setup() {
        contentMode = .scaleToFill
        
        // using DeviceKit from https://github.com/devicekit/DeviceKit
        // replace with your lookup code that gets
        //  the device's ppi
        let device = Device.current
        guard let ppi = device.ppi else { return }
        
        // convert to pixels-per-millimeter
        let ppmm = CGFloat(ppi) / 25.4
        // we want 15mm size
        let mm15 = 15.0 * ppmm
        // convert based on screen scale
        let mmScale = mm15 / UIScreen.main.scale
        // update our intrinsic size
        self.qrIntrinsicSize = CGSize(width: mmScale, height: mmScale)

    }
    override func layoutSubviews() {
        super.layoutSubviews()
        generateCode(content)
    }
    
    func generateCode(_ string: String) {
        guard let filter = filter,
            let data = string.data(using: .isoLatin1, allowLossyConversion: false) else {
                return
        }
        
        filter.setValue(data, forKey: "inputMessage")
        
        guard let ciImage = filter.outputImage else {
            return
        }
        
        let scX = bounds.width / ciImage.extent.size.width
        let scY = bounds.height / ciImage.extent.size.height

        let transform = CGAffineTransform(scaleX: scX, y: scY)

        let scaled = UIImage(ciImage: ciImage.transformed(by: transform))

        self.image = scaled
        
    }
    
}