2

I have a UIImageView in a UICollectionViewCell. I am using auto layout to set it to the top left of the cell with fixed width and height.

enter image description here

enter image description here

When I set the image on the image view, from an image in the asset catalog, things work great.

When I set the image on the image view using SF Symbols, the UIImageView's frame changes randomly.

Autolayout sets the UIImageView's frame to the top left of the cell and sets the width and height to 24. That is respected when using images from Asset Catalog UIImage(named:...

the frame changes randomly when I do this:

let conf = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium, scale: .large)
let image = UIImage(systemName: "doc.fill", withConfiguration: conf)
imageView.image = image

At times the cell shows the image like this:

enter image description here

Other times like this:

enter image description here

If I print the frame of the UIImageView to console I see the change, either like this:

testImageView: (8.0, 6.666666666666668, 24.0, 27.0)

or like this:

testImageView: (8.0, 7.0, 24.0, 26.666666666666664)

All other frames when logged look constant, the cell frame is constant etc... the image generated from SF Symbol forces the frame of the UIImageView to change.

Why is this happening, and how can I use SF symbols properly so the frame I set in auto layout is constant at run time when setting the ImageView's image to an image generated from SF Symbols?

zumzum
  • 17,984
  • 26
  • 111
  • 172

1 Answers1

4

Using SF Symbols with configurations for image views is very quirky.

I don't know if I'd call it a "bug" or just un-documented (or very obscurely documented) behavior.

Setting the .image property of a UIImageView like this:

let conf = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium, scale: .large)
let image = UIImage(systemName: "doc.fill", withConfiguration: conf)
imageView.image = image

will change the image view's frame! ... regardless of constraints.

Here is a clear example...

We start with 4 image views, constrained to Width: 80, Height: equalTo Width. The red lines are constrained to the Top and Bottom of each image view:

enter image description here

Now, we set each imageView's image, using symbol configurations of pointSize (starting at 10.0), weight: .medium and scale: with small, medium and large, plus the bottom one using NO configuration --img = UIImage(systemName: "doc.fill").

At 10.0 pointSize, we can already see changes in the image view frames:

enter image description here

when we get up to point size of 50.0 the "weird frame sizing" is very obvious:

enter image description here

here's a pixel-accurate capture:

enter image description here

Here's the code for this example so you can inspect everything in more detail:

class SFSymbolsViewController: UIViewController {
    
    var imgViews: [UIImageView] = []
    var labels: [[UILabel]] = []
    
    // this will be incremented with each tap
    var ptSize: CGFloat = 5.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let g = view.safeAreaLayoutGuide
        
        let infoLabel = UILabel()
        infoLabel.translatesAutoresizingMaskIntoConstraints = false
        infoLabel.numberOfLines = 0
        infoLabel.textAlignment = .center
        infoLabel.font = .systemFont(ofSize: 14.0)
        infoLabel.text = "Tap to add images.\nPointSize will start at 10, and each Tap will increment the Point Size by 5.0 and re-generate the images."

        view.addSubview(infoLabel)
        NSLayoutConstraint.activate([
            infoLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
        ])
        
        var y: CGFloat = 140
        let yInc: CGFloat = 100

        for _ in 1...4 {
            let imageView = UIImageView()
            imageView.backgroundColor = .systemYellow
            imageView.contentMode = .scaleAspectFit
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imgViews.append(imageView)
            
            view.addSubview(imageView)
            
            // horizontal "line" views
            let h1 = UIView()
            let h2 = UIView()
            [h1, h2].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                v.backgroundColor = .red
                view.addSubview(v)
                NSLayoutConstraint.activate([
                    v.heightAnchor.constraint(equalToConstant: 1.0),
                    v.widthAnchor.constraint(equalTo: g.widthAnchor),
                    v.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                ])
            }
            
            // info labels
            var imgLabels: [UILabel] = []
            for _ in 1...3 {
                let label = UILabel()
                label.font = .systemFont(ofSize: 12.0)
                label.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(label)
                // add label to right of imageView
                NSLayoutConstraint.activate([
                    label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12.0),
                    label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -12.0),
                ])
                imgLabels.append(label)
            }
            imgLabels[1].text = "cfg:"
            labels.append(imgLabels)
            
            NSLayoutConstraint.activate([
                // image view Top = y, Leading = 20
                //  width = 80, height = width (1:1 ratio)
                imageView.topAnchor.constraint(equalTo: g.topAnchor, constant: y),
                imageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                imageView.widthAnchor.constraint(equalToConstant: 80.0),
                imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
                
                // put a "line" on top and bottom of imageView
                h1.bottomAnchor.constraint(equalTo: imageView.topAnchor),
                h2.topAnchor.constraint(equalTo: imageView.bottomAnchor),
                
                // label y positions
                imgLabels[0].topAnchor.constraint(equalTo: h1.bottomAnchor, constant: 4.0),
                imgLabels[1].centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
                imgLabels[2].bottomAnchor.constraint(equalTo: h2.topAnchor, constant: -4.0),
            ])
            y += yInc
        }
        
        let t = UITapGestureRecognizer(target: self, action: #selector(self.setImages(_:)))
        view.addGestureRecognizer(t)
        
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        updateSizeLabels(true)
    }
    
    func updateSizeLabels(_ orig: Bool) -> Void {
        for (l, v) in zip(labels, imgViews) {
            if orig {
                l[0].text = "Orig Frame: \(v.frame)"
            }
            l[2].text = "New Frame: \(v.frame)"
        }
    }
    
    @objc func setImages(_ g: UITapGestureRecognizer) -> Void {
        
        ptSize += 5.0
        
        var cfg: UIImage.SymbolConfiguration!
        var img: UIImage!
        var i: Int = 0
        
        labels[i][1].text = "cfg: \(ptSize) / medium / small"
        cfg = UIImage.SymbolConfiguration(pointSize: ptSize, weight: .medium, scale: .small)
        img = UIImage(systemName: "doc.fill", withConfiguration: cfg)
        
        imgViews[i].image = img
        
        i += 1
        
        labels[i][1].text = "cfg: \(ptSize) / medium / medium"
        cfg = UIImage.SymbolConfiguration(pointSize: ptSize, weight: .medium, scale: .medium)
        img = UIImage(systemName: "doc.fill", withConfiguration: cfg)
        
        imgViews[i].image = img
        
        i += 1
        
        labels[i][1].text = "cfg: \(ptSize) / medium / large"
        cfg = UIImage.SymbolConfiguration(pointSize: ptSize, weight: .medium, scale: .large)
        img = UIImage(systemName: "doc.fill", withConfiguration: cfg)
        
        imgViews[i].image = img
        
        i += 1
        
        labels[i][1].text = "cfg: NO SymbolConfiguration"
        img = UIImage(systemName: "doc.fill")
        
        imgViews[i].image = img

        // update the size labels after UI updates
        DispatchQueue.main.async {
            self.updateSizeLabels(false)
        }
    }
    
}

Bottom-line: I believe the configuration options are more directly related to cooperating with fonts. When using SF Symbols in this way, you may be better off not using the UIImage.SymbolConfiguration (unless you can't get your desired appearance, in which case you may need to jump through some hoops to get sizing / alignment correct).

DonMag
  • 69,424
  • 5
  • 50
  • 86