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:

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:

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

here's a pixel-accurate capture:

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).