Home > Back-end >  TextView adds bottom padding/space when text is one line
TextView adds bottom padding/space when text is one line

Time:11-03

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: 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\[email protected]"
    ]
    
    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()
    }
}

CodePudding user response:

  • Use This SDK and apply min height then it's manage height automatically

https://github.com/KennethTsang/GrowingTextView

  • Related