Set Size for UIButton with new iOS15 Configuration Approach

39 Views Asked by At

My goal is to add button to the right hand side of a section header in a UITableView.

However, when I made my new target iOS15, I found that I had to adopt UIButton configuration to set the padding to the add icon in the button. The problem is that, configuration doesn't seem to have a "frame" property and passing a configuration to a button then setting its frame builds to show no button at all.

My new, faulty code is:

func tableView (_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let frame = tableView.frame
    let headerView = UIView(frame: CGRect(x:0, y: 0, width: frame.size.width, height: frame.size.height))  // create custom view

    let sectionName = hasSections ? sectionTasks[section].sectionName : nil
    if sectionName != nil {
        let view = UIView()
        let label = UILabel()
        ... // Label config, skipping here
        let tableWidth = tableView.frame.width
        label.frame = CGRect(x: 5, y: 0, width: tableWidth, height: 30)
        headerView.addSubview(label)
    }
            
    // My faulty button
    var buttonConfig = UIButton.Configuration.filled()
    buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
    buttonConfig.image = UIImage(named: "add_AsTemplate")
    let xPos = tableView.frame.width-40-15
    let button = UIButton(configuration: buttonConfig)
    button.frame = CGRect(x: xPos, y: 0, width: 40, height: 40)
    button.tag = section
    button.addTarget(self, action: #selector(BaseTaskListVC.sectionNewTask_BtnPressed), for: .touchUpInside)  // add selector called by clicking on the button
    headerView.addSubview(button)
    
    return headerView
}

My original button code (skipping context of parent function):

let xPos = tableView.frame.width-40-15
let button = UIButton(frame: CGRect(x: xPos, y: 0, width: 40, height: 40))
button.tag = section
button.imageEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5  )
button.setImage(UIImage(named: "add_AsTemplate"), for: UIControl.State.normal) 
button.addTarget(self, action: #selector(BaseTaskListVC.sectionNewTask_BtnPressed), for: .touchUpInside) 
headerView.addSubview(button)

I've read the Apple documentation but it doesn't seem to address this explicitly. All the other SO threads on this show how to use configuration but not configuration + frame.

Original code:

Original code at runtime

With new configuration approach:

With new configuration approach

Lastly, I've tried the suggested answer:

var buttonConfig = UIButton.Configuration.plain()
buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

var theImage: UIImage
if let img = UIImage(named: "add_AsTemplate") {
    theImage = img
} else if let img = UIImage(systemName: "plus") {
    // I don't have your "add_AsTemplate" image, so I'll use a SF Symbol
    theImage = img
} else {
    fatalError("Could not load a button image!")
}
buttonConfig.image = theImage
let button = UIButton(configuration: buttonConfig)

button.translatesAutoresizingMaskIntoConstraints = false
headerView.addSubview(button)

NSLayoutConstraint.activate([
    button.topAnchor.constraint(equalTo: headerView.topAnchor),
    button.widthAnchor.constraint(equalToConstant: 40.0),
    button.heightAnchor.constraint(equalToConstant: 40.0),
    button.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -15.0),
])

Which produced this result:

Image overflowing its bounds

1

There are 1 best solutions below

2
DonMag On

You are not required to use the "new" UIButton.Configuration button style... you can continue to use the "old" button class.

However, in either case, you should be using auto-layout / constraints, rather than setting the .frame.

For example:

    let button = UIButton()
    
    var theImage: UIImage
    if let img = UIImage(named: "add_AsTemplate") {
        theImage = img
    } else if let img = UIImage(systemName: "plus") {
        // I don't have your "add_AsTemplate" image, so I'll use a SF Symbol
        theImage = img
    } else {
        fatalError("Could not load a button image!")
    }
    button.setImage(theImage, for: .normal)

    button.imageEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5  )

    button.translatesAutoresizingMaskIntoConstraints = false
    headerView.addSubview(button)
    
    NSLayoutConstraint.activate([
        button.topAnchor.constraint(equalTo: headerView.topAnchor),
        button.widthAnchor.constraint(equalToConstant: 40.0),
        button.heightAnchor.constraint(equalToConstant: 40.0),
        button.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -15.0),
    ])

or, using UIButton.Configuration:

    var buttonConfig = UIButton.Configuration.plain()
    buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

    var theImage: UIImage
    if let img = UIImage(named: "add_AsTemplate") {
        theImage = img
    } else if let img = UIImage(systemName: "plus") {
        // I don't have your "add_AsTemplate" image, so I'll use a SF Symbol
        theImage = img
    } else {
        fatalError("Could not load a button image!")
    }
    buttonConfig.image = theImage
    let button = UIButton(configuration: buttonConfig)

    button.translatesAutoresizingMaskIntoConstraints = false
    headerView.addSubview(button)
    
    NSLayoutConstraint.activate([
        button.topAnchor.constraint(equalTo: headerView.topAnchor),
        button.widthAnchor.constraint(equalToConstant: 40.0),
        button.heightAnchor.constraint(equalToConstant: 40.0),
        button.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -15.0),
    ])

