What you're seeing has nothing to do with your selector
/ @objc func
...
Many UIKit classes do a lot of "under-the-hood" work.
In the case of UIButton
, subviews are only added as-needed.
For example, if this is all the code you execute:
let balloon = UIButton()
balloon.frame = CGRect(x: 100, y: 200, width: 240, height: 100)
view.addSubview(balloon)
The resulting button has Zero subviews.
If we do this:
let balloon = UIButton()
balloon.frame = CGRect(x: 100, y: 200, width: 240, height: 100)
view.addSubview(balloon)
balloon.setTitle("ABC", for: [])
The resulting button now has 1 subviews.
let balloon = UIButton()
balloon.frame = CGRect(x: 100, y: 200, width: 240, height: 100)
view.addSubview(balloon)
balloon.setTitle("ABC", for: [])
balloon.setBackgroundImage(img, for: .normal)
The resulting button now has 2 subviews.
let balloon = UIButton()
balloon.frame = CGRect(x: 100, y: 200, width: 240, height: 100)
view.addSubview(balloon)
balloon.setTitle("ABC", for: [])
balloon.setBackgroundImage(imgA, for: .normal)
balloon.addSubview(subview)
The resulting button now has 3 subviews.
let balloon = UIButton()
balloon.frame = CGRect(x: 100, y: 200, width: 240, height: 100)
view.addSubview(balloon)
balloon.setTitle("ABC", for: [])
balloon.setBackgroundImage(imgA, for: .normal)
balloon.addSubview(subview)
balloon.setImage(imgB, for: [])
And now we have 4 subviews.
Apple strongly discourages messing with the internals of UIButton
. You might be better off creating a view subclass that contains a button and any additional subviews, rather than your current approach.
Worth Noting
- the subviews may not be added until they are needed. So, if you set the title or image or background image in
viewDidLoad()
, those subviews will not be created until viewDidLayoutSubviews
... or even until the button is actually rendered.
- the title label may be created even when you don't expect it to. For example, if the only thing you do to the button is constrain its position, it will get a title label. However, if you set the background image, the title label will be omitted.
Here is a quick example to demonstrate some of the resulting subview counts:
class BtnVC: UIViewController {
var buttons: [UIButton] = []
let stack = UIStackView()
let infoLabel: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
return v
}()
var didLoadStr: String = ""
var didLayoutStr: String = ""
var didAppearStr: String = ""
override func viewDidLoad() {
super.viewDidLoad()
let largeFont = UIFont.systemFont(ofSize: 32)
let configuration = UIImage.SymbolConfiguration(font: largeFont) // <1>
guard let img2 = UIImage(systemName: "02.circle.fill", withConfiguration: configuration),
let img3 = UIImage(systemName: "03.circle.fill"),
let img4 = UIImage(systemName: "04.circle.fill")
else {
return
}
var b: UIButton!
b = UIButton()
buttons.append(b)
b = UIButton()
b.setTitle("1", for: [])
buttons.append(b)
b = UIButton()
b.setTitle("1", for: [])
b.setImage(img2, for: [])
buttons.append(b)
b = UIButton()
b.setTitle("1", for: [])
b.setImage(img2, for: [])
b.setBackgroundImage(img3.withTintColor(.systemGreen, renderingMode: .alwaysOriginal), for: [])
buttons.append(b)
b = UIButton()
b.setTitle("1", for: [])
b.setImage(img2, for: [])
b.setBackgroundImage(img3.withTintColor(.systemGreen, renderingMode: .alwaysOriginal), for: [])
let v = UIImageView(image: img4)
v.tintColor = .systemRed
v.frame = CGRect(x: 0, y: 0, width: 32, height: 32)
b.addSubview(v)
buttons.append(b)
stack.axis = .vertical
stack.spacing = 8
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 120.0),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add this first button *without* auto-layout
buttons[0].frame = CGRect(x: 80, y: 40, width: 200, height: 50)
view.addSubview(buttons[0])
// add the rest of the buttons to the stack view
buttons.forEach { v in
v.backgroundColor = .systemYellow
v.setTitleColor(.black, for: [])
if v != buttons.first {
stack.addArrangedSubview(v)
}
}
stack.addArrangedSubview(infoLabel)
var s = "didLoad : "
buttons.forEach { v in
s += "\(v.subviews.count) / "
}
didLoadStr += s + "\n"
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// first button is not using auto-layout constraints, so
// let's size it to match the buttons in the stack view
// and posiiton it 8-pts above the stack view
var r = buttons[1].bounds
r.origin.y = stack.frame.origin.y - r.height - 8.0
r.origin.x = stack.frame.origin.x
buttons[0].frame = r
var s = "didLayout: "
buttons.forEach { v in
s += "\(v.subviews.count) / "
}
didLayoutStr += s + "\n"
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var s = "didAppear: "
buttons.forEach { v in
s += "\(v.subviews.count) / "
}
didAppearStr += s + "\n"
infoLabel.text = didLoadStr + didLayoutStr + didAppearStr
}
}
That code will create 5 buttons, with each successive button getting another subview - title, image, backgroundImage, addSubview - and the "Info Label" at the bottom will show the subviews count at each stage (as is often the case, didLayoutSubviews()
is called more than once):
