Why does the selector, @objc, function add a subview, of my button image (as a UIImageView), in my UIButton?

73 Views Asked by At

I first declare the button

 let balloon = UIButton()

A background image then gets added to the balloon

balloon.setBackgroundImage(UIImage(named:"balloon.jpg"), for: .normal)

An image view of the points get added to the balloon as a subview

subView = UIImageView(image: UIImage(named: "1") )
subView.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
subView.contentMode = UIView.ContentMode.scaleAspectFit
subView.center = CGPoint(x: balloon.frame.width/2, y: balloon.frame.height/2)
balloon.addSubview(subView)

I then use the addTarget function for the balloon

balloon.addTarget(self, action: #selector(pop), for: .touchUpInside)

After pop gets called (when the user taps on the balloon), the balloon now contains 2 subviews - At index 0 of balloon.subviews, there is a UIImageView that is essentially the picture of the balloon with the same dimensions as the balloon button - And the subs view that I added (aka the points)

here is how I found this problem in my addTarget function (pop):

@objc func pop(_ balloon: UIButton){
    print("4.This is the balloon after calling pop \(balloon)")
    print("5. This is the subview of the balloon after calling pop \(balloon.subviews)")

Ive added print statements in my function that verifies that the balloons are the same in both the pop func and my balloon creation func

Here is what terminal prints

I have already looked at the documentation for both UIButton and addTarget and neither of them have specified why the background image of the button gets created as a subview of the button when the selector func gets called

There shouldn't be that extra UIImageView in my UIButton since I never added that

1

There are 1 best solutions below

0
On

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

enter image description here