From Apple’s docs:
Discussion
At a minimum, always set an image for the normal state when associating images to a button. If you don’t specify an image for the other states, the button uses the image associated with normal. If you don’t specify an image for the normal state, the button uses a system value.
What may not be immediately clear is that calling:
button.setImage(nil, for: .selected)
can also be read as: “did not specify an image for .selected state.”
So, the button will use the image from .normal.
If you want to remove the image when setting the button state to .selected:
button.isSelected = true
button.setImage(nil, for: .normal)
button.setImage(nil, for: .selected)
Here is some sample code to play with:
class SelectedButtonVC: UIViewController {
let button = UIButton()
var toggles: [UISwitch] = []
var imgs: [String : UIImage] = [:]
let titles: [String] = [
"Normal", "Highlighted", "Selected",
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
var promptLabel: UILabel!
var vSep: UIView!
var hs: UIStackView!
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
let colors: [UIColor] = [
.systemBlue, .systemRed, .systemGreen,
]
let states: [UIControl.State] = [
.normal, .highlighted, .selected,
]
let largeConfig = UIImage.SymbolConfiguration(pointSize: 40, weight: .bold, scale: .large)
for (t, c) in zip(titles, colors) {
guard let f = t.first?.description else { fatalError() }
guard let img = UIImage(systemName: "\(f.lowercased()).square.fill", withConfiguration: largeConfig)?.withTintColor(c, renderingMode: .alwaysOriginal) else {
fatalError("Could not load system image for \(t.lowercased())")
}
imgs[t] = img
}
for (t, s) in zip(titles, states) {
button.setTitle(t, for: s)
button.setImage(imgs[t], for: s)
}
for (c, s) in zip(colors, states) {
button.setTitleColor(c, for: s)
}
button.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
button.layer.cornerRadius = 8
button.layer.borderWidth = 1
promptLabel = UILabel()
promptLabel.text = "This is a button..."
stack.addArrangedSubview(promptLabel)
stack.addArrangedSubview(button)
stack.setCustomSpacing(20.0, after: button)
vSep = UIView()
vSep.backgroundColor = .gray
vSep.heightAnchor.constraint(equalToConstant: 2.0).isActive = true
stack.addArrangedSubview(vSep)
hs = UIStackView()
hs.spacing = 8
hs.alignment = .center
promptLabel = UILabel()
promptLabel.text = "Toggle button.isSelected:"
hs.addArrangedSubview(promptLabel)
let sw = UISwitch()
sw.isOn = false
toggles.append(sw)
hs.addArrangedSubview(sw)
stack.addArrangedSubview(hs)
vSep = UIView()
vSep.backgroundColor = .gray
vSep.heightAnchor.constraint(equalToConstant: 2.0).isActive = true
stack.addArrangedSubview(vSep)
stack.setCustomSpacing(20.0, after: vSep)
promptLabel = UILabel()
promptLabel.text = "Toggle button images for states:"
stack.addArrangedSubview(promptLabel)
titles.forEach { t in
hs = UIStackView()
hs.spacing = 8
hs.alignment = .center
promptLabel = UILabel()
promptLabel.text = t
hs.addArrangedSubview(promptLabel)
let v = UIImageView(image: imgs[t])
v.contentMode = .scaleAspectFit
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
hs.addArrangedSubview(v)
let sw = UISwitch()
sw.isOn = true
toggles.append(sw)
hs.addArrangedSubview(sw)
stack.addArrangedSubview(hs)
}
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
button.heightAnchor.constraint(equalToConstant: 60.0),
])
toggles.forEach { sw in
sw.addTarget(self, action: #selector(swTapped(_:)), for: .valueChanged)
}
button.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
@objc func swTapped(_ sender: UISwitch) {
guard let idx = toggles.firstIndex(of: sender) else { return }
switch idx {
case 1:
button.setImage(sender.isOn ? imgs[titles[0]] : nil, for: .normal)
()
case 2:
button.setImage(sender.isOn ? imgs[titles[1]] : nil, for: .highlighted)
()
case 3:
button.setImage(sender.isOn ? imgs[titles[2]] : nil, for: .selected)
()
default:
button.isSelected = sender.isOn
()
}
}
@objc func btnTapped(_ sender: UIButton) {
print("button.isSelected =", sender.isSelected)
}
}
Looks like this:
When running, toggling the .isSelected switch will, as you might guess, set button.isSelected to true or false.
Toggling the button image state switches will either set the image to an image (if On) or set it to nil if Off.
Perhaps worth noting…
When a button has:
btn.isSelected = true
the normal properties (title, title color, image, etc) are used for the highlighted state.
This can be seen in the above example code by toggling .isSelected to On, and then tapping the button. We no longer see the “Highlighted” title or image.
If that is your intended behavior, great! If not, you may need to re-think your approach.





