简体   繁体   中英

Swift: height of tableView row whose tableView cell has nested tableView with dynamic number of rows

I have been looking around for a solution or a best way to determine the height of a tableView row in heightForRowAt , that has a tableView based on some conditions in the data model.

When my data model has a data type called MULTISELECT , I need to display a cell with a tableView inside it. There are no problems in doing so. The inner tableView's data is assigned in outer tableView's cellForRowAt .

The question here is how to get the height of my outer tableView row for the MULTISELECT type cells, after the data is populated for the inner tableView rows?

Outer tableView code (inside a ViewController) -

public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let preferenceCategories = self.preferenceCategories else {
            return UITableViewCell()
        }
        
        let categoryCode = preferenceCategories[indexPath.section].code
        
        let filteredPreferenceSet = self.preferenceSet.filter({$0.categoryCode == categoryCode}).filter({$0.dataType == "BOOLEAN"/* || $0.dataType == "MULTISELECT"*/})
        
        
        if let preferenceDataType = filteredPreferenceSet[indexPath.row].dataType {
            if preferenceDataType == "BOOLEAN" {
                
                let cell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefSetCell", for: indexPath) as! CustPrefSetCell
                cell.preferenceName.text = filteredPreferenceSet[indexPath.row].name
                cell.preferenceDescription.text = filteredPreferenceSet[indexPath.row].description
                
                cell.switchDelegate = self
                
                let propertyValue = ((filteredPreferenceSet[indexPath.row].value ?? "false") as NSString).boolValue
                
                propertyValue ? cell.preferenceSwitch.setOn(true, animated: true) : cell.preferenceSwitch.setOn(false, animated: true)
                
                cell.preferenceCode = filteredPreferenceSet[indexPath.row].code
                
                return cell
                
            }
            
            else if preferenceDataType == "MULTISELECT" {
                
                let multiSelectCell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefMultiSelectTableViewCell", for: indexPath) as! CustPrefMultiSelectTableViewCell
                
                multiSelectCell.preferenceValues = filteredPreferenceSet[indexPath.row].preferenceValues
                
//                self.rowHeight = multiSelectCell.tableView.contentSize.height
                
                return multiSelectCell
            }
            else {
                return UITableViewCell()
            }
        }
        else {
            return UITableViewCell()
        }
    }

   public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        return UITableView.automaticDimension
    }

The inner tableView is inside the multiSelectCell , whose code is below -

class CustPrefMultiSelectTableViewCell: UITableViewCell {

    @IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var preferenceDescription: UILabel!
    @IBOutlet weak var preferenceTitle: UILabel!
    
    @IBOutlet weak var tableView: UITableView!
    
    var preferenceValues: [PreferenceValue]?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        self.tableView.delegate = self
        self.tableView.dataSource = self
        
        guard let frameworkBundle = Bundle(identifier: "com.frameworkbundle.asdf") else {
            fatalError("Framework bundle identifier is incorrect.")
        }
        
        let custPrefHeaderCell = UINib(nibName: "CustPrefMultiSelectPreferenceTableViewCell", bundle: frameworkBundle)
        self.tableView.register(custPrefHeaderCell, forCellReuseIdentifier: "CustPrefMultiSelectPreferenceTableViewCell")
        
        self.tableView.rowHeight = UITableView.automaticDimension
        self.tableView.estimatedRowHeight = 64.0
        
    }

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

        // Configure the view for the selected state
    }
}

extension CustPrefMultiSelectTableViewCell: UITableViewDataSource, UITableViewDelegate {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        guard let preferenceValues = self.preferenceValues else {
            return 0
        }
        
        return preferenceValues.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let preferenceCategories = self.preferenceValues else {
            return UITableViewCell()
        }
        
        let cell = self.tableView.dequeueReusableCell(withIdentifier: "CustPrefMultiSelectPreferenceTableViewCell", for: indexPath) as! CustPrefMultiSelectPreferenceTableViewCell
        
