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.
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:
Other times like this:
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?
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With