How can one make a UITableViewCell's height expand to fit the contents of a UITextView with wrapping text without the textView scrolling? (Swift 5)

Viewed 1204

I have a UITableViewController that utilizes custom cells. Within the custom cell is a UITextView and a UIButton. I need for the selected tableView cell to fit the multiple lines of text that may be input by the user, as the user is typing. An app that demonstrates this perfectly would be iPhone's pre-installed Reminders app. Currently in my app, the tableView cell's height remains static and the textView enables scrolling when necessary in order to accommodate multiple lines of text.

I tried implementing the solutions in this post (Dynamically change cell's height while typing text, and reload the containing tableview for resize) which seemed to ask the same question, however, the post is nearly 3 years old and even if the answer is still valid, there's not enough sample code present for me to understand how to implement the answer. I did my best with no luck and cleared my current code of any previous attempts so that it's presently a clean slate as far as this desired feature is concerned. It would be amazing to see an answer that includes sample code in Swift 5.

Here is the custom cell class if it would be useful:

class newNoteTableViewCell: UITableViewCell {

    @IBOutlet weak var lyricsField: UITextView!
    @IBOutlet weak var recordButton: UIButton!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
    
    @IBAction func recordButtonPressed(_ sender: UIButton) {
    }
}

And my cellForRowAt, which is currently the only table view delegate method implemented other than numberOfRowsInSection:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "lyricsCell", for: indexPath) as! newNoteTableViewCell
        
        cell.lyricsField.delegate = self
        
        cell.lyricsField.tag = indexPath.row
        
        if let safeLyrics = lyrics {
            if indexPath.row < safeLyrics.count {
                cell.lyricsField.text = safeLyrics[indexPath.row].text
            } else {
                cell.lyricsField.text = ""
            }
        }
        
        return cell
    }
1 Answers

We can do this by adding a "callback" closure to your cell.

When the text view is edited, the cell class will "call back" to the controller, where we can tell the table view to recalculate row heights (as well as saving the edited text).

Here's a simple cell layout with constraints. Make sure to disable scrolling on the text view:

enter image description here

Note that the height of the cell at design-time doesn't matter... our constraints will allow auto-layout to handle that.

Result:

enter image description here

Here's the example code:

class ExampleCell: UITableViewCell, UITextViewDelegate {
    
    @IBOutlet var recordButton: UIButton!
    @IBOutlet var lyricsField: UITextView!
    
    var callback: ((String) -> ())?
    
    override func didMoveToSuperview() {
        super.didMoveToSuperview()
        // make sure scroll is disabled
        lyricsField.isScrollEnabled = false
        // make sure delegate is set
        lyricsField.delegate = self
        // if these are set in Storyboard this func is not needed
    }
    func textViewDidChange(_ textView: UITextView) {
        let str = textView.text ?? ""
        // tell the controller
        callback?(str)
    }

}

class ExampleTableViewController: UITableViewController {

    var myData: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // start with 20 sample strings for our data
        // fill data array with 30 strings
        myData = (1...30).map { "This is row \($0)" }

        // give the second row some longer sample text
        myData[1] = "Some sample text so we see that the text view height will be automatically handled by auto-layout."
        
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }

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

        cell.lyricsField.text = myData[indexPath.row]
        
        // set the closure
        weak var tv = tableView
        cell.callback = { [weak self] str in
            guard let self = self, let tv = tv else { return }
            print("called back", str)
            // update our data with the edited string
            self.myData[indexPath.row] = str
            // we don't need to do anything else here
            // this will force the table to recalculate row heights
            tv.performBatchUpdates(nil)
        }

        return cell
    }

}

