Search code examples
iosswiftnslayoutconstraint

update nslayoutconstraints to accommodate new view size?


I have a UIView that increases in size once the text in a UITextView reaches a certain point. Both views are resizing, however I can't get the view to move upwards to accommodate the bottomconstraint I set on the UIView. Here's a screenshot of the issue.

before the view is resized

after the view is resized

I have tried using both view.setNeedsLayout() and view.layoutIfNeeded() but I'm assuming I might be implementing them wrong...

Here's the code for the project

import UIKit

class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UITextViewDelegate {


var bottomConstraint: NSLayoutConstraint?
var phconstraint: NSLayoutConstraint?

let searchBarContainer: UIView = {
    let sBarContainer = UIView()
    sBarContainer.backgroundColor = UIColor.gray
    sBarContainer.layer.cornerRadius = 3
    return sBarContainer
}()

let searchBar: UITextView = {
    let sBar = UITextView()
    sBar.textAlignment = .left
    sBar.font = .systemFont(ofSize: 12)
    sBar.backgroundColor = UIColor.red
    sBar.sizeToFit()

    return sBar
}()

let placeholder: UILabel = {
    let pholder = UILabel()

    pholder.font = UIFont.systemFont(ofSize: 16, weight: 0.20)
    pholder.frame.size = pholder.intrinsicContentSize
    pholder.backgroundColor = UIColor.clear
    pholder.textColor = UIColor.gray

    pholder.text = "Share!"

    pholder.textAlignment = .left


    return pholder
}()

let dividerLine: UIView = {
    let line = UIView()
    line.backgroundColor = UIColor.init(red: 240/255, green: 240/255, blue: 240/255, alpha: 1)
    return line
}()

let bubbleview : UIView = {
   let bView = UIView()
    bView.backgroundColor = UIColor.clear
    return bView

}()

func setupKeyboardObservers() {

    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillShow, object: nil)

    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardNotification), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}

func handleKeyboardNotification(_ notification: Notification) {

    if let userInfo = notification.userInfo {
        let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue
        print(keyboardFrame as Any)

        let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow
        let isKeybobardNotShowing = notification.name == NSNotification.Name.UIKeyboardWillHide

        bottomConstraint?.constant = isKeyboardShowing ? -keyboardFrame!.height : 0

        UIView.animate(withDuration: 0, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {

            self.view.layoutIfNeeded()
        }, completion: { (completed) in


        })
    }
}


override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.title = "Search Bar"
    collectionView?.backgroundColor = UIColor.white

    setupKeyboardObservers()
    self.searchBar.delegate = self    
    view.addSubview(searchBarContainer)
    searchBarContainer.addSubview(searchBar)
    searchBar.translatesAutoresizingMaskIntoConstraints = false
    searchBarContainer.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint(item: searchBarContainer, attribute: .width, relatedBy: .equal, toItem: view, attribute: .width, multiplier: 0.75, constant: 0).isActive = true

    NSLayoutConstraint(item: searchBarContainer, attribute: .height, relatedBy: .equal, toItem: view, attribute: .height, multiplier: 0.05, constant: 0).isActive = true

    NSLayoutConstraint(item: searchBarContainer, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1, constant: 0).isActive = true

     bottomConstraint = NSLayoutConstraint(item: searchBarContainer, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0)

            view.addConstraint(bottomConstraint!)

}


func textViewDidChange(_ textView: UITextView) {
            let currentString: String = textView.text!
            let length: Int = (currentString.characters.count )

        let messageText = currentString
        let size = CGSize(width: searchBarContainer.frame.width , height: searchBarContainer.frame.height * 2)
        let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
        let estimatedFrame = NSString(string: messageText).boundingRect(with: size, options: options, attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: 12)], context: nil)
    print("this is the ewidth \(estimatedFrame.width)")
    searchBarContainer.addSubview(bubbleview)
    bubbleview.frame = estimatedFrame

    searchBar.sizeToFit()
    searchBar.frame.size.width = searchBarContainer.frame.width


            if estimatedFrame.height >= (searchBarContainer.frame.height) {

            searchBarContainer.frame.size.height = estimatedFrame.height


                self.view.addConstraint(self.bottomConstraint!)

                UIView.animate(withDuration: 0, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {

                    self.view.layoutIfNeeded()
                }, completion: { (completed) in



                })


        }
    view.setNeedsLayout()
    view.layoutIfNeeded()

}

}


