4

My view has 2 subviews. A UITextView and a UIView. The views are dynamically (programmatically) created.

Using AutoLayout. I would like

  • UITextView to grow to the height of the text
  • UIView to fill remaining of the space of the parent

(see attachment)

How could I do this programmatically in swift ?

enter image description here

Prakash Raman
  • 13,319
  • 27
  • 82
  • 132

3 Answers3

1

1) Set constraint on UIView to keep zero distance between UIView's top and UITextview's bottom.

2) Add a height constraint on UITextView. Make an IBOutlet for the UITextview's height constraint.

3) Now change the constant property of UITextview's height constraint in your code

san
  • 3,350
  • 1
  • 28
  • 40
1

Alright, It's a bit large, and perhaps It can be improved a lot. But this will do the mojo. :)

AdjustableView.swift

import UIKit
import Foundation

class AdjustableView: UIView, UITextViewDelegate {
 var viewTop : UITextView!
 var viewBottom : UIView!
 weak var heightConstraint : NSLayoutConstraint!
 var countFinalLines : Int?

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

private func buildBothViews(frame: CGRect) {
    viewTop = UITextView()
    viewBottom = UIView()

    //This is just for testing and checking that it does exactly what I want
    viewTop.backgroundColor = UIColor.orangeColor()
    viewBottom.backgroundColor = UIColor.greenColor()

    setConstraints()
    initTextView()
}

/**
Sets the settings for the UITextView
*/
func initTextView(){
    let insets = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10)
    viewTop.contentInset = insets
    viewTop.delegate = self
    viewTop.bounces = false
    viewTop.scrollEnabled = false
    viewTop.textAlignment = NSTextAlignment.Center
}

/**
This will arrange the views
*/
func setConstraints(){
     //I'm using this proportion as my initial height for the UITextView, you can set it for any other.
    //My UITextView will not be allowed to go less tall than this height
    let initialProportion = frame.height/8

    let leftConstraint1 = NSLayoutConstraint(
        item: viewBottom, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Left, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Left, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let leftConstraint2 = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Left, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Left, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let rightConstraint1 = NSLayoutConstraint(
        item: viewBottom, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Right, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Right, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let rightConstraint2 = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Right, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Right, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )


    let topViewHeightConstraint = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Height, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.GreaterThanOrEqual, //-- how we want to relate THIS object to A DIFF object
        toItem: nil, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.NotAnAttribute, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: initialProportion
    )
    let topConstraint = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Top, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Top, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let distanceConstraint = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Bottom, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: viewBottom, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Top, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let bottomConstraint = NSLayoutConstraint(
        item: viewBottom, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Bottom, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: self, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.Bottom, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: 0
    )
    let bottomViewHeightConstraint = NSLayoutConstraint(
        item: viewBottom, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Height, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: nil, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.NotAnAttribute, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: frame.height - initialProportion
    )
    //This is the one I will need to modify
    heightConstraint = NSLayoutConstraint(
        item: viewTop, //-- the object that we want to constrain
        attribute: NSLayoutAttribute.Height, //-- the attribute of the object we want to constrain
        relatedBy: NSLayoutRelation.Equal, //-- how we want to relate THIS object to A DIFF object
        toItem: nil, //-- this is the different object we want to constrain to
        attribute: NSLayoutAttribute.NotAnAttribute, //-- the attribute of the different object
        multiplier: 1, //-- multiplier
        constant: initialProportion
    )

    bottomViewHeightConstraint.priority = 750
    topViewHeightConstraint.priority = 900

    var constraints = [NSLayoutConstraint]()
    //recopilate constraints created here
    constraints.append(heightConstraint)
    constraints.append(distanceConstraint)
    constraints.append(bottomConstraint)
    constraints.append(topConstraint)

    constraints.append(leftConstraint1)
    constraints.append(leftConstraint2)
    constraints.append(rightConstraint1)
    constraints.append(rightConstraint2)

    constraints.append(bottomViewHeightConstraint)
    constraints.append(topViewHeightConstraint)

    viewTop.setTranslatesAutoresizingMaskIntoConstraints(false)
    viewBottom.setTranslatesAutoresizingMaskIntoConstraints(false)
    //add them to the desired control

    self.addSubview(viewTop)
    self.addSubview(viewBottom)

    //Activates constrants
    NSLayoutConstraint.activateConstraints(constraints)
}
/**
We will listen for changes in the textView

:param: textView the top view
*/
func textViewDidChange(textView: UITextView){
    adjustHeightAccordingToText(textView)
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    if let theFrame = (aDecoder.decodeObjectForKey("frame") as? NSValue) {
        buildBothViews(theFrame.CGRectValue())
    }

}

//If my number of lines is changing for the modification occurring, then the height of this view should change
func adjustHeightAccordingToText(textView: UITextView){
    if countFinalLines == nil {
        countFinalLines = countLabelLines(textView)
    }else {
        let tentativeCountLines = countLabelLines(textView)
        if countFinalLines != tentativeCountLines {
            countFinalLines = tentativeCountLines
            let singleLineHeight = CGFloat(textView.font.lineHeight)
            heightConstraint?.constant = floor((CGFloat(countFinalLines! + 3) * singleLineHeight))
        }
    }
}
/**
Measure count of lines that will have a label after applying a font to it

:param: label the label to measure
:param: font  the font with the one to measure this label

:returns: number of lines
*/
private func countLabelLines(label:UITextView)->Int{
    if let text = label.text{
        // cast text to NSString so we can use sizeWithAttributes
        let myText = text as NSString

        //Set attributes
        let attributes = [NSFontAttributeName : label.font]

        //Calculate the size of your UILabel by using the systemfont and the paragraph we created before. Edit the font and replace it with yours if you use another
        let labelSize = myText.boundingRectWithSize(CGSizeMake(label.bounds.width, CGFloat.max), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: attributes, context: nil)

        //Now we return the amount of lines using the ceil method
        let lines = ceil(CGFloat(labelSize.height) / label.font.lineHeight)
        return Int(lines)
    }
    return 0
}
}

then at your UIViewController

override func viewDidLoad() {
    super.viewDidLoad()
    //Create an instance of AdjustableView and add it to view hierarchy 
    let adjustableView = AdjustableView(frame: view.frame)
    view.addSubview(adjustableView)
}

The result should be like this:

Initial State

Initial State

After some Text Insertion

After some Text Insertion

Hugo Alonso
  • 6,684
  • 2
  • 34
  • 65
1

For swift 4:

To have a view fill in the remainding screen space:

yourView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
yourView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

will attach yourView to the top and bottom anchor of it's parent view, allowing it to grow/shirnk in size upon height change

G. Böhm
  • 41
  • 1
  • 4