Both give this result (the yellow rectangle is "simulating" your table header view), regardless of the actual width:

enter image description here

enter image description here


Edit

Here is a complete, runnable example. Just assign DemoTableViewController as the class of a table view controller:

class MySectionHeaderView: UITableViewHeaderFooterView {
    
    // closure for "add" button tap
    var addTapClosure: (() -> ())?
    
    var theButton: UIButton!
    var theLabel: UILabel = UILabel()
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        var buttonConfig = UIButton.Configuration.plain()
        buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
        
        var theImage: UIImage
        if let img = UIImage(named: "add_AsTemplate") {
            theImage = img
        } else if let img = UIImage(systemName: "plus") {
            // I don't have your "add_AsTemplate" image, so I'll use a SF Symbol
            theImage = img
        } else {
            fatalError("Could not load a button image!")
        }
        buttonConfig.image = theImage
        
        theButton = UIButton(configuration: buttonConfig, primaryAction: UIAction() { _ in
            self.addTapClosure?()
        })
        
        theLabel.translatesAutoresizingMaskIntoConstraints = false
        theButton.translatesAutoresizingMaskIntoConstraints = false

        contentView.addSubview(theLabel)
        contentView.addSubview(theButton)

        let g = contentView.layoutMarginsGuide
        
        // this avoids auto-layout warnings
        let b = theButton.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0)
        b.priority = .required - 1
        
        NSLayoutConstraint.activate([
            
            theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            theLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),

            theButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            theButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            b,

            theButton.widthAnchor.constraint(equalToConstant: 40.0),
            theButton.heightAnchor.constraint(equalToConstant: 40.0),
        ])

        // so we can see the header framing
        contentView.backgroundColor = .yellow
    }
}

class MyDemoCell: UITableViewCell {
    
    var theButton: UIButton!
    var theLabel: UILabel = UILabel()
    var theSubLabel: UILabel = UILabel()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier);
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        var buttonConfig = UIButton.Configuration.plain()
        buttonConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
        
        var theImage: UIImage
        if let img = UIImage(named: "check_AsTemplate") {
            theImage = img
        } else if let img = UIImage(systemName: "checkmark.circle.fill") {
            // I don't have a "check_AsTemplate" image, so I'll use a SF Symbol
            theImage = img
        } else {
            fatalError("Could not load a button image!")
        }
        buttonConfig.image = theImage
        
        theButton = UIButton(configuration: buttonConfig)
        
        theLabel.textColor = .darkGray
        theSubLabel.textColor = .gray
        theLabel.font = .systemFont(ofSize: 15.0, weight: .regular)
        theSubLabel.font = .systemFont(ofSize: 13.0, weight: .regular)

        theLabel.translatesAutoresizingMaskIntoConstraints = false
        theSubLabel.translatesAutoresizingMaskIntoConstraints = false
        theButton.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(theLabel)
        contentView.addSubview(theSubLabel)
        contentView.addSubview(theButton)
        
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            theLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            theLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 12.0),
            
            theSubLabel.topAnchor.constraint(equalTo: theLabel.bottomAnchor, constant: 6.0),
            theSubLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 24.0),
            theSubLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
            theButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            theButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            
            theButton.widthAnchor.constraint(equalToConstant: 40.0),
            theButton.heightAnchor.constraint(equalToConstant: 40.0),
        ])
        
    }
}

struct MyTask {
    var title: String = "Example Task"
    var subTitle: String = "Tap to update this to a real task"
}

class DemoTableViewController: UITableViewController {
    
    var theData: [[MyTask]] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.register(MyDemoCell.self, forCellReuseIdentifier: "c")
        tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: "h")
        tableView.sectionHeaderTopPadding = 0
        
        // create sample data - 5 sections, each with one task
        for _ in 1...5 {
            var secData: [MyTask] = []
            secData.append(MyTask())
            theData.append(secData)
        }
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return theData.count
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return theData[section].count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! MyDemoCell
        let d = theData[indexPath.section][indexPath.row]
        c.theLabel.text = d.title
        c.theSubLabel.text = d.subTitle
        return c
    }
    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: "h") as! MySectionHeaderView
        v.theLabel.text = "Header for Section: \(section)"
        
        v.addTapClosure = { [weak self] in
            guard let self = self else { return }
            self.theData[section].append(MyTask(title: "Added", subTitle: "via header button tap"))
            self.tableView.reloadSections([section], with: .automatic)
        }
        return v
    }
    
}

Should look like this (after I added two additional rows to section 1):

enter image description here