Elizabeth
Elizabeth

Reputation: 153

Increase UIView Height Programmatically Based on Changing SubView Height

I have been unable to figure out how to solve this issue of mine. I tried to follow the answer from this post. How to change UIView height based on elements inside it

Like the post answer says to do, I have:

I have to do this all programmatically. I first set the frame for the container view and give it a specified height. I'm not sure if that is okay too. I also add (#1) in viewDidLoad and am unsure if that's correct.

The text view is not able to increase height either with the current constraints (it is able to if I remove the topAnchor constraint but the container view still doesn't change size).

class ChatController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

lazy var containerView: UIView = {
    let containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    containerView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height * 0.075)
    return containerView
}()

lazy var textView: UITextView = {
    let textView = UITextView()
    textView.text = "Enter message..."
    textView.isScrollEnabled = false
    textView.translatesAutoresizingMaskIntoConstraints = false
    textView.delegate = self
    return textView
}()

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    textViewDidChange(self.textView)
    addContainerSubViews()
    (#1)
    containerView.topAnchor.constraint(equalTo: self.textView.topAnchor, constant: -UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true
    containerView.bottomAnchor.constraint(equalTo: self.textView.bottomAnchor, constant: UIScreen.main.bounds.size.height * 0.075 * 0.2).isActive = true

}

func addContainerSubViews() {
    
    let height = UIScreen.main.bounds.size.height
    let width = UIScreen.main.bounds.size.width
    let containerHeight = height * 0.075
    
    ...//constraints for imageView and sendButton...

    containerView.addSubview(self.textView)
    self.textView.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: width/20).isActive = true
    self.textView.rightAnchor.constraint(equalTo: sendButton.leftAnchor, constant: -width/20).isActive = true
    (#2)
    self.textView.heightAnchor.constraint(equalToConstant: containerHeight * 0.6).isActive = true

}

override var inputAccessoryView: UIView? {
    get {
        return containerView
    }
}

(#3)
func textViewDidChange(_ textView: UITextView) {
    let size = CGSize(width: view.frame.width, height: .infinity)
    let estimatedSize = textView.sizeThatFits(size)
    textView.constraints.forEach { (constraint) in
        if constraint.firstAttribute == .height {
            constraint.constant = estimatedSize.height
        }
    }
}

enter image description here

Upvotes: 0

Views: 1784

Answers (1)

DonMag
DonMag

Reputation: 77690

You can do this all with auto-layout / constraints. Because a UITextView with scrolling disabled will "auto-size" its height based on the text, no need to calculate height and change constraint constant.

Here's an example -- it's from a previous answer, modified to include your image view and send button:

class ViewController: UIViewController {
    
    let testLabel: InputLabel = InputLabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let instructionLabel = UILabel()
        instructionLabel.textAlignment = .center
        instructionLabel.text = "Tap yellow label to edit..."
        
        let centeringFrameView = UIView()
        
        // label properties
        let fnt: UIFont = .systemFont(ofSize: 32.0)
        testLabel.isUserInteractionEnabled = true
        testLabel.font = fnt
        testLabel.adjustsFontSizeToFitWidth = true
        testLabel.minimumScaleFactor = 0.25
        testLabel.numberOfLines = 2
        testLabel.setContentHuggingPriority(.required, for: .vertical)
        let minLabelHeight = ceil(fnt.lineHeight)
        
        // so we can see the frames
        centeringFrameView.backgroundColor = .red
        testLabel.backgroundColor = .yellow
        
        [centeringFrameView, instructionLabel, testLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        view.addSubview(instructionLabel)
        view.addSubview(centeringFrameView)
        centeringFrameView.addSubview(testLabel)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            // instruction label centered at top
            instructionLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            instructionLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            // centeringFrameView 20-pts from instructionLabel bottom
            centeringFrameView.topAnchor.constraint(equalTo: instructionLabel.bottomAnchor, constant: 20.0),
            // Leading / Trailing with 20-pts "padding"
            centeringFrameView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            centeringFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            
            // test label centered vertically in centeringFrameView
            testLabel.centerYAnchor.constraint(equalTo: centeringFrameView.centerYAnchor, constant: 0.0),
            // Leading / Trailing with 20-pts "padding"
            testLabel.leadingAnchor.constraint(equalTo: centeringFrameView.leadingAnchor, constant: 20.0),
            testLabel.trailingAnchor.constraint(equalTo: centeringFrameView.trailingAnchor, constant: -20.0),
            
            // height will be zero if label has no text,
            //  so give it a min height of one line
            testLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: minLabelHeight),
            
            // centeringFrameView height = 3 * minLabelHeight
            centeringFrameView.heightAnchor.constraint(equalToConstant: minLabelHeight * 3.0)
        ])
        
        // to handle user input
        testLabel.editCallBack = { [weak self] str in
            guard let self = self else { return }
            self.testLabel.text = str
        }
        testLabel.doneCallBack = { [weak self] in
            guard let self = self else { return }
            // do something when user taps done / enter
        }
        
        let t = UITapGestureRecognizer(target: self, action: #selector(self.labelTapped(_:)))
        testLabel.addGestureRecognizer(t)
        
    }
    
    @objc func labelTapped(_ g: UITapGestureRecognizer) -> Void {
        testLabel.becomeFirstResponder()
        testLabel.inputContainerView.theTextView.text = testLabel.text
        testLabel.inputContainerView.theTextView.becomeFirstResponder()
    }
    
}

