skip to Main Content

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://imgur.com/a/nvrNYZM (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?

2

Answers


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

    https://github.com/KennethTsang/GrowingTextView

    Login or Signup to reply.
  1. 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 withnembeddednnewlinencharacters.",
            "Message with data detection...nhttps://[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()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search