Emre Önder
Emre Önder

Reputation: 2537

Multilinelabel inside multiple stackviews inside UITableViewCell

I have view hierarchy like below;

UITableViewCell ->
                  -> UIView -> UIStackView (axis: vertical, distribution: fill)
                    -> UIStackView (axis: horizontal, alignment: top, distribution: fillEqually)
                     -> UIView -> UIStackView(axis:vertical, distribution: fill)
                      -> TwoLabelView

My problem is that labels don't get more than one line. I read every question in SO and also tried every possibility but none of them worked. On below screenshot, on the top left box, there should be two pair of label but even one of them isn't showing.

My Question is that how can I achieve multiline in the first box (both for left and right)?

If I change top stack views distribution to fillProportionally, labels get multiline but there will be a gap between last element of first box and the box itself

image 1.1

My first top stack views

//This is the Stackview used just below UITableViewCell
private let stackView: UIStackView = {
let s = UIStackView()
s.distribution = .fill
s.axis = .vertical
s.spacing = 10
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()

//This is used to create two horizontal box next to each other
private let myStackView: UIStackView = {
let s = UIStackView()
s.distribution = .fillEqually
s.spacing = 10
s.axis = .horizontal
//s.alignment = .center
s.translatesAutoresizingMaskIntoConstraints = false
return s
}()

UILabel Class:

fileprivate class FixAutoLabel: UILabel {

override func layoutSubviews() {
    super.layoutSubviews()
    if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
        self.preferredMaxLayoutWidth = self.bounds.size.width
    }
}

}

@IBDesignable class TwoLabelView: UIView {

var topMargin: CGFloat = 0.0
var verticalSpacing: CGFloat = 3.0
var bottomMargin: CGFloat = 0.0

@IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
@IBInspectable var secondLabelText: String = "" { didSet { updateView() } }

fileprivate var firstLabel: FixAutoLabel!
fileprivate var secondLabel: FixAutoLabel!

override init(frame: CGRect) {
    super.init(frame: frame)
    setUpView()
}

required public init?(coder: NSCoder) {
    super.init(coder:coder)
    setUpView()
}

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    setUpView()
}

func setUpView() {

    firstLabel = FixAutoLabel()
    firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
    firstLabel.numberOfLines = 0
    firstLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail

    secondLabel = FixAutoLabel()
    secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
    secondLabel.numberOfLines = 1
    secondLabel.lineBreakMode = NSLineBreakMode.byTruncatingTail

    addSubview(firstLabel)
    addSubview(secondLabel)

    // we're going to set the constraints
    firstLabel .translatesAutoresizingMaskIntoConstraints = false
    secondLabel.translatesAutoresizingMaskIntoConstraints = false

    // pin both labels' left-edges to left-edge of self
    firstLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true
    secondLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 0.0).isActive = true

    // pin both labels' right-edges to right-edge of self
    firstLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true
    secondLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 0.0).isActive = true

    // pin firstLabel to the top of self + topMargin (padding)
    firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin).isActive = true

    // pin top of secondLabel to bottom of firstLabel + verticalSpacing
    secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing).isActive = true

    // pin bottom of self to bottom of secondLabel + bottomMargin (padding)
    bottomAnchor.constraint(equalTo: secondLabel.bottomAnchor, constant: bottomMargin).isActive = true

    // call common "refresh" func
    updateView()
}

func updateView() {

    firstLabel.preferredMaxLayoutWidth = self.bounds.width
    secondLabel.preferredMaxLayoutWidth = self.bounds.width

    firstLabel.text = firstLabelText
    secondLabel.text = secondLabelText

    firstLabel.sizeToFit()
    secondLabel.sizeToFit()

    setNeedsUpdateConstraints()

}

override open var intrinsicContentSize : CGSize {
    // just has to have SOME intrinsic content size defined
    // this will be overridden by the constraints
    return CGSize(width: 1, height: 1)
}
}

UIView -> UIStackView class

class ViewWithStack: UIView {

let verticalStackView: UIStackView = {
    let s = UIStackView()
    s.distribution = .fillEqually
    s.spacing = 10
    s.axis = .vertical
    s.translatesAutoresizingMaskIntoConstraints = false
    return s
}()

override init(frame: CGRect) {
    super.init(frame: frame)

    self.translatesAutoresizingMaskIntoConstraints = false
    self.backgroundColor = UIColor.white
    self.layer.cornerRadius = 6.0
    self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)