class InputLabel: UILabel {
    
    var editCallBack: ((String) -> ())?
    var doneCallBack: (() -> ())?
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    override var canResignFirstResponder: Bool {
        return true
    }
    override var inputAccessoryView: UIView? {
        get { return inputContainerView }
    }
    
    lazy var inputContainerView: CustomInputAccessoryView = {
        let v = CustomInputAccessoryView()
        v.editCallBack = { [weak self] str in
            guard let self = self else { return }
            self.editCallBack?(str)
        }
        v.doneCallBack = { [weak self] in
            guard let self = self else { return }
            self.resignFirstResponder()
        }
        return v
    }()

}

class CustomInputAccessoryView: UIView, UITextViewDelegate {
    
    var editCallBack: ((String) -> ())?
    var doneCallBack: (() -> ())?
    
    let theTextView: UITextView = {
        let tv = UITextView()
        tv.isScrollEnabled = false
        tv.font = .systemFont(ofSize: 16)
        tv.autocorrectionType = .no
        tv.returnKeyType = .done
        return tv
    }()
    
    let imgView: UIImageView = {
        let v = UIImageView()
        v.contentMode = .scaleAspectFit
        v.clipsToBounds = true
        return v
    }()
    
    let sendButton: UIButton = {
        let v = UIButton()
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .lightGray
        autoresizingMask = [.flexibleHeight, .flexibleWidth]

        if let img = UIImage(named: "testImage") {
            imgView.image = img
        } else {
            imgView.backgroundColor = .systemBlue
        }
        
        let largeConfig = UIImage.SymbolConfiguration(pointSize: 22, weight: .regular, scale: .large)
        let buttonImg = UIImage(systemName: "paperplane.fill", withConfiguration: largeConfig)
        sendButton.setImage(buttonImg, for: .normal)
        
        [theTextView, imgView, sendButton].forEach { v in
            addSubview(v)
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // if we want to see the image view and button frames
        //[imgView, sendButton].forEach { v in
        //  v.backgroundColor = .systemYellow
        //}
        
        NSLayoutConstraint.activate([
            
            // constrain image view 40x40 with 8-pts leading
            imgView.widthAnchor.constraint(equalToConstant: 40.0),
            imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor),
            imgView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8.0),

            // constrain image view 40x40 with 8-pts trailing
            sendButton.widthAnchor.constraint(equalToConstant: 40.0),
            sendButton.heightAnchor.constraint(equalTo: sendButton.widthAnchor),
            sendButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8.0),

            // constrain text view with 10-pts from
            //  image view trailing
            //  send button leading
            theTextView.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 10),
            theTextView.trailingAnchor.constraint(equalTo: sendButton.leadingAnchor, constant: -10),

            // constrain image view and button
            //  centered vertically
            //  at least 8-pts top and bottom
            imgView.centerYAnchor.constraint(equalTo: centerYAnchor),
            sendButton.centerYAnchor.constraint(equalTo: centerYAnchor),
            imgView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
            sendButton.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 8.0),
            imgView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),
            sendButton.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -8.0),

            // constrain text view 8-pts top/bottom
            theTextView.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
            theTextView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),

        ])
        
        theTextView.delegate = self
    }
    
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if (text == "\n") {
            textView.resignFirstResponder()
            doneCallBack?()
        }
        return true
    }
    func textViewDidChange(_ textView: UITextView) {
        editCallBack?(textView.text ?? "")
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override var intrinsicContentSize: CGSize {
        return .zero
    }
    
}

Output:

enter image description here

enter image description here

enter image description here

Upvotes: 0

Related Questions