How can I set dynamic height of textview inside of stackview and tableviewcell?

89 Views Asked by At

I'm working on to-do app but I don't know what to do. I already did every single solution of it. (isScrollEnabled = false, .sizetofit(), translaste~ =true ...)

How can I set dynamic height of textview inside of stackview and tableviewcell? I want to basic height of textview greater than 50, and then change the textview height dynamically.

I attached my code below.

class ToDoTableViewCell: UITableViewCell, UITextViewDelegate {
    
    lazy var backColorView: UIView = {
        let view = UIView()
        view.backgroundColor = .yellow
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    let memoString: UITextView = {
        let textView = UITextView()
        textView.font = .systemFont(ofSize: 17, weight: .regular)
        textView.layer.cornerRadius = 0
        textView.layer.borderWidth = 1
        textView.layer.borderColor = #colorLiteral(red: 0.2588235438, green: 0.7568627596, blue: 0.9686274529, alpha: 1)
        textView.backgroundColor = .clear
        textView.isS
        return textView
    }()
    
    let dateString: UILabel = {
        let date = UILabel()
        date.font = .systemFont(ofSize: 14, weight: .light)
        date.text = "2021-11-12"
        return date
    }()
    
    let updateButton: UIButton = {
        let button = UIButton()
        button.setTitle("UPDATE", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 9, weight: .bold)
        button.layer.cornerRadius = 10
        button.backgroundColor = .gray
        button.setImage(UIImage(systemName: "pencil"), for: .normal)
        return button
    }()
    
    let totalStackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        stack.distribution  = .fill
        stack.alignment = .fill
        stack.spacing = 10
        return stack
    }()
    
    let subStackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        stack.distribution  = .fill
        stack.alignment = .fill
        stack.spacing = 0
        return stack
    }()
    
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        memoString.delegate = self

    }

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

        // Configure the view for the selected state
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: .default, reuseIdentifier: reuseIdentifier)
        setupStackView()
        setConstraints()
        

    }

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

    func setupStackView() {
        
        self.contentView.addSubview(backColorView)
        
        totalStackView.addArrangedSubview(memoString)
        totalStackView.addArrangedSubview(subStackView)
        
        subStackView.addArrangedSubview(dateString)
        subStackView.addArrangedSubview(updateButton)
        
        self.contentView.addSubview(totalStackView)
        
    }
    
    
    func setConstraints() {
        totalStackView.translatesAutoresizingMaskIntoConstraints = false
        subStackView.translatesAutoresizingMaskIntoConstraints = false
        updateButton.translatesAutoresizingMaskIntoConstraints = false
        memoString.translatesAutoresizingMaskIntoConstraints = false
       
        
        
        
        
        NSLayoutConstraint.activate([
            
            totalStackView.leadingAnchor.constraint(equalTo: backColorView.leadingAnchor, constant: 10),
            totalStackView.trailingAnchor.constraint(equalTo: backColorView.trailingAnchor, constant: -10),
            totalStackView.topAnchor.constraint(equalTo: backColorView.topAnchor, constant: 10),
            totalStackView.bottomAnchor.constraint(equalTo: backColorView.bottomAnchor, constant: -10),
            
            
            subStackView.heightAnchor.constraint(equalToConstant: 30),
            updateButton.widthAnchor.constraint(equalToConstant: 80),
            
            backColorView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 25),
            backColorView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -25),
            backColorView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
            backColorView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -10),
            
           memoString.heightAnchor.constraint(greaterThanOrEqualToConstant: 50)
           
 
        ])
    }
}
class ViewController: UIViewController {

    private let tableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        subviews()
        constraints()
        setupTableView()
        makeUI()
        makeButton()
    }

    func setupTableView() {
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(ToDoTableViewCell.self, forCellReuseIdentifier: "cell")
    }
}

extension ViewController {
    
    func makeUI() {
            let navigationBarAppearance = UINavigationBarAppearance()
            navigationBarAppearance.configureWithOpaqueBackground()
            navigationController?.navigationBar.standardAppearance = navigationBarAppearance
            navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
            navigationController?.navigationBar.tintColor = .blue

            navigationItem.scrollEdgeAppearance = navigationBarAppearance
            navigationItem.standardAppearance = navigationBarAppearance
            navigationItem.compactAppearance = navigationBarAppearance

            navigationController?.setNeedsStatusBarAppearanceUpdate()
                    

            navigationController?.navigationBar.isTranslucent = false
            navigationController?.navigationBar.prefersLargeTitles = true
            //navigationController?.navigationBar.backgroundColor = .white
            title = "메모"
        }
    