and here's the Storyboard source for reference:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="UxY-Y6-LYS">
    <device id="retina3_5" orientation="portrait" appearance="light"/>
    <dependencies>
        <deployment identifier="iOS"/>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
        <capability name="System colors in document resources" minToolsVersion="11.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <scenes>
        <!--Example Table View Controller-->
        <scene sceneID="HKA-46-9zH">
            <objects>
                <tableViewController id="UxY-Y6-LYS" customClass="ExampleTableViewController" customModule="Temp" customModuleProvider="target" sceneMemberID="viewController">
                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="ZaD-4v-hEm">
                        <rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                        <prototypes>
                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="exampleCell" rowHeight="141" id="1bW-gv-rnI" customClass="ExampleCell" customModule="Temp" customModuleProvider="target">
                                <rect key="frame" x="0.0" y="28" width="320" height="141"/>
                                <autoresizingMask key="autoresizingMask"/>
                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="1bW-gv-rnI" id="H65-Gy-hPe">
                                    <rect key="frame" x="0.0" y="0.0" width="320" height="141"/>
                                    <autoresizingMask key="autoresizingMask"/>
                                    <subviews>
                                        <button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5rZ-Jh-Wyd">
                                            <rect key="frame" x="96" y="11" width="128" height="30"/>
                                            <color key="backgroundColor" red="0.85215073819999998" green="0.88016217949999997" blue="0.94548028709999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                            <state key="normal" title="Record"/>
                                        </button>
                                        <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" keyboardDismissMode="onDrag" text="The Text View" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="ddd-0L-1pM">
                                            <rect key="frame" x="24" y="49" width="272" height="81"/>
                                            <color key="backgroundColor" red="0.99953407049999998" green="0.98835557699999999" blue="0.47265523669999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                            <color key="textColor" systemColor="labelColor"/>
                                            <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                            <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                                        </textView>
                                    </subviews>
                                    <constraints>
                                        <constraint firstItem="ddd-0L-1pM" firstAttribute="top" secondItem="5rZ-Jh-Wyd" secondAttribute="bottom" constant="8" id="MHf-Yq-xQq"/>
                                        <constraint firstAttribute="trailingMargin" secondItem="ddd-0L-1pM" secondAttribute="trailing" constant="8" id="QMH-AZ-j7k"/>
                                        <constraint firstItem="5rZ-Jh-Wyd" firstAttribute="leading" secondItem="H65-Gy-hPe" secondAttribute="leadingMargin" constant="80" id="TYR-1f-iit"/>
                                        <constraint firstAttribute="trailingMargin" secondItem="5rZ-Jh-Wyd" secondAttribute="trailing" constant="80" id="Yf1-2g-dlf"/>
                                        <constraint firstItem="5rZ-Jh-Wyd" firstAttribute="top" secondItem="H65-Gy-hPe" secondAttribute="topMargin" id="gaW-td-egM"/>
                                        <constraint firstItem="ddd-0L-1pM" firstAttribute="bottom" secondItem="H65-Gy-hPe" secondAttribute="bottomMargin" id="lKj-ML-H4Q"/>
                                        <constraint firstItem="ddd-0L-1pM" firstAttribute="leading" secondItem="H65-Gy-hPe" secondAttribute="leadingMargin" constant="8" id="uxs-rD-Pdj"/>
                                    </constraints>
                                </tableViewCellContentView>
                                <connections>
                                    <outlet property="lyricsField" destination="ddd-0L-1pM" id="9Nz-Ru-psp"/>
                                    <outlet property="recordButton" destination="5rZ-Jh-Wyd" id="qNA-Up-zLK"/>
                                </connections>
                            </tableViewCell>
                        </prototypes>
                        <connections>
                            <outlet property="dataSource" destination="UxY-Y6-LYS" id="cmh-tD-hLg"/>
                            <outlet property="delegate" destination="UxY-Y6-LYS" id="xk4-oC-WNJ"/>
                        </connections>
                    </tableView>
                </tableViewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="7mc-zX-bKw" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="330" y="127.5"/>
        </scene>
    </scenes>
    <resources>
        <systemColor name="labelColor">
            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
        <systemColor name="systemBackgroundColor">
            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
    </resources>
</document>

Edit in response to comment....

Your constraints in your cell are all wrong...

Here's how they look from your original xib:

enter image description here

Here's how they should look:

enter image description here

Important: your stack view settings need to be:

enter image description here

Related