Search code examples
iosswiftuiviewuilabeluistackview

iOS - Take all the available space in a horizontal UIStackView


I have UITableViewCell that contains a horizontal UIStackView. The UIStackView contains four views in the following order.

UIImageView   UILabel    UILabel UIImageView

There are 16 points spacing after the arrangedSubViews. I want that the second UILabel takes all the available space. If there is not enough space, it text should wrap.

I have used the following codes. It works almost. The problem is that even though there is enough space between the UILabel's as you see in the screenshot attached, the second UILabel breaks. But I want it to break only if there is not enough space.

enter image description here


class TableViewCell: UITableViewCell {
    
    static let identifier = "TableViewCell"
    
    private let leadingImageView: UIImageView = {
        let view = UIImageView(image: UIImage(systemName: "calendar"))
        view.setConstraints(heightConstant: 25, widthConstant: 25)
        view.tintColor = .text
        return view
    }()
    
    private let leadingLabel: UILabel = {
        let label = UILabel()
        label.textColor = .label
        label.text = "Start Date"
        label.numberOfLines = 0
        label.sizeToFit()
        return label
    }()
    
    let trailingLabel: UILabel = {
        let label = UILabel()
        label.text = "Friday, 17 July 2020"
        label.textColor = .label
        label.numberOfLines = 0
        label.textAlignment = .right
        return label
    }()
    
    let trailingImageView: UIImageView  = {
        let configuration = UIImage.SymbolConfiguration(pointSize: 12, weight: .light)
        let image = UIImage(systemName: "arrowtriangle.down", withConfiguration: configuration)
        let view = UIImageView(image: image)
        return view
    }()
    
    private let superStackView: UIStackView = {
        let view = UIStackView()
        view.distribution = .fillProportionally
        view.alignment = .center
        view.spacing = 16
        return view
    }()
    
    private  let containerView: UIView = {
        let view = UIView()
        view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        view.setContentHuggingPriority(.defaultHigh, for: .vertical)
        return view
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setUpSubviews()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}
extension TableViewCell {
    private  func setUpSubviews(){
        containerView.addSubview(trailingImageView)
        trailingImageView.alignCenter(centerXAnchor: containerView.centerXAnchor,
                                      centerYAnchor: containerView.centerYAnchor)
        
        superStackView.addArrangedSubview(leadingImageView)
        superStackView.addArrangedSubview(leadingLabel)
        superStackView.addArrangedSubview(trailingLabel)
        superStackView.addArrangedSubview(containerView)
        
        let constant = CGFloat(16)
    
        self.addSubview(superStackView)
        self.contentView.addSubview(superStackView)
        superStackView.setConstraints(topAnchor: contentView.topAnchor, leadingAnchor: contentView.leadingAnchor,
                              bottomAnchor: contentView.bottomAnchor, trailingAnchor: contentView.trailingAnchor,
                              topConstant: constant, leadingConstant: constant, bottomConstant: constant, trailingConstant: constant)
        
        
        self.contentView.updateConstraints()
        
    }
}

How can I fix this issue so that the cell looks like the cells in the following image?

enter image description here


Solution

  • You're close...

    First, setting a stack view's Distribution to Fill Proportionally is the most misunderstood distribution option, and you don't want to use it here.

    Second, it helps greatly when designing to use contrasting backgrounds to make it easy to see what your frames are doing.

    Here is your code, modified to get to what appears to be your goal. I don't have your "constraint helpers" so I changed it to standard constraint format. I also added a few comments for some clarification:

    class TableViewCell: UITableViewCell {
        
        static let identifier = "TableViewCell"
        
        private let leadingImageView: UIImageView = {
            let view = UIImageView(image: UIImage(systemName: "calendar"))
            view.translatesAutoresizingMaskIntoConstraints = false
            
            //view.setConstraints(heightConstant: 25, widthConstant: 25)
            view.widthAnchor.constraint(equalToConstant: 25).isActive = true
            view.heightAnchor.constraint(equalToConstant: 25).isActive = true
    
            view.tintColor = .blue // .text
            return view
        }()
        
        private let leadingLabel: UILabel = {
            let label = UILabel()
            label.textColor = .label
            label.text = "Start Date"
            // only single line for "leading label"
            label.numberOfLines = 1
            // content hugging
            label.setContentHuggingPriority(.required, for: .horizontal)
            label.backgroundColor = .cyan
            return label
        }()
        
        let trailingLabel: UILabel = {
            let label = UILabel()
            label.text = "Friday, 17 July 2020"
            label.textColor = .label
            label.numberOfLines = 0
            label.textAlignment = .right
            label.backgroundColor = .green
            return label
        }()
        
        let trailingImageView: UIImageView  = {
            let configuration = UIImage.SymbolConfiguration(pointSize: 12, weight: .light)
            let image = UIImage(systemName: "arrowtriangle.down", withConfiguration: configuration)
            let view = UIImageView(image: image)
            return view
        }()
        
        private let superStackView: UIStackView = {
            let view = UIStackView()
            view.translatesAutoresizingMaskIntoConstraints = false
            // do NOT use .fillProportionally
            view.distribution = .fill
            view.alignment = .center
            view.spacing = 16
            return view
        }()
        
        private  let containerView: UIView = {
            let view = UIView()
            // not needed
            //view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
            //view.setContentHuggingPriority(.defaultHigh, for: .vertical)
            return view
        }()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setUpSubviews()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    }
    extension TableViewCell {
        private  func setUpSubviews(){
            containerView.addSubview(trailingImageView)
            //trailingImageView.alignCenter(centerXAnchor: containerView.centerXAnchor,
            //                            centerYAnchor: containerView.centerYAnchor)
            
            superStackView.addArrangedSubview(leadingImageView)
            superStackView.addArrangedSubview(leadingLabel)
            superStackView.addArrangedSubview(trailingLabel)
            superStackView.addArrangedSubview(containerView)
            
            let constant = CGFloat(16)
            
            self.addSubview(superStackView)
            self.contentView.addSubview(superStackView)
            //superStackView.setConstraints(topAnchor: contentView.topAnchor, leadingAnchor: contentView.leadingAnchor,
            //                            bottomAnchor: contentView.bottomAnchor, trailingAnchor: contentView.trailingAnchor,
            //                            topConstant: constant, leadingConstant: constant, bottomConstant: constant, trailingConstant: constant)
            
            NSLayoutConstraint.activate([
                
                trailingImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
                trailingImageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
                
                superStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: constant),
                superStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: constant),
                superStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -constant),
                superStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -constant),
    
            ])
    
            // not needed
            //self.contentView.updateConstraints()
            
        }
    }
    

    And, an example view controller:

    class MahanTableViewController: UITableViewController {
        
        var myData: [String] = [
            "Saturday, 18 July 2020",
            "Sunday, 19 July 2020",
            "Wednesday, 22 July 2020",
            "Saturday, 26 September 2020",
            "Sunday, 27 September 2020",
            "Wednesday, 30 September 2020",
        ]
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            tableView.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
            
        }
        
        // MARK: - Table view data source
        
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData.count
        }
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as! TableViewCell
            
            cell.trailingLabel.text = myData[indexPath.row]
            
            return cell
        }
        
    }
    

    Producing this output:

    enter image description here