2

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 Answers1

2

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

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Wow! Thanks for the very detailed answer. I really appreciate the sample code and wireframes. I had typed out a detailed response that ended up being too long to leave as a comment here, but long story short, I tried implementing these changes and still the cell maintains a fixed height. The only thing that changed was that scrolling was disabled. I know it may be a lot to ask, but would you be willing to look at my repo? I left the detailed response for you about how I tried to make the changes as a comment in the newNoteTableViewCell file. [link](https://github.com/michaelhandkins/topline) – michaelthedeveloper Oct 01 '20 at 02:55
  • @michaelthedeveloper - see the **Edit** at the bottom of my answer. If you want, you can add me as a "Collaborator" to your GitHub repo... I'll update your code also and then commit changes on a new branch so you can clearly see the differences. (My GitHub id is also `DonMag`) – DonMag Oct 01 '20 at 14:17
  • I fixed the restraints and reimplemented the initial solution and it's working perfectly now! There is something else I'm needing help with if you're interested. I set up the recording functionality for the record button so that currently in the simulator, pressing the record button allows you to record some audio and then play it back once done, however, I've experimented with a couple different ways of saving these recordings to realm so that they also repopulate their cell with their corresponding lyrics, but can't seem to figure out how to do it. Adding you as Collaborator now – michaelthedeveloper Oct 01 '20 at 22:34
  • @michaelthedeveloper - how to manage saving / reloading user entered data and audio is an entirely different question. I suggest you start with save / load text strings from a ***simple*** app with one text view and one button. Then work on save / load audio... also from a simple app. Once you have a solid handle on those tasks, *then* work on adding it to your full app. – DonMag Oct 02 '20 at 12:18
  • I am already able to save strings to Realm and then repopulate cells with the strings, but the issue only arises when attempting to access the URL’s associated with the saved strings. I know it’s another question but since it’s the same project just thought I’d see if you were able to help. Thanks for helping resolve the table cell dynamic height issue though! I’ll continue searching for an answer on the audio storage and will try to first implement it on a simple app. – michaelthedeveloper Oct 02 '20 at 19:27