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:
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):
and here's the Debug View Hierarchy look:
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