0

Issue

I am experiencing a weird behaviour with a UITextView which is inside a UIStackView with a defined spacing of 0. The UITextView is used to display a message inside a chat bubble. When the message is only one line, the UITextView expands and adds a bottom spacing/padding of about 2.3 px. Looking at the view hierarchy, the text itself is a _UITextLayoutFragmentView which is inside a _UITextLayoutCanvasView. It appears that this canvas view is drawn too big, causing the spacing.

The following images are from the view hierarchy with a one line message where the issue appears and from a message with two lines, where the issue is not appearing: https://i.stack.imgur.com/azhsq.jpg (Link since I cannot post images on Stackoverflow yet).

What I tried

I already set the UITextView insets with:

messageTextView.textContainerInset = .zero
messageTextView.textContainer.lineFragmentPadding = .zero
messageTextView.contentInset = .zero

which removed the normal padding the UITextView has (using this answer/thread).

My initial thought was that the messageTextView has a minimum height specified somewhere, which causes the view to expand when the text inside (one line) does not fill the minimum height. However I don't know if thats correct or if there's even a possibility to change a minimum height. Shrinking the font size creates a bigger spacing.

The chat bubble uses UIStackViews to automatically adjust its size to the contents. Before changing to a UITextView we used an UILabel which worked fine and did not create any spacing.

Has anybody experienced a similar issue?

benedom
  • 3
  • 1
  • *"Shrinking the font size creates a bigger spacing"* ... it sounds like something else is going on. Are you creating this via code or Storyboard / IB? – DonMag Nov 01 '22 at 13:35
  • @DonMag the entire chat bubble which holds the UIStackViews and also the UITextView is created in a .xib file – benedom Nov 01 '22 at 15:44
  • Add a screen-shot of your XIB layout - with all the constraints expanded in the document outline pane. Best would be to create a [mre] -- doesn't need any interactivity or data retrieval... just enough with a couple of strings to reproduce the issue. – DonMag Nov 01 '22 at 15:53
  • @DonMag I uploaded a screenshot of the XIB layout [here](https://imgur.com/a/NK7cocm). Meanwhile I will try to create a reproducible example. – benedom Nov 02 '22 at 14:58
  • a tip: when trying to debug layouts, give your elements contrasting background colors. Almost impossible to tell what's there looking at that screenshot. – DonMag Nov 02 '22 at 15:03
  • @DonMag I put some background colors on, should be better visible now [here](https://imgur.com/a/P9duwoX). – benedom Nov 02 '22 at 15:51
  • Hmmm... the layout *appears* correct. Tough to say without seeing the actual files. I'm adding an answer with some samples that work for my quick testing, and that may help you figure out what's going on with yours. – DonMag Nov 02 '22 at 19:27

2 Answers2

0

The layout you've shown appears correct... but something is not-quite-right.

I put this together as a quick test - it should be pretty close to what you have...

How the cell xib looks in IB:

enter image description here

How it looks when running - 5 "messages" related twice. The first set has the "documentsStack and imagesStack" hidden, the second set they are showing (with no subviews, but with 10-point height constraint):

enter image description here

enter image description here

and here's the Debug View Hierarchy look:

enter image description here

enter image description here

So... source for the xib:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
    <device id="retina6_1" orientation="portrait" appearance="light"/>
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
        <capability name="System colors in document resources" minToolsVersion="11.0"/>
        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
    </dependencies>
    <objects>
        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
        <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="315" id="efZ-1S-bN1" customClass="RightStandardMessageCell" customModule="qtest" customModuleProvider="target">
            <rect key="frame" x="0.0" y="0.0" width="442" height="315"/>
            <autoresizingMask key="autoresizingMask"/>
            <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="efZ-1S-bN1" id="mpK-CA-Lnn">
                <rect key="frame" x="0.0" y="0.0" width="442" height="315"/>
                <autoresizingMask key="autoresizingMask"/>
                <subviews>
                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="2vd-KS-ll8">
                        <rect key="frame" x="106" y="8" width="328" height="299"/>
                        <color key="backgroundColor" red="0.71935945749999997" green="0.8054707646" blue="0.8795467615" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                    </imageView>
                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="7Bs-WC-QCo">
                        <rect key="frame" x="118" y="20" width="304" height="275"/>
                        <subviews>
                            <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="bottom" translatesAutoresizingMaskIntoConstraints="NO" id="Ykv-rE-DFT">
                                <rect key="frame" x="0.0" y="0.0" width="304" height="227"/>
                                <subviews>
                                    <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" text="Text View" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="xKc-db-bx1" userLabel="Message Text View" customClass="MyTextViewLabel" customModule="qtest" customModuleProvider="target">
                                        <rect key="frame" x="220.5" y="0.0" width="83.5" height="227"/>
                                        <color key="backgroundColor" red="0.41512373645565315" green="0.80431303791610775" blue="0.90702960527304444" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                                        <color key="textColor" systemColor="labelColor"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                        <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                                    </textView>
                                </subviews>
                                <color key="backgroundColor" red="0.79694263352245631" green="0.91265043646398214" blue="0.50891842927836861" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                            </stackView>
                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jKv-au-EZL">
                                <rect key="frame" x="0.0" y="231" width="304" height="10"/>
                                <color key="backgroundColor" systemColor="systemOrangeColor"/>
                                <constraints>
                                    <constraint firstAttribute="height" constant="10" id="AoS-qw-PIV"/>
                                </constraints>
                            </stackView>
                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9wb-aV-6Ah">
                                <rect key="frame" x="0.0" y="245" width="304" height="10"/>
                                <color key="backgroundColor" systemColor="systemPurpleColor"/>
                                <constraints>
                                    <constraint firstAttribute="height" constant="10" id="ZGO-O7-jpC"/>
                                </constraints>
                            </stackView>
                            <stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="wQ2-iG-u2V">
                                <rect key="frame" x="0.0" y="259" width="304" height="16"/>
                                <subviews>
                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83z-yh-Mfn" userLabel="Timestamp">
                                        <rect key="frame" x="0.0" y="0.0" width="278" height="16"/>
                                        <constraints>
                                            <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="16" id="zeh-oM-fP6"/>
                                        </constraints>
                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                        <nil key="textColor"/>
                                        <nil key="highlightedColor"/>
                                    </label>
                                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="checkmark.rectangle" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="fqI-0d-Wy4" userLabel="Checkmark Image View">
                                        <rect key="frame" x="286" y="1" width="18" height="13.5"/>
                                        <constraints>
                                            <constraint firstAttribute="width" constant="18" id="9NX-a1-Fzz"/>
                                            <constraint firstAttribute="height" constant="16" id="Pdb-F0-5qp"/>
                                        </constraints>
                                    </imageView>
                                </subviews>
                                <color key="backgroundColor" red="1" green="0.83234566450000003" blue="0.47320586440000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                            </stackView>
                        </subviews>
                    </stackView>
                </subviews>
                <color key="backgroundColor" red="0.93024982769507736" green="0.76566540916270232" blue="0.97523039579391479" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                <constraints>
                    <constraint firstItem="2vd-KS-ll8" firstAttribute="top" secondItem="mpK-CA-Lnn" secondAttribute="top" constant="8" id="95G-hj-ZRa"/>
                    <constraint firstItem="7Bs-WC-QCo" firstAttribute="top" secondItem="2vd-KS-ll8" secondAttribute="top" constant="12" id="ARj-eW-yGu"/>
                    <constraint firstItem="2vd-KS-ll8" firstAttribute="width" relation="lessThanOrEqual" secondItem="mpK-CA-Lnn" secondAttribute="width" multiplier="0.85" id="F2v-hy-pn9"/>
                    <constraint firstItem="7Bs-WC-QCo" firstAttribute="leading" secondItem="2vd-KS-ll8" secondAttribute="leading" constant="12" id="KIV-td-SQh"/>
                    <constraint firstItem="2vd-KS-ll8" firstAttribute="trailing" secondItem="mpK-CA-Lnn" secondAttribute="trailing" constant="-8" id="P1I-QI-v0Y"/>
                    <constraint firstAttribute="bottom" secondItem="2vd-KS-ll8" secondAttribute="bottom" constant="8" id="VsS-Hf-Fv6"/>
                    <constraint firstItem="7Bs-WC-QCo" firstAttribute="trailing" secondItem="2vd-KS-ll8" secondAttribute="trailing" constant="-12" id="YAZ-d9-R8n"/>
                    <constraint firstItem="7Bs-WC-QCo" firstAttribute="bottom" secondItem="2vd-KS-ll8" secondAttribute="bottom" priority="999" constant="-12" id="ke7-OJ-E00"/>
                </constraints>
            </tableViewCellContentView>
            <connections>
                <outlet property="bubbleImageView" destination="2vd-KS-ll8" id="OIs-Qk-bNu"/>
                <outlet property="documentsStack" destination="jKv-au-EZL" id="HU0-6P-C0b"/>
                <outlet property="footerStack" destination="wQ2-iG-u2V" id="zUl-YI-Gh0"/>
                <outlet property="imagesStack" destination="9wb-aV-6Ah" id="OlB-1t-tQ5"/>
                <outlet property="messageStack" destination="Ykv-rE-DFT" id="SIz-up-iY1"/>
                <outlet property="messageTextView" destination="xKc-db-bx1" id="805-2W-d4g"/>
                <outlet property="outerStack" destination="7Bs-WC-QCo" id="bM8-CF-WYS"/>
                <outlet property="timestamp" destination="83z-yh-Mfn" id="4hp-VP-93P"/>
            </connections>
            <point key="canvasLocation" x="489.85507246376818" y="264.17410714285711"/>
        </tableViewCell>
    </objects>
    <resources>
        <image name="checkmark.rectangle" catalog="system" width="128" height="93"/>
        <systemColor name="labelColor">
            <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
        <systemColor name="systemOrangeColor">
            <color red="1" green="0.58431372549019611" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </systemColor>
        <systemColor name="systemPurpleColor">
            <color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </systemColor>
    </resources>
</document>

The "text view acting like a label" class

class MyTextViewLabel: UITextView {
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        isEditable = false
        isScrollEnabled = false
        showsVerticalScrollIndicator = false
        showsHorizontalScrollIndicator = false
        
        textContainerInset = .zero
        textContainer.lineFragmentPadding = .zero
        contentInset = .zero
        
        dataDetectorTypes = .all
    }
    
}

Cell class

class RightStandardMessageCell: UITableViewCell {
    
    @IBOutlet var bubbleImageView: UIImageView!
    @IBOutlet var messageTextView: MyTextViewLabel!
    @IBOutlet var timestamp: UILabel!

    @IBOutlet var messageStack: UIStackView!
    @IBOutlet var documentsStack: UIStackView!
    @IBOutlet var imagesStack: UIStackView!
    @IBOutlet var footerStack: UIStackView!

    @IBOutlet var outerStack: UIStackView!

    var debugColors: [UIColor] = []
    var showDebugColors: Bool = false
    var debugViews: [UIView] = []
    
    override func awakeFromNib() {
        super.awakeFromNib()
        debugViews = [
            contentView,
            bubbleImageView, messageTextView, timestamp,
            messageStack, documentsStack, imagesStack, footerStack,
            outerStack,
        ]
        debugViews.forEach { v in
            debugColors.append(v.backgroundColor ?? .clear)
        }
        // because this is the "Right Standard" call
        messageTextView.textAlignment = .right
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        bubbleImageView.layer.cornerRadius = 12.0
        bubbleImageView.layer.masksToBounds = true
        
        for (v, c) in zip(debugViews, debugColors) {
            v.backgroundColor = showDebugColors ? c : .clear
        }
        // always use the bubble image view background
        bubbleImageView.backgroundColor = UIColor(red: 0.72, green: 0.80, blue: 0.90, alpha: 1.0)
        contentView.backgroundColor = showDebugColors ? debugColors.first : .gray
    }
}

and sample controller

class MsgTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let tableView = UITableView()
    
    var myData: [String] = [
        "Hallo",
        "This is a longer message.",
        "Hier ist mal eine Nachricht welche sich über zwei Zeilen erstreckt.",
        "Message with\nembedded\nnewline\ncharacters.",
        "Message with data detection...\nhttps://apple.com\n770-555-1212\nme@somedomain.com"
    ]
    
    var showDebugColors: Bool = true
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        // a button to toggle the cell's "debug" colors
        let btn: UIButton = {
            var cfg = UIButton.Configuration.filled()
            cfg.title = "Toggle Debug Colors"
            let b = UIButton(configuration: cfg)
            b.addTarget(self, action: #selector(toggleDebugColors(_:)), for: .touchUpInside)
            return b
        }()
        
        btn.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(btn)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([

            btn.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
            btn.centerXAnchor.constraint(equalTo: g.centerXAnchor),

            tableView.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            
        ])
        
        tableView.register(UINib(nibName: "RightStandardMessageCell", bundle: nil), forCellReuseIdentifier: "c")
        tableView.dataSource = self
        tableView.delegate = self
    }
    
    // MARK: - Table view data source
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // we'll show the same data twice
        //  - first set with documentsStack and imagesStack hidden
        //  - second set with documentsStack and imagesStack showing
        return myData.count * 2
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! RightStandardMessageCell
        
        cell.timestamp.text = "Row: \(indexPath.row)"
        cell.messageTextView.text = myData[indexPath.row % myData.count]
        
        // show the documentsStack and imagesStack for the 2nd set
        cell.documentsStack.isHidden = indexPath.row < myData.count
        cell.imagesStack.isHidden = indexPath.row < myData.count

        cell.showDebugColors = self.showDebugColors
        
        return cell
    }
    
    @objc func toggleDebugColors(_ sender: Any?) -> Void {
        showDebugColors.toggle()
        tableView.reloadData()
    }
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • It seems that the spacing in your example is created when the documents and image stack are not hidden. The Stackviews are hidden in my project and are not added, as it can be seen in the debug view. The spacing is solely coming from the `UITextView`, specifically the `CanvasView` which is larger than the `FragmentView`. Other than that your example seems to have the same properties set as my project. Maybe the spacing is caused by the layout of the `UITableView`? – benedom Nov 04 '22 at 11:06
  • @benedom - I'm assuming that when you **do not** have content for the docs and images stacks, they will be hidden. That's why I showed the two examples. However, in both cases, the textViews behave as desired -- no "extra space" on the bottom. So, yes, it sounds like *something* else you are doing with the layout is causing the issue. If you can create a [mre] I'd be happy to look at it. – DonMag Nov 04 '22 at 11:11
  • I managed to reproduce the issue and also found what was causing it. The `bubbleImageView` is being filled by an asset which is a pdf (idk why). It seems like this pdf has a minimum height and causes the issues with the `UITextView` **only** when it's one line. Cropping the asset to a smaller height removes the spacing. You should be able to reproduce the issue in your example by using [this asset](https://docdro.id/2xGT6kP) to fill the `bubbleImageView`. – benedom Nov 04 '22 at 14:32
  • @benedom - yep... figured it would be something like that. You might want to dump the `asset` and use a `UIView` subclass. Lots of examples out there -- this is one I just found with quick searching that would be easily "tweaked" to get your desired result: https://talsharon.github.io/Custom-Speech-Bubble/ – DonMag Nov 04 '22 at 15:17
-1
  • Use This SDK and apply min height then it's manage height automatically

https://github.com/KennethTsang/GrowingTextView

  • @benedom Could you provide any screenshots for your UI? – kumaresh saran Nov 01 '22 at 09:54
  • @ keyur kathrotiya unfortunately I cannot use any third party SDKs in this project @kumareshsaran I uploaded the screenshots from the view hierarchy here: https://imgur.com/a/nvrNYZM – benedom Nov 01 '22 at 09:59
  • @benedom Why are you not using UILabel instead of UITextView ? – keyur kathrotiya Nov 01 '22 at 10:03
  • @keyurkathrotiya we need a detection of links, emails, phone numbers and addresses in the chat message, so that they are clickable. Apples UITextView enables automatic detection for that data. – benedom Nov 01 '22 at 10:08