        cell.preferenceName.text = preferenceCategories[indexPath.row].name
        cell.preferenceDescription.text = preferenceCategories[indexPath.row].description
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }
}

I thought of an approach by having a height constraint for the inner tableView, and update the outer tableView height when it is ready/reloaded with data. But where should I implement that logic? With a fixed height of inner tableView, I get an unwanted behavior of scrolling. That need to be avoided.

How do I go further with this? Thanks in advance!

I think using nested tableView is not the best solution, anyway, I hope this example will help you.

struct Foo {
    let strings: [String]
}

class NestedViewController: UIViewController {

let dataSource = [Foo(strings: ["String1", "String2"]),
                Foo(strings: ["Long long long long long long long long long long long long long string"])]

let tableView: UITableView = {
    let tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.register(NestedCell.self, forCellReuseIdentifier: NestedCell.identifier)
    tableView.tableFooterView = UIView()
    return tableView
}()

override func viewDidLoad() {
    super.viewDidLoad()

    view.addSubview(tableView)
    setupConstraints()
    
    tableView.dataSource = self
    tableView.delegate = self
    tableView.reloadData()
}

func setupConstraints() {
    NSLayoutConstraint.activate([
        tableView.topAnchor.constraint(equalTo: view.topAnchor),
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    ])
}
}

extension NestedViewController: UITableViewDelegate & UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    dataSource.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: NestedCell.identifier, for: indexPath) as? NestedCell else {
        return UITableViewCell()
    }
    
    cell.setup(foo: dataSource[indexPath.row])
    
    return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    NestedCell.heightFor(foo: dataSource[indexPath.row])
}
}

class NestedCell: UITableViewCell {

static let identifier = "NestedCell"

let nestedTableView: UITableView = {
    let tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.register(TextCell.self, forCellReuseIdentifier: TextCell.identifier)
    tableView.tableFooterView = UIView()
    return tableView
}()

private var foo = Foo(strings: [""])

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    contentView.addSubview(nestedTableView)
    setConstraints()
    
    nestedTableView.dataSource = self
    nestedTableView.delegate = self
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func setup(foo: Foo) {
    self.foo = foo
    nestedTableView.reloadData()
}

static func heightFor(foo: Foo) -> CGFloat {
    foo.strings.reduce(0) { $0 + TextCell.heightFor(text: $1) }
}

private func setConstraints() {
    NSLayoutConstraint.activate([
        nestedTableView.topAnchor.constraint(equalTo: contentView.topAnchor),
        nestedTableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        nestedTableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
        nestedTableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
    ])
}
}

extension NestedCell: UITableViewDelegate & UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    foo.strings.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: TextCell.identifier, for: indexPath) as? TextCell else {
        return UITableViewCell()
    }
    
    cell.setup(text: foo.strings[indexPath.row])
    
    return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    TextCell.heightFor(text: foo.strings[indexPath.row])
}
}

class TextCell: UITableViewCell {

static let identifier = "TextCell"
static let labelOffset: CGFloat = 10

private let label: UILabel = {
    let label = UILabel()
    label.numberOfLines = 0
    label.font = .systemFont(ofSize: 15, weight: .medium)
    label.translatesAutoresizingMaskIntoConstraints = false
    return label
}()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    contentView.addSubview(label)
    setConstraints()
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

func setup(text: String) {
    label.text = text
}

static func heightFor(text: String) -> CGFloat {
    text.height(width: UIScreen.main.bounds.width - 2 * TextCell.labelOffset,
                       font: .systemFont(ofSize: 15, weight: .medium)) + 2 * TextCell.labelOffset
}

private func setConstraints() {
    NSLayoutConstraint.activate([
        label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: TextCell.labelOffset),
        label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -TextCell.labelOffset),
        label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: TextCell.labelOffset),
        label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -TextCell.labelOffset)
    ])
}
}

extension String {
func height(width: CGFloat, font: UIFont) -> CGFloat {
    let rect = CGSize(width: width, height: .greatestFiniteMagnitude)
    let boundingBox = self.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil)

    return ceil(boundingBox.height)
}
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM