8

I have been trying to get a UITextView to resize all week. I don't see how this should be done, so I have decided to include practically all relevant code.

I have this conversation view:

Conversation View

This conversationview is a UIViewController with a UITableView inside (using constraints). I have a custom UIView subclass ConversationToolbar set as inputAccessoryView (the UIViewController containing it can become the first responder, so the view is visible at all times), that contains 2 subviews. One for the UITextView and left and right buttons, and one for the emoticons. The emoticons only show when the left button is tapped:

Emotion view

And when one is chosen, it shows in a floating label:

Floating emotion

I now have trouble to resize this UITextView when multiple lines are used. I have tried calculating all frames myself, but then that seems to conflict with my constraints in some weird way. Almost always is the UITextView either too small or it resizes only after I press another key to update the view. Or the UITextView gets bigger over the keyboard, or it slides out of view when the keyboard is dismissed.

I have removed all resizing abilities from my code and I would like to know what I need to do to make this resize.

In my ConversationViewController:

var toolbar: ConversationToolbar!

override var inputAccessoryView: UIView! {
    get {
        if toolbar == nil {
            toolbar = NSBundle.mainBundle().loadNibNamed("ConversationToolbar", owner: nil, options: nil).last! as ConversationToolbar
            toolbar.frame.size = CGSize(width: UIScreen.mainScreen().bounds.size.width, height: 80)
            toolbar.delegate = self
            toolbar.setDraft(conversation.draft)
        }
        return toolbar
    }
}

With ConversationToolbar.xib being:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6254" systemVersion="14D87p" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6247"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ConversationToolbar" customModule="Heaven_Help" customModuleProvider="target">
            <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
            <subviews>
                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="s5O-PN-dtz">
                    <rect key="frame" x="0.0" y="554" width="600" height="46"/>
                    <subviews>
                        <button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="pbw-hg-sNn">
                            <rect key="frame" x="0.0" y="0.0" width="36" height="46"/>
                            <inset key="contentEdgeInsets" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
                            <state key="normal" title="">
                                <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
                            </state>
                            <connections>
                                <action selector="emoPress:" destination="iN0-l3-epB" eventType="touchUpInside" id="hLw-rH-Hym"/>
                            </connections>
                        </button>
                        <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Vid-Ac-jz3">
                            <rect key="frame" x="548" y="0.0" width="52" height="46"/>
                            <inset key="contentEdgeInsets" minX="8" minY="0.0" maxX="8" maxY="0.0"/>
                            <state key="normal" title="Send">
                                <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
                            </state>
                            <connections>
                                <action selector="sendPress:" destination="iN0-l3-epB" eventType="touchUpInside" id="rzU-Vk-nJa"/>
                            </connections>
                        </button>
                        <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Czv-f6-jOP">
                            <rect key="frame" x="36" y="8" width="512" height="30"/>
                            <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                            <fontDescription key="fontDescription" type="system" pointSize="14"/>
                            <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                        </textView>
                    </subviews>
                    <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                    <constraints>
                        <constraint firstAttribute="height" constant="46" id="D1D-QM-5n2"/>
                        <constraint firstAttribute="trailing" secondItem="Vid-Ac-jz3" secondAttribute="trailing" id="H9Y-xT-aqe"/>
                        <constraint firstAttribute="centerY" secondItem="pbw-hg-sNn" secondAttribute="centerY" id="IYf-5R-S97"/>
                        <constraint firstItem="Czv-f6-jOP" firstAttribute="leading" secondItem="pbw-hg-sNn" secondAttribute="trailing" id="LpH-ir-M9U"/>
                        <constraint firstAttribute="centerY" secondItem="Vid-Ac-jz3" secondAttribute="centerY" id="NQ1-m1-9rk"/>
                        <constraint firstAttribute="bottom" secondItem="Vid-Ac-jz3" secondAttribute="bottom" id="UUt-1r-emN"/>
                        <constraint firstItem="Czv-f6-jOP" firstAttribute="top" secondItem="s5O-PN-dtz" secondAttribute="top" constant="8" id="VWB-7N-LrK"/>
                        <constraint firstAttribute="bottom" secondItem="Czv-f6-jOP" secondAttribute="bottom" constant="8" id="WTi-8a-kaM"/>
                        <constraint firstItem="pbw-hg-sNn" firstAttribute="leading" secondItem="s5O-PN-dtz" secondAttribute="leading" id="a4Q-W4-ROh"/>
                        <constraint firstAttribute="bottom" secondItem="pbw-hg-sNn" secondAttribute="bottom" id="hGG-Xe-FZZ"/>
                        <constraint firstItem="Vid-Ac-jz3" firstAttribute="leading" secondItem="Czv-f6-jOP" secondAttribute="trailing" id="jc6-18-MYq"/>
                    </constraints>
                </view>
                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rh6-cD-U1e">
                    <rect key="frame" x="0.0" y="508" width="600" height="46"/>
                    <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
                </view>
                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6zz-GF-eHs">
                    <rect key="frame" x="279" y="525" width="42" height="21"/>
                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
                    <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
                    <nil key="highlightedColor"/>
                </label>
            </subviews>
            <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
            <constraints>
                <constraint firstAttribute="centerX" secondItem="6zz-GF-eHs" secondAttribute="centerX" id="3at-fx-ZCR"/>
                <constraint firstAttribute="bottom" secondItem="s5O-PN-dtz" secondAttribute="bottom" id="3r5-GG-6Eo"/>
                <constraint firstItem="s5O-PN-dtz" firstAttribute="height" secondItem="rh6-cD-U1e" secondAttribute="height" id="MBP-k2-Qre"/>
                <constraint firstItem="s5O-PN-dtz" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="PLK-DE-38r"/>
                <constraint firstItem="s5O-PN-dtz" firstAttribute="top" secondItem="6zz-GF-eHs" secondAttribute="bottom" constant="8" symbolic="YES" id="PRr-qQ-oyf"/>
                <constraint firstAttribute="trailing" secondItem="s5O-PN-dtz" secondAttribute="trailing" id="PqA-VB-NxV"/>
                <constraint firstItem="s5O-PN-dtz" firstAttribute="top" secondItem="rh6-cD-U1e" secondAttribute="bottom" id="ZxJ-8p-Mjq"/>
                <constraint firstItem="rh6-cD-U1e" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="lsS-5I-Saq"/>
                <constraint firstAttribute="trailing" secondItem="rh6-cD-U1e" secondAttribute="trailing" id="yRP-hS-dpr"/>
            </constraints>
            <connections>
                <outlet property="emoButton" destination="pbw-hg-sNn" id="VJx-J0-hBJ"/>
                <outlet property="emoLabel" destination="6zz-GF-eHs" id="pUW-yD-pIq"/>
                <outlet property="emoView" destination="rh6-cD-U1e" id="0YV-Wx-clp"/>
                <outlet property="sendButton" destination="Vid-Ac-jz3" id="6FV-Q2-ufA"/>
                <outlet property="textSuperView" destination="s5O-PN-dtz" id="BIT-bH-p4M"/>
                <outlet property="textView" destination="Czv-f6-jOP" id="syf-v4-LLy"/>
            </connections>
        </view>
    </objects>
</document>

And finally my ConversationToolbar.swift:

import Foundation

class ConversationToolbar: UIView, UITextViewDelegate {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var emoView: UIView!
    @IBOutlet weak var sendButton: UIButton!
    @IBOutlet weak var textSuperView: UIView!
    @IBOutlet weak var emoButton: UIButton!
    @IBOutlet weak var emoLabel: UILabel!

    var delegate: ConversationToolbarDelegate!
    var emobuttons: [String: UIButton]!
    var emoSelected: String?
    var clickableSend: Bool {
        get {
            return ((delegate?.hasEnoughCredits() ?? false) && !(textView?.text?.isEmpty ?? true)) ?? false
        }
    }

    override func awakeFromNib() {
        textView.delegate = self
        sendButton.enabled = clickableSend
        self.autoresizingMask = .FlexibleHeight

        // Make textView pretty
        textView.backgroundColor = UIColor.whiteColor() // (white: 250/255, alpha: 1)
        textView.font = UIFont.systemFontOfSize(17)
        textView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 205/255, alpha:1).CGColor
        textView.layer.borderWidth = 0.5
        textView.layer.cornerRadius = 5
        textView.scrollsToTop = false
        textView.textContainerInset = UIEdgeInsetsMake(4, 3, 3, 3)
        textView.autoresizingMask = .FlexibleHeight

        // Add a nice border to the view
        textSuperView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 205/255, alpha:1).CGColor
        textSuperView.layer.borderWidth = 0.5
        textSuperView.backgroundColor = UIColor(white: 235/255, alpha: 1)
        textSuperView.autoresizingMask = .FlexibleHeight

        // Prettify emoView
        emoView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 205/255, alpha:1).CGColor
        emoView.layer.borderWidth = 0.5
        emoView.backgroundColor = UIColor(white: 235/255, alpha: 1)

        // Emobar setup
        emobuttons = StaticData.getEmoButtons()

        let unusedSpace = UIScreen.mainScreen().bounds.width - emobuttons.sum {
            button in
            return button.intrinsicContentSize().width
        }
        let spaceBetweenButtons = Int(unusedSpace) / (emobuttons.count + 1)

        var visualLayout = "H:|"
        for button in emobuttons {
            button.1.setTranslatesAutoresizingMaskIntoConstraints(false)
            visualLayout += "-(space)-[\(button.0)]"
            emoView.addSubview(button.1)
            button.1.addTarget(self, action: "emoChosen:", forControlEvents: .TouchUpInside)
        }

        emoView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("\(visualLayout)-(space)-|", options: .AlignAllCenterY, metrics: ["space":spaceBetweenButtons], views: emobuttons))
        emoView.addConstraint(NSLayoutConstraint(item: emoView, attribute: .CenterY, relatedBy: .Equal, toItem: emobuttons.values.array.first!, attribute: .CenterY, multiplier: 1, constant: 0))

        // Prettify toolbar itself
        self.backgroundColor = UIColor.clearColor()

        // start hidden
        emoView.hidden = true
        emoLabel.hidden = emoSelected == nil
    }

    @IBAction func emoPress(sender: UIButton) {
        emoView.hidden = !emoView.hidden
        updateEmoLabel()
        if !emoView.hidden {
            emoLabel.hidden = true
        }
    }

    @IBAction func sendPress(sender: UIButton) {
        delegate.sendMessage(textView.text, feeling: emoSelected)
        textView.text = ""
        emoSelected = nil
        sendButton.enabled = false
        textViewDidChange(textView)
        updateEmoLabel()
    }

    func emoChosen(sender: UIButton) {
        emoView.hidden = true
        emoSelected = sender.titleLabel?.text == "" ? nil : sender.titleLabel?.text
        updateEmoLabel()
    }

    func setDraft(draft: String) {
        textView.text = draft
    }

    func updateEmoLabel() {
        emoLabel.text = emoSelected
        emoLabel.hidden = emoSelected == nil
    }

    /// MARK: UITextFieldDelegate

    func textViewDidChange(textView: UITextView) {
        sendButton.enabled = clickableSend
    }
}

protocol ConversationToolbarDelegate {
    func sendMessage(text: String, feeling: String?)
    func hasEnoughCredits() -> Bool
}
vrwim
  • 13,020
  • 13
  • 63
  • 118

2 Answers2

8

I have solved this problem with AutoLayout. I have added a new constraint defining the height of my UITextField and I am setting the constant of that constraint in textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool:

func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
    let oldHeight = textView.frame.height
    let newText = (textView.text as NSString).stringByReplacingCharactersInRange(range, withString: text)
    let newSize = (newText as NSString).boundingRectWithSize(CGSize(width: textView.frame.width - textView.textContainerInset.right - textView.textContainerInset.left - 10, height: CGFloat.max), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: textView.font], context: nil)
    let heightChange = newSize.height + textView.textContainerInset.top + textView.textContainerInset.bottom + 2.719 - oldHeight

    textFieldHeightLayoutConstraint.constant += heightChange
    return true
}

The problem I had was that I was setting AutoLayout and UIView.frame to different settings, which lead to them fighting.

I am an idiot.

vrwim
  • 13,020
  • 13
  • 63
  • 118
  • Could you please add the code for your textFieldHeightLayoutConstraint? – Stephan Mar 24 '15 at 12:43
  • 1
    @Stephan It was a storyboard layout, but here it is in code: NSLayoutConstraint(item: textField, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30) – vrwim Mar 24 '15 at 13:04
  • I like this solution, I tried to do this long time ago, but it **breaks constraints**. I find your solution while I'm still trying to improve the `textview` still. Can you tell me if yours is broken or not? – Brian Nezhad Oct 08 '15 at 03:38
  • I have a question: what are the magic numbers 10 and 2.719 stand for? – lancy Apr 29 '16 at 09:11
  • @lancy damn what you're asking me, I guess it was just me trying out different things to make it work. I don't use magic numbers anymore, just looking at this makes me cringe – vrwim Apr 29 '16 at 09:17
1

I solved this issue with resizing inputAccessoryView long time ago, but did not fix the issues with proper UITextView resize for iOS8 until today. Here is my sample app - github

It uses frames for iOS7 resizing (UITextEffectsWindow does layout manually and creating constraints in inputAccessoryView's heirarchy means creating autolayout engine, which causes autolayout engine failure after strange transformations with inf/nan frames) and constraints for iOS8 resizing. Anyway, layout of views inside inputAccessoryView is never done correctly, so we have to perform frame calculations.

Example1 Example2

Nikita Ivaniushchenko
  • 1,425
  • 11
  • 11
  • Importing this framework and using it as-is as my `inputAccessoryView` crashes my application without any useful stacktrace. These extra fields in the tableView are only distracting and adding extra bloat. I only need the resizing of the `inputAccessoryView` with the `UITextView`. Can you tell me how I can do that? – vrwim Mar 17 '15 at 13:55
  • That's not that easy, this sample is as short as possible to make it working on iOS7 and iOS8. Could you can provide me your code so i could check what is going wrong? – Nikita Ivaniushchenko Mar 17 '15 at 14:17
  • I have found the problem, I have solved it using AutoLayout and a bit of code, it is much easier than your solution... – vrwim Mar 17 '15 at 16:22
  • Did you try interface rotation, copy/paste text and having limit for number of lines? There are a lot of issues. BTW, pay attention of your view controller's lifecycle. Sometimes it's not destroyed when using `inputAccessoryView` - there are some fixes in my code. – Nikita Ivaniushchenko Mar 17 '15 at 16:25
  • Interface rotation is not needed in my case, but it works. Copy and paste work as expected. I'll check out the max lines, but I'm guessing that is also an easy fix. Swift offers automatic reference counting, so not needed to clean up. – vrwim Mar 18 '15 at 00:39
  • I can confirm that the github sample code works, even on iOS8.4, which most other solutions don't. Seems the important points are the auto layout line that initialises a heightConstraint and the following line which calls setHeight. The only parts of setHeight I took were: -(void)setHeight:(CGFloat)height { self.resizableTextView.height = height; [self.superview.superview addConstraint:self.heightConstraint]; self.heightConstraint.constant = height; [self.superview layoutIfNeeded]; [self layoutIfNeeded]; [self.window layoutIfNeeded]; } That's it, works. Great work @Nikita. – Brett Jul 13 '15 at 07:21