    addSubview(verticalStackView)
    let lessThan = verticalStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: 0)
    lessThan.priority = UILayoutPriority(1000)
    lessThan.isActive = true
    verticalStackView.leftAnchor.constraint(equalTo: self.leftAnchor,constant: 0).isActive = true
    verticalStackView.rightAnchor.constraint(equalTo: self.rightAnchor,constant: 0).isActive = true
    verticalStackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true

    verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
    verticalStackView.isLayoutMarginsRelativeArrangement = true
}

convenience init(orientation: NSLayoutConstraint.Axis,labelsArray: [UIView]) {
    self.init()
    verticalStackView.axis = orientation
    for label in labelsArray {
        verticalStackView.addArrangedSubview(label)
    }
}
required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
}

Example Controller Class (This is a minimized version of the whole project):

class ViewController: UIViewController, UITableViewDelegate,UITableViewDataSource {

@IBOutlet weak var tableView: UITableView!
let viewWithStack = BoxView()
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    tableView.delegate = self
    tableView.dataSource = self
    tableView.register(TableViewCell.self, forCellReuseIdentifier: "myCell")
    tableView.rowHeight = UITableView.automaticDimension

}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 2
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "myCell") as! TableViewCell

    if (indexPath.row == 0) {
        cell.setup(viewWithStack: self.viewWithStack)
    } else {
        cell.backgroundColor = UIColor.black
    }
    return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    //return 500
    if ( indexPath.row == 0) {
        return UITableView.automaticDimension
    } else {
        return 40
    }
}

}

EDIT I created a minimal project then I found that my problem is that my project implements heightForRow function which overrides UITableViewAutomaticDimension so that It gives wrong height for my component. I think I should look how to get height size of the component? because I can't delete heightForRow function which solves my problem.

Example Project Link https://github.com/emreond/tableviewWithStackView/tree/master/tableViewWithStackViewEx

Example project has ambitious layouts when you open view debugger. I think when I fix them, everything should be fine.

Upvotes: 0

Views: 845

Answers (1)

DonMag
DonMag

Reputation: 77442

Here is a full example that should do what you want (this is what I mean by a minimal reproducible example):

Best way to examine this is to:

  • create a new project
  • create a new file, named TestTableViewController.swift
  • copy and paste the code below into that file (replace the default template code)
  • add a UITableViewController to the Storyboard
  • assign its Custom Class to TestTableViewController
  • embed it in a UINavigationController
  • set the UINavigationController as Is Initial View Controller
  • run the app

This is what you should see as the result:

enter image description here

I based the classes on what you had posted (removed unnecessary code, and I am assuming you have the other cells working as desired).

//
//  TestTableViewController.swift
//
//  Created by Don Mag on 10/21/19.
//

import UIKit

class SideBySideCell: UITableViewCell {

    let horizStackView: UIStackView = {
        let v = UIStackView()
        v.axis = .horizontal
        v.alignment = .fill
        v.distribution = .fillEqually
        v.spacing = 10
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    override func prepareForReuse() {
        horizStackView.arrangedSubviews.forEach {
            $0.removeFromSuperview()
        }
    }

    func commonInit() -> Void {

        contentView.backgroundColor = UIColor(white: 0.8, alpha: 1.0)

        contentView.addSubview(horizStackView)

        let g = contentView.layoutMarginsGuide

        NSLayoutConstraint.activate([
            horizStackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            horizStackView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            horizStackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            horizStackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
        ])

    }

    func addViewWithStack(_ v: ViewWithStack) -> Void {
        horizStackView.addArrangedSubview(v)
    }

}

class TestTableViewController: UITableViewController {

    let sideBySideReuseID = "sbsID"

    override func viewDidLoad() {
        super.viewDidLoad()

        // register custom SideBySide cell for reuse
        tableView.register(SideBySideCell.self, forCellReuseIdentifier: sideBySideReuseID)

        tableView.separatorStyle = .none

    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        if indexPath.row == 0 {

            let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell

            let twoLabelView1 = TwoLabelView()
            twoLabelView1.firstLabelText = "Text for first label on left-side."
            twoLabelView1.secondLabelText = "10.765,00TL"

            let twoLabelView2 = TwoLabelView()
            twoLabelView2.firstLabelText = "Text for second-first label on left-side."
            twoLabelView2.secondLabelText = "10.765,00TL"

            let twoLabelView3 = TwoLabelView()
            twoLabelView3.firstLabelText = "Text for the first label on right-side."
            twoLabelView3.secondLabelText = "10.765,00TL"

            let leftStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView1, twoLabelView2])
            let rightStackV = ViewWithStack(orientation: .vertical, labelsArray: [twoLabelView3])

            cell.addViewWithStack(leftStackV)
            cell.addViewWithStack(rightStackV)

            return cell

        }

        // create ViewWithStack using just a simple label
        let cell = tableView.dequeueReusableCell(withIdentifier: sideBySideReuseID, for: indexPath) as! SideBySideCell

        let v = UILabel()
        v.text = "This is row \(indexPath.row)"
        let aStackV = ViewWithStack(orientation: .vertical, labelsArray: [v])
        cell.addViewWithStack(aStackV)

        return cell

    }

}

@IBDesignable class TwoLabelView: UIView {

    var topMargin: CGFloat = 0.0
    var verticalSpacing: CGFloat = 3.0
    var bottomMargin: CGFloat = 0.0

    @IBInspectable var firstLabelText: String = "" { didSet { updateView() } }
    @IBInspectable var secondLabelText: String = "" { didSet { updateView() } }

    fileprivate var firstLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    fileprivate var secondLabel: UILabel = {
        let v = UILabel()
        return v
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setUpView()
    }

    required public init?(coder: NSCoder) {
        super.init(coder:coder)
        setUpView()
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setUpView()
    }

    func setUpView() {

        firstLabel.font = UIFont.systemFont(ofSize: 18.0, weight: UIFont.Weight.bold)
        firstLabel.numberOfLines = 0

        secondLabel.font = UIFont.systemFont(ofSize: 13.0, weight: UIFont.Weight.regular)
        secondLabel.numberOfLines = 1

        addSubview(firstLabel)
        addSubview(secondLabel)

        // we're going to set the constraints
        firstLabel .translatesAutoresizingMaskIntoConstraints = false
        secondLabel.translatesAutoresizingMaskIntoConstraints = false

        // Note: recommended to use Leading / Trailing rather than Left / Right
        NSLayoutConstraint.activate([

            // pin both labels' left-edges to left-edge of self
            firstLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            secondLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),

            // pin both labels' right-edges to right-edge of self
            firstLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),
            secondLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),

            // pin firstLabel to the top of self + topMargin (padding)
            firstLabel.topAnchor.constraint(equalTo: topAnchor, constant: topMargin),

            // pin top of secondLabel to bottom of firstLabel + verticalSpacing
            secondLabel.topAnchor.constraint(equalTo: firstLabel.bottomAnchor, constant: verticalSpacing),

            // pin bottom of self to >= (bottom of secondLabel + bottomMargin (padding))
            bottomAnchor.constraint(greaterThanOrEqualTo: secondLabel.bottomAnchor, constant: bottomMargin),

        ])

    }

    func updateView() -> Void {
        firstLabel.text = firstLabelText
        secondLabel.text = secondLabelText
    }

}

class ViewWithStack: UIView {

    let verticalStackView: UIStackView = {
        let s = UIStackView()
        s.distribution = .fill
        s.spacing = 10
        s.axis = .vertical
        s.translatesAutoresizingMaskIntoConstraints = false
        return s
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.translatesAutoresizingMaskIntoConstraints = false
        self.backgroundColor = UIColor.white
        self.layer.cornerRadius = 6.0
//      self.layer.applySketchShadow(color: UIColor(red:0.56, green:0.56, blue:0.56, alpha:1), alpha: 0.2, x: 0, y: 0, blur: 10, spread: 0)

        addSubview(verticalStackView)

        NSLayoutConstraint.activate([

            // constrain to all 4 sides
            verticalStackView.topAnchor.constraint(equalTo: topAnchor, constant: 0.0),
            verticalStackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0.0),
            verticalStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0.0),
            verticalStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0.0),

        ])

        verticalStackView.layoutMargins = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
        verticalStackView.isLayoutMarginsRelativeArrangement = true

    }

    convenience init(orientation: NSLayoutConstraint.Axis, labelsArray: [UIView]) {
        self.init()
        verticalStackView.axis = orientation
        for label in labelsArray {
            verticalStackView.addArrangedSubview(label)
        }
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Upvotes: 3

Related Questions