extension UIView {
func addConstraintsWithFormat(_ format: String, views : UIView...) {


    var viewsDictionary = [String: UIView]()

    for(index, view) in views.enumerated(){
        let key = "v\(index)"
        viewsDictionary[key] = view
        view.translatesAutoresizingMaskIntoConstraints = false

    }
    addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))

}
}

Solution

  • Couple notes...

    When trying to use an "auto-sizing" UITextView you will find it works best if you disable scrolling... so add this to your searchBar setup:

        sBar.isScrollEnabled = false
    

    You are mixing auto-layout / constraints with explicitly setting frame sizes. That frequently gets you in trouble. Use one or the other (with constraints being the preferred method). In your code, you set

        searchBar.translatesAutoresizingMaskIntoConstraints = false
    

    But you never give it any constraints.

    Edit: After playing around a bit, you can greatly simplify things just by relying on auto-layout.

    Instead of using a "container" view to hold the searchBar text view, create a "padding" view that will sit underneath searchBar. Then:

    1. searchBar should sit "on top of" paddingView
    2. paddingView should slide up with the keyboard
    3. searchBar will automatically slide up, because it's pinned to the top of paddingView
    4. searchBar's height will automatically change based on text content

    So -- paddingView will control the position, width and Y-position.

    You already have a property to control the Y-position

    var bottomConstraint: NSLayoutConstraint?
    

    Change searchBar definition and add paddingView definition:

    let searchBar: UITextView = {
        let sBar = UITextView()
        sBar.textAlignment = .left
        sBar.font = .systemFont(ofSize: 12)
        sBar.backgroundColor = UIColor.red
        sBar.isScrollEnabled = false
        sBar.translatesAutoresizingMaskIntoConstraints = false
        return sBar
    }()
    
    let paddingView: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = .gray
        v.layer.cornerRadius = 3.0
        return v
    }()
    

    viewDidLoad ends up like this:

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Search Bar"
        collectionView?.backgroundColor = UIColor.white
        setupKeyboardObservers()
        self.searchBar.delegate = self
    
        // add bottom "padding" view
        view.addSubview(paddingView)
    
        // add searchBar text view
        view.addSubview(searchBar)
    
        // set width of padding view to 75% of view width
        paddingView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.75).isActive = true
        // set center X of padding view to center X of view
        paddingView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 1.0).isActive = true
        // set height of padding view to 6.0 (we're rounding the corners with a radius of 3.0)
        paddingView.heightAnchor.constraint(equalToConstant: 6.0).isActive = true
    
        // assign a constraint to bottomConstraint property so we can change the Y-position of padding view (and searchBar) when desired
        // initially set the constant to 0 (so it sits on the bottom of the view)
        bottomConstraint = paddingView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0)
        bottomConstraint?.isActive = true
    
        // set searchBar width to padView width
        searchBar.widthAnchor.constraint(equalTo: paddingView.widthAnchor, multiplier: 1.0).isActive = true
        // set searchBar center X to padView center X
        searchBar.centerXAnchor.constraint(equalTo: paddingView.centerXAnchor, constant: 0.0).isActive = true
        // set searchBar bottom to padView Top + 3.0 (we want to cover the top 3 pts)
        searchBar.bottomAnchor.constraint(equalTo: paddingView.topAnchor, constant: 3.0).isActive = true
    }
    

    Since UITextView auto-adjusts its own size when using auto-layout, you don't even need the textViewDidChange function:

    func textViewDidChange(_ textView: UITextView) {
        // nothing to do here...
    }
    

    enter image description here

    enter image description here

    Note: All I could see from your bubbleview was that it covered a portion of the searchBar text view... so I ignored it.