Search code examples
swiftuitableviewuikituilabel

2 UILabels placed horizontally in a table view cell


I am working on a table view and in the cell I need to create 2 labels horizontally. The first label has fixed content "Location Name:". The second label will have dynamic content and it can be multiple lines. When I first get to the screen, my cell looks like this:

first get in screen

But when I scroll down and scroll back up, it looks fine:

enter image description here

Here's my code:

var locationNameLabel: UILabel = {
    let label = UILabel()
    label.text = "Location Name:"
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = .systemFont(ofSize: 18, weight: .bold)
    label.numberOfLines = 0
    return label
}()

var locationNameContent: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.font = .systemFont(ofSize: 18, weight: .regular)
    label.numberOfLines = 0
    label.lineBreakMode = .byWordWrapping
    return label
}()

override func setSelected(_ selected: Bool, animated: Bool) {
    super.setSelected(selected, animated: animated)
    setupUI()
}

func setupUI() {

    let locationNameView = createLocationNameContainer(label: locationNameLabel, labelContent: locationNameContent)
    contentView.addSubview(locationNameView)
    contentView.addSubview(locationImage)

    NSLayoutConstraint.activate([
        locationImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
        locationImage.widthAnchor.constraint(equalToConstant: 200),
        locationImage.heightAnchor.constraint(equalToConstant: 200),
        locationImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
        locationNameView.topAnchor.constraint(equalTo: locationImage.bottomAnchor, constant: 15),
        locationNameView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
        locationNameView.widthAnchor.constraint(equalToConstant: contentView.frame.width - 50),
        locationNameView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5)
    ])
}

func createLocationNameContainer(label: UILabel, labelContent: UILabel) -> UIView {

    let containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    containerView.backgroundColor = .green
    containerView.addSubview(label)
    containerView.addSubview(labelContent)
    NSLayoutConstraint.activate([
        label.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5),
        label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 5),
        label.firstBaselineAnchor.constraint(equalTo: labelContent.firstBaselineAnchor),
        labelContent.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5),
        labelContent.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 5),
        labelContent.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -5),
        labelContent.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -5)
    ])
    return containerView
}

func configure(model: LocationAndPhotoModel) {

    // get the first oject of "data" array of PhotoModel
    let imageURL = model.locationPhotoModel.data.first?.images.medium.url ?? ""
    locationImage.sd_setImage(with: URL(string: imageURL))
    
    // get location name
    locationNameContent.text = model.locationDetail.name
    // get location address
    locationAddressContent.text = model.locationDetail.address_obj.address_string
}

Here's how I implement my tableView:

view.addSubview(tableView)

NSLayoutConstraint.activate([
    tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
    tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
    tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0),
    tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
])

extension SearchResultViewController: UITableViewDelegate, UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return arrayOfLocationsAndPhotos?.count ?? 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultTableViewCell.identifier, for: indexPath) as? SearchResultTableViewCell else {
            return UITableViewCell()
        }
        guard self.arrayOfLocationsAndPhotos != nil else {return UITableViewCell()}
        guard let model = arrayOfLocationsAndPhotos?[indexPath.row] else {return UITableViewCell()}
        cell.configure(model: model)
        return cell
    }
}

Solution

  • First, in your cell class, setSelected(...) will be called many times. Every time a cell is displayed... every time a cell is selected or deselected... etc. So you do NOT want to be adding subviews there.

    Instead, setup the UI elements when the cell is init'd:

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupUI()
    }
    

    Next, you can easily accomplish your "two-label" layout with a horizontal UIStackView.

    Here is your code - modified... I don't have your data, so I used a simple struct with just the "name" string:

    struct LocationAndPhotoModel {
        var name: String = ""
    }
    
    class SearchResultTableViewCell: UITableViewCell {
        
        static let identifier: String = "SearchResultTableViewCell"
        
        var locationNameLabel: UILabel = {
            let label = UILabel()
            label.text = "Location Name:"
            label.translatesAutoresizingMaskIntoConstraints = false
            label.font = .systemFont(ofSize: 18, weight: .bold)
            // we don't want to allow the label to be squeezed or stretched
            label.setContentHuggingPriority(.required, for: .horizontal)
            label.setContentCompressionResistancePriority(.required, for: .horizontal)
            return label
        }()
        
        var locationNameContent: UILabel = {
            let label = UILabel()
            label.translatesAutoresizingMaskIntoConstraints = false
            label.font = .systemFont(ofSize: 18, weight: .regular)
            label.numberOfLines = 0
            label.lineBreakMode = .byWordWrapping
            // we don't want to allow the label to be squeezed or stretched
            //  but this is the multiline label, so we ue required - 1
            label.setContentHuggingPriority(.required - 1, for: .horizontal)
            label.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
            return label
        }()
        
        var locationImage: UIImageView = {
            let imgView = UIImageView()
            imgView.translatesAutoresizingMaskIntoConstraints = false
            imgView.backgroundColor = .systemBlue
            return imgView
        }()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            setupUI()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setupUI()
        }
        
        // DO NOT implement setSelected, unless you want to customize the
        //  appearance when a cell is selected
        //override func setSelected(_ selected: Bool, animated: Bool) {
        //  super.setSelected(selected, animated: animated)
        //
        //  // DEFINITELY do not do this here!!!
        //  //setupUI()
        //}
        
        func setupUI() {
            
            let locationNameView = createLocationNameContainer(label: locationNameLabel, labelContent: locationNameContent)
            contentView.addSubview(locationNameView)
            contentView.addSubview(locationImage)
            
            NSLayoutConstraint.activate([
                
                locationImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
                locationImage.widthAnchor.constraint(equalToConstant: 200),
                
                // I made the image view shorter so we can see more rows
                //  change back to 200 when ready
                locationImage.heightAnchor.constraint(equalToConstant: 100),
                
                locationImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
    
                locationNameView.topAnchor.constraint(equalTo: locationImage.bottomAnchor, constant: 15),
                locationNameView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
                locationNameView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
    
                // keep at least 5-points on each side
                locationNameView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.leadingAnchor, constant: 5),
                locationNameView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -5),
                
            ])
        }
        
        func createLocationNameContainer(label: UILabel, labelContent: UILabel) -> UIView {
            
            let containerView = UIView()
            containerView.translatesAutoresizingMaskIntoConstraints = false
            containerView.backgroundColor = .green
            
            let stackView = UIStackView()
            stackView.axis = .horizontal
            // alignment top, so the left-side label will stay at the top
            //  when the right-side label has multiple lines
            stackView.alignment = .top
            stackView.spacing = 8
            
            // add the labels to the stack view
            stackView.addArrangedSubview(label)
            stackView.addArrangedSubview(labelContent)
            
            stackView.translatesAutoresizingMaskIntoConstraints = false
            containerView.addSubview(stackView)
    
            NSLayoutConstraint.activate([
                stackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 5),
                stackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 5),
                stackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -5),
                stackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -5)
            ])
            
            return containerView
        }
        
        func configure(model: LocationAndPhotoModel) {
            
            // get the first oject of "data" array of PhotoModel
            //let imageURL = model.locationPhotoModel.data.first?.images.medium.url ?? ""
            //locationImage.sd_setImage(with: URL(string: imageURL))
            
            // get location name
            locationNameContent.text = model.name // model.locationDetail.name
            
        }
        
    }
    
    class SearchResultViewController: UIViewController {
        
        let tableView = UITableView()
        
        var arrayOfLocationsAndPhotos: [LocationAndPhotoModel] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            tableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(tableView)
            
            NSLayoutConstraint.activate([
                tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
                tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
                tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0),
                tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0)
            ])
            
            tableView.register(SearchResultTableViewCell.self, forCellReuseIdentifier: SearchResultTableViewCell.identifier)
            tableView.dataSource = self
            tableView.delegate = self
    
            // let's add some sample data
            var m = LocationAndPhotoModel(name: "Short")
            arrayOfLocationsAndPhotos.append(m)
    
            m = LocationAndPhotoModel(name: "Longer Name Here")
            arrayOfLocationsAndPhotos.append(m)
    
            m = LocationAndPhotoModel(name: "Long enough name that it will probably need to wrap onto multiple lines.")
            arrayOfLocationsAndPhotos.append(m)
    
            m = LocationAndPhotoModel(name: "Medium Name")
            arrayOfLocationsAndPhotos.append(m)
            
            // let's repeat those 3 times so we have enough to scroll
            for _ in 1...3 {
                arrayOfLocationsAndPhotos.append(contentsOf: arrayOfLocationsAndPhotos)
            }
        }
    }
    
    extension SearchResultViewController: UITableViewDelegate, UITableViewDataSource {
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return arrayOfLocationsAndPhotos.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: SearchResultTableViewCell.identifier, for: indexPath) as! SearchResultTableViewCell
            cell.configure(model: arrayOfLocationsAndPhotos[indexPath.row])
            return cell
        }
    }
    

    When run, it will look like this:

    enter image description here

    I don't have your images, so the image view is blue, and I set its height to 100 so we can see more rows.