Search code examples
swiftuitableviewautolayout

UITableViewCell changeable content based data coming from server


I am struggling this issue which is related to UITableViewCell. I have a subclass UITableViewCell called ApplicantMessageCell and it has some subviews, labels, imageviews etc. Top part of it does not depend on the state. Just gets the data, changes labels text and imageView's image.

However for the bottom part I have completely different 3 subclasses of UIView for each state coming in. I need to show related UIView subclass at the bottom part of ApplicationMessageCell. But I could not find a way to do it.

  • Of course, I could create different UITableViewCell subclasses for each state but I didnot want to go that road beacuse this is just one case, I have more.
  • I tried to create a subclass of UIView which will behave like UILabel when it comes to resizing itself. I could not manage to it.
  • Lastly, I know adding each UIView subclass regarding each state and explicitly showing the one/hiding rest would solve it but I believe there are better ways to achieve this.

I did not share any code because I think this more of a theoretical question, but of course I will if anyone requests.

Thanks in advance.


Solution

  • Here is a quick example...

    The cell class has two labels, a stack view, and 3 views (red, green, blue) with varying heights to use as the "show or not" views:

    • First label is constrained to the Top
    • Second label is constrained to the bottom of First label
    • stack view is constrained to the bottom of Second label and to the bottom of the cell (contentView, of course)

    Three views of varying heights are then added to the stack view. Presumably, the constraints on the subviews of your different views will determine their respective heights. For this example, they are set to 40, 80 and 160.

    Review the comments in the following code - it should be pretty self-explanatory:

    class ApplicantMessageCell: UITableViewCell {
        
        let titleLabel = UILabel()
        let subLabel = UILabel()
        
        let stackView = UIStackView()
        
        let viewA = UIView()
        let viewB = UIView()
        let viewC = UIView()
    
        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() -> Void {
            
            [titleLabel, subLabel, stackView].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                contentView.addSubview(v)
            }
            
            let g = contentView.layoutMarginsGuide
    
            NSLayoutConstraint.activate([
                
                // constrain titleLabel at top
                titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
                titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                titleLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                
                // subLabel 8-pts below titleLabel
                subLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8.0),
                subLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                subLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor),
    
                // stackView 8-pts below subLabel
                stackView.topAnchor.constraint(equalTo: subLabel.bottomAnchor, constant: 8.0),
                stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
                stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
                
            ])
            
            // constrain stackView bottom to bottom
            // this will avoid auto-layout complaints while the cells are configured
            let c = stackView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
            c.priority = .defaultHigh
            c.isActive = true
            
            // UI element properties
            stackView.axis = .vertical
            stackView.spacing = 8
            
            titleLabel.backgroundColor = .yellow
            subLabel.backgroundColor = .cyan
            
            viewA.backgroundColor = .red
            viewB.backgroundColor = .green
            viewC.backgroundColor = .blue
    
            // you'll be filling the views with something to determine their heights
            //  but here we'll just set them to 40, 80 and 160 pts
            
            for (v, h) in zip([viewA, viewB, viewC], [40.0, 80.0, 160.0]) {
                stackView.addArrangedSubview(v)
                v.heightAnchor.constraint(equalToConstant: CGFloat(h)).isActive = true
            }
            
        }
        
        func fillData(_ top: String, sub: String, showViews: [Bool]) -> Void {
            titleLabel.text = top
            subLabel.text = sub
            // hide views as defined in showViews array
            for (v, b) in zip(stackView.arrangedSubviews, showViews) {
                v.isHidden = !b
            }
        }
        
    }
    
    struct ApplicationStruct {
        var title: String = ""
        var subTitle: String = ""
        var showViews: [Bool] = [true, true, true]
    }
    
    class FarukTableViewController: UITableViewController {
    
        var theData: [ApplicationStruct] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            for i in 0..<20 {
                // cycle through views 1, 2, 3
                let b1 = i % 3 == 0
                let b2 = i % 3 == 1
                let b3 = i % 3 == 2
                let a = [b1, b2, b3]
                let d = ApplicationStruct(title: "Title \(i)", subTitle: "", showViews: a)
                theData.append(d)
            }
            
            // just to test, set more than one view visible in a couple cells
            theData[11].showViews = [true, false, true] // red and blue
            theData[12].showViews = [false, true, true] // green and blue
            theData[13].showViews = [true, true, false] // red and green
            theData[14].showViews = [true, true, true]  // all three
    
            tableView.register(ApplicantMessageCell.self, forCellReuseIdentifier: "cell")
        }
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return theData.count
        }
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ApplicantMessageCell
            
            let d = theData[indexPath.row]
            
            let subStr = "showViews: " + d.showViews.description
            
            c.fillData(d.title, sub: subStr, showViews: d.showViews)
            
            return c
        }
        
    }
    

    Result where first row shows "ViewType1" second row shows "ViewType2" and third row shows "ViewType3" ... then the rows cycle, until we hit row "Title 11" where we've set a few rows to show more than one of the "subview types":

    enter image description here