    func makeButton() {
            let button = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(action))
            navigationItem.rightBarButtonItem = button
        }
    
    @objc func action() {
        
    }
    
    func subviews() {
        view.addSubview(tableView)
    }

    func constraints() {
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return UITableView.automaticDimension
        }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        return cell
    }
 


}

I want the basic height of the textview to be greater than 50, and then change the textview height dynamically.

isScrollEnabled = false, .sizetofit(), translaste~ =true ...

2

There are 2 best solutions below

0
On

Couple things...

awakeFromNib() is only called on cells that are loaded from Storyboard Prototype or a XIB, so we can remove that. Set the text view's delegate in init(style: UITableViewCell.CellStyle, reuseIdentifier: String?).

You DO want to set .isScrollEnabled = false on your text view. This will cause it to automatically adjust its height to fit the text.

When constraints are setup correctly (as you've done), there is no need to implement heightForRowAt -- in fact, that is counter-productive.

When a cell's height is changed dynamically while it is displayed, we want to call tableView.performBatchUpdates(nil) to tell the table view to re-layout its cells. A very common way to do that is by setting a Closure in the cell that will "call back" to the controller.

We can implement textViewDidChange in the cell class where we will call that Closure.


So, as a starter, in your cell class we add this property:

var basicClosure: (() -> ())?

and implement:

func textViewDidChange(_ textView: UITextView) {
    // inform the controller that the memo text changed
    basicClosure?()
}

Then in the controller's cellForItemAt we set the closure:

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

    cell.basicClosure = {
        self.tableView.performBatchUpdates(nil)
    }
    
    return cell
}

Now, as you edit the text in a cell's text view, its height (along with the cell it is in) will automatically adjust.


We need to do a couple additional things though, because we need to save the edited text ... if we don't, then any edits will be lost when we scroll the cell out of view.

So, let's change the Closure to this:

// Closure so we can inform the controller when the text is edited
public var memoEdited: ((UITableViewCell, String) -> ())?

and implement that in textViewDidChange:

func textViewDidChange(_ textView: UITextView) {
    // inform the controller that the memo text changed
    memoEdited?(self, textView.text ?? "")
}

then we'll setup that Closure like this:

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

    // assuming we have a data source with a ",memo" String property
    cell.memoString.text = myData[indexPath.row].memo
    
    cell.memoEdited = { [weak self] theCell, theText in
        guard let self = self,
              let idx = self.tableView.indexPath(for: theCell)
        else { return }
        // update the data with the edited text
        myData[idx.row].memo = theText
        // tell the table view to re-layout its cells
        self.tableView.performBatchUpdates(nil)
    }

    return cell
}

Here is the code you posted, edited to include those changes:

/// basic data structure
struct ToDoStruct {
    var date: Date = Date()
    var memo: String = ""
}

class ToDoTableViewCell: UITableViewCell, UITextViewDelegate {
    
    // Closure so we can inform the controller when the text is edited
    public var memoEdited: ((UITableViewCell, String) -> ())?

    lazy var backColorView: UIView = {
        let view = UIView()
        view.backgroundColor = .yellow
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    let memoString: UITextView = {
        let textView = UITextView()
        textView.font = .systemFont(ofSize: 17, weight: .regular)
        textView.layer.cornerRadius = 0
        textView.layer.borderWidth = 1
        textView.layer.borderColor = #colorLiteral(red: 0.2588235438, green: 0.7568627596, blue: 0.9686274529, alpha: 1)
        textView.backgroundColor = .clear
        // disable scrolling
        textView.isScrollEnabled = false
        return textView
    }()
    
    let dateString: UILabel = {
        let date = UILabel()
        date.font = .systemFont(ofSize: 14, weight: .light)
        date.text = "2021-11-12"
        return date
    }()
    
    let updateButton: UIButton = {
        let button = UIButton()
        button.setTitle("UPDATE", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 9, weight: .bold)
        button.layer.cornerRadius = 10
        button.backgroundColor = .gray
        button.setImage(UIImage(systemName: "pencil"), for: .normal)
        return button
    }()
    
    let totalStackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .vertical
        stack.distribution  = .fill
        stack.alignment = .fill
        stack.spacing = 10
        return stack
    }()
    
    let subStackView: UIStackView = {
        let stack = UIStackView()
        stack.axis = .horizontal
        stack.distribution  = .fill
        stack.alignment = .fill
        stack.spacing = 0
        return stack
    }()

    // this is not called when using a cell that is
    //  NOT coming from a Storyboard Prototype or XIB
    //override func awakeFromNib() {
    //  super.awakeFromNib()
    //  // Initialization code
    //  memoString.delegate = self
    //}
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        
        // Configure the view for the selected state
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: .default, reuseIdentifier: reuseIdentifier)
        setupStackView()
        setConstraints()
        
        memoString.delegate = self
    }
    
    func textViewDidChange(_ textView: UITextView) {
        // inform the controller that the memo text changed
        memoEdited?(self, textView.text ?? "")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupStackView() {
        
        self.contentView.addSubview(backColorView)
        
        totalStackView.addArrangedSubview(memoString)
        totalStackView.addArrangedSubview(subStackView)
        
        subStackView.addArrangedSubview(dateString)
        subStackView.addArrangedSubview(updateButton)
        
        self.contentView.addSubview(totalStackView)
        
    }
    
    
    func setConstraints() {
        totalStackView.translatesAutoresizingMaskIntoConstraints = false
        subStackView.translatesAutoresizingMaskIntoConstraints = false
        updateButton.translatesAutoresizingMaskIntoConstraints = false
        memoString.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            
            totalStackView.leadingAnchor.constraint(equalTo: backColorView.leadingAnchor, constant: 10),
            totalStackView.trailingAnchor.constraint(equalTo: backColorView.trailingAnchor, constant: -10),
            totalStackView.topAnchor.constraint(equalTo: backColorView.topAnchor, constant: 10),
            totalStackView.bottomAnchor.constraint(equalTo: backColorView.bottomAnchor, constant: -10),
            
            
            subStackView.heightAnchor.constraint(equalToConstant: 30),
            updateButton.widthAnchor.constraint(equalToConstant: 80),
            
            backColorView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 25),
            backColorView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -25),
            backColorView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 10),
            backColorView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -10),
            
            memoString.heightAnchor.constraint(greaterThanOrEqualToConstant: 50)
            
            
        ])
    }
}

class TestToDoViewController: UIViewController {
    
    var myData: [ToDoStruct] = []
    
    private let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // generate some sample data
        var d: Date = Date()
        for i in 0..<10 {
            myData.append(ToDoStruct(date: d, memo: "Memo item \(i)"))
            // add 1 day
            d = d.addingTimeInterval(60 * 60 * 24)
        }
        
        tableView.delegate = self
        tableView.dataSource = self
        subviews()
        constraints()
        setupTableView()
        makeUI()
        makeButton()
        
    }
    
    func setupTableView() {
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(ToDoTableViewCell.self, forCellReuseIdentifier: "cell")
        
        // we probably want to dismiss the keyboard when scrolling
        tableView.keyboardDismissMode = .onDrag

    }
}

extension TestToDoViewController {
    
    func makeUI() {
        let navigationBarAppearance = UINavigationBarAppearance()
        navigationBarAppearance.configureWithOpaqueBackground()
        navigationController?.navigationBar.standardAppearance = navigationBarAppearance
        navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
        navigationController?.navigationBar.tintColor = .blue
        
        navigationItem.scrollEdgeAppearance = navigationBarAppearance
        navigationItem.standardAppearance = navigationBarAppearance
        navigationItem.compactAppearance = navigationBarAppearance
        
        navigationController?.setNeedsStatusBarAppearanceUpdate()
        
        
        navigationController?.navigationBar.isTranslucent = false
        navigationController?.navigationBar.prefersLargeTitles = true
        //navigationController?.navigationBar.backgroundColor = .white
        title = "메모"
    }
    
    func makeButton() {
        let button = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(action))
        navigationItem.rightBarButtonItem = button
    }
    
    @objc func action() {
        
    }
    
    func subviews() {
        view.addSubview(tableView)
    }
    
    func constraints() {
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
            tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
}

extension TestToDoViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    // do NOT implement heightForRowAt
    //func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    //  return UITableView.automaticDimension
    //}
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ToDoTableViewCell

        let df = DateFormatter()
        df.dateFormat = "YYYY-MM-dd"
        
        cell.dateString.text = df.string(from: myData[indexPath.row].date)

        cell.memoString.text = myData[indexPath.row].memo
        
        cell.memoEdited = { [weak self] theCell, theText in
            guard let self = self,
                  let idx = self.tableView.indexPath(for: theCell)
            else { return }
            // update the data with the edited text
            myData[idx.row].memo = theText
            // tell the table view to re-layout its cells
            self.tableView.performBatchUpdates(nil)
        }

        return cell
    }
    
}

You have a button labeled "Update" in your cell, but it's not clear what you want to do with that. If you want to use that to, for example, Save the edited "memo" to storage, you'd want to add another Closure to handle the "Update" button tap.

0
On

You can get the height of the UITextView with given width considering contents.

func textViewHeight(textView: UITextView) -> CGFloat {
    let width = textView.frame.width
    let fitSize = textView.sizeThatFits(.init(width: width, height: .greatestFiniteMagnitude))
    return fitSize.height
}

After get height from calling this at textViewDidChange(_:), then you could update the height constraints of the textView.