Search code examples
swiftautolayoutuistackview

Why Hugging priority is ignored?


I made a text bubble which have 2 dynamic horizontal constraints with superview: left with 1000 priority and right with 100 priority. If bubble was created by user left constraint will have 100 priority and right will have 1000 priority.

Inside of Bubble there is StackView with labels. Stack view is constrained to have the same size as bubble. Both hugging and compression priorities of labels are higher than 100 and lower than 1000(but upper label have a priority over lower).

Idea is simple, constraint with priority 100 will be broken, and bubble will be as big as it needs to show the whole text.

However it doesn't work. Bubbles are much bigger than they should be. It seems that autolayout ignores hugging priorities.

enter image description here

Here are description of all constraints of the bubble with text "Hhbhj":

<NSLayoutConstraint:0x283341630 V:|-(10)-[UIStackView:0x113310d90]   (active, names: '|':UIView:0x113310c20 )>
    <NSLayoutConstraint:0x283341810 H:[UIView:0x113310c20]-(>=16)-|   (active, names: '|':UITableViewCellContentView:0x113310a90 )>
    <NSLayoutConstraint:0x2833414a0 UILabel:0x113311360.height >= 14 priority:999   (active)>
    <NSLayoutConstraint:0x2833416d0 V:|-(6)-[UIView:0x113310c20]   (active, names: '|':UITableViewCellContentView:0x113310a90 )>
    <NSLayoutConstraint:0x283341540 H:|-(10)-[UIStackView:0x113310d90]   (active, names: '|':UIView:0x113310c20 )>
    <NSLayoutConstraint:0x2833415e0 V:[UIStackView:0x113310d90]-(10)-|   (active, names: '|':UIView:0x113310c20 )>
    <NSLayoutConstraint:0x283340be0 UIImageView:0x113310f20.width == 0 priority:1   (active)>
    <NSLayoutConstraint:0x283341680 H:[UIView:0x113310c20]-(16)-|   (active, names: '|':UITableViewCellContentView:0x113310a90 )>
    <NSLayoutConstraint:0x283341360 UILabel:0x1133110f0.height >= 24 priority:999   (active)>
    <NSLayoutConstraint:0x2833417c0 H:|-(16@100)-[UIView:0x113310c20] priority:100   (active, names: '|':UITableViewCellContentView:0x113310a90 )>
    <NSLayoutConstraint:0x283341770 H:|-(>=16)-[UIView:0x113310c20]   (active, names: '|':UITableViewCellContentView:0x113310a90 )>
    <NSLayoutConstraint:0x283340c80 UIImageView:0x113310f20.height == 183 priority:999   (active)>
    <NSLayoutConstraint:0x283341590 H:[UIStackView:0x113310d90]-(10)-|   (active, names: '|':UIView:0x113310c20 )>
    <NSLayoutConstraint:0x283341720 V:[UIView:0x113310c20]-(6)-|   (active, names: '|':UITableViewCellContentView:0x113310a90 )>

P.S You can also see an imageView in bubble. Don't worry if there is no image in bubble imageView will be hidden and constraints will be deactivated.
P.S.S Two vertical greater than constraints, just ensure that bubble will not get bigger than they need to be.


Solution

  • Here is one way to approach this...

    For your labels, give the top (the message) label:

    Content Hugging Priority
        Horizontal: 750
        Vertical: 1000
    
    Content Compression Resistance Priority
        Horizontal: 1000
        Vertical: 750
    

    and give the bottom (the time) label:

    Content Hugging Priority
        Horizontal: 251
        Vertical: 1000
    
    Content Compression Resistance Priority
        Horizontal: 1000
        Vertical: 750
    

    There are two examples here... the top one will constrain the "bubble view" to the cell's Content View margins. The bottom one will constrain the width of the "bubble view" to 80% of the width of the cell.

    Setup your constraints like this - ALL with Priority: 1000:

    enter image description here

    define @IBOutlet vars for the leading and trailing constraints:

    @IBOutlet var sentConstraint: NSLayoutConstraint!
    @IBOutlet var receivedConstraint: NSLayoutConstraint!
    

    connect sentConstraint to the Bubble View.trailing = trailingMargin constraint, and connect receivedConstraint to the Bubble View.leading = leadingMargin constraint.

    It will now look like this:

    enter image description here

    When you set the data for the cell, activate / deactivate the corresponding constraint, for example:

    // activate / deactivate Trailing / Leading constraints
    //  based on sent or received
    sentConstraint.isActive = msg.type == .sent
    receivedConstraint.isActive = msg.type == .received
    

    The >= Leading and Trailing constraints will prevent the bubble view from extending outside the content view frame.

    Here's how it could look:

    enter image description here

    enter image description here

    It's common in messaging apps to leave a bit of "padding" on the side. By limiting the width of the bubble view to 80%, it could look like this:

    enter image description here

    enter image description here

    Here's the code I used to produce this layout:

    enum MessageType {
        case sent, received
    }
    
    struct MessageStruct {
        var imageName: String = ""
        var message: String = ""
        var time: String = ""
        var type: MessageType = .sent
    }
    
    class BubbleCell: UITableViewCell {
        
        @IBOutlet var bubbleView: UIView!
        @IBOutlet var myImageView: UIImageView!
        @IBOutlet var messageLabel: UILabel!
        @IBOutlet var timeLabel: UILabel!
        
        @IBOutlet var sentConstraint: NSLayoutConstraint!
        @IBOutlet var receivedConstraint: NSLayoutConstraint!
    
        func fillData(_ msg: MessageStruct) -> Void {
    
            // I don't know how you're getting / setting your possible image,
            //  so I'll just set it to hidden for now
            myImageView.isHidden = true
    
            // set message label text
            messageLabel.text = msg.message
            
            // set time label text, append either sent or rec'd
            //  and set alignment
            timeLabel.text = msg.time + (msg.type == .sent ? " - sent" : " - rec'd")
            timeLabel.textAlignment = msg.type == .sent ? .right : .left
            
            // set colors based on sent or received
            bubbleView.backgroundColor = msg.type == .sent ? .systemGreen : .white
            messageLabel.textColor = msg.type == .sent ? .white : .black
            timeLabel.textColor = msg.type == .sent ? .white : .black
            
            // activate / deactivate Trailing / Leading constraints
            //  based on sent or received
            sentConstraint.isActive = msg.type == .sent
            receivedConstraint.isActive = msg.type == .received
            
            // set corner radii based on sent or received
            bubbleView.layer.cornerRadius = 16
            bubbleView.layer.maskedCorners = msg.type == .sent
                ? [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMinXMaxYCorner]
                : [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
            
        }
        
    }
    
    class MessageTableViewController: UITableViewController {
        
        var myData: [MessageStruct] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            var msg: MessageStruct!
            
            msg = MessageStruct(imageName: "",
                                message: "Short message.",
                                time: "09:15",
                                type: .sent)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "Short message.",
                                time: "09:15",
                                type: .received)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "This is a longer message.",
                                time: "09:20",
                                type: .sent)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "This is a longer message.",
                                time: "09:20",
                                type: .received)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "This message should be long enough that it needs to wrap in the cell.",
                                time: "09:25",
                                type: .sent)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "Another Short message.",
                                time: "09:30",
                                type: .sent)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "Another message, long enough that it will need to wrap in the cell.",
                                time: "09:35",
                                type: .sent)
            myData.append(msg)
            
            msg = MessageStruct(imageName: "",
                                message: "Another message, long enough that it will need to wrap in the cell.",
                                time: "09:35",
                                type: .received)
            myData.append(msg)
            
            tableView.separatorStyle = .none
        }
        
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData.count
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            // use "bubbleCell" identifier for margin-width limit
            // use "bubble80Cell" identifier for 80% width limit
            let c = tableView.dequeueReusableCell(withIdentifier: "bubbleCell", for: indexPath) as! BubbleCell
            let d = myData[indexPath.row]
            c.fillData(d)
            return c
        }
        
    }
    

    and here's the Storyboard source so you can inspect it:

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="t48-nh-14V">
        <device id="retina4_7" orientation="portrait" appearance="light"/>
        <dependencies>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
            <capability name="System colors in document resources" minToolsVersion="11.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <scenes>
            <!--Message Table View Controller-->
            <scene sceneID="mMr-vR-h6X">
                <objects>
                    <tableViewController id="t48-nh-14V" customClass="MessageTableViewController" customModule="QuickTest" 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="BrW-mC-6e6">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                            <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="bubbleCell" rowHeight="260" id="Oi2-T7-oyW" customClass="BubbleCell" customModule="QuickTest" customModuleProvider="target">
                                    <rect key="frame" x="0.0" y="28" width="375" height="260"/>
                                    <autoresizingMask key="autoresizingMask"/>
                                    <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Oi2-T7-oyW" id="gQY-hx-B2I">
                                        <rect key="frame" x="0.0" y="0.0" width="375" height="260"/>
                                        <autoresizingMask key="autoresizingMask"/>
                                        <subviews>
                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="h2Z-Bt-MGu">
                                                <rect key="frame" x="16" y="11" width="343" height="238"/>
                                                <subviews>
                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="wrN-dr-6PW">
                                                        <rect key="frame" x="8" y="12" width="327" height="214"/>
                                                        <subviews>
                                                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Aio-Ve-8b1">
                                                                <rect key="frame" x="0.0" y="0.0" width="327" height="165.5"/>
                                                            </imageView>
                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="750" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" text="Label" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="83w-nH-MFV">
                                                                <rect key="frame" x="0.0" y="171.5" width="327" height="20.5"/>
                                                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                                                <nil key="textColor"/>
                                                                <nil key="highlightedColor"/>
                                                            </label>
                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" text="13:50 - sent" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3MO-It-xQw">
                                                                <rect key="frame" x="0.0" y="198" width="327" height="16"/>
                                                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
                                                                <nil key="textColor"/>
                                                                <nil key="highlightedColor"/>
                                                            </label>
                                                        </subviews>
                                                    </stackView>
                                                </subviews>
                                                <color key="backgroundColor" systemColor="systemGreenColor"/>
                                                <constraints>
                                                    <constraint firstItem="wrN-dr-6PW" firstAttribute="top" secondItem="h2Z-Bt-MGu" secondAttribute="top" constant="12" id="61F-T7-VXd"/>
                                                    <constraint firstAttribute="trailing" secondItem="wrN-dr-6PW" secondAttribute="trailing" constant="8" id="Aso-zu-ZPc"/>
                                                    <constraint firstItem="wrN-dr-6PW" firstAttribute="leading" secondItem="h2Z-Bt-MGu" secondAttribute="leading" constant="8" id="bsG-sY-260"/>
                                                    <constraint firstAttribute="bottom" secondItem="wrN-dr-6PW" secondAttribute="bottom" constant="12" id="oAO-Jt-UX3"/>
                                                </constraints>
                                            </view>
                                        </subviews>
                                        <color key="backgroundColor" red="0.897361517" green="0.8974906802" blue="0.89733326440000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                        <constraints>
                                            <constraint firstAttribute="trailingMargin" relation="greaterThanOrEqual" secondItem="h2Z-Bt-MGu" secondAttribute="trailing" id="6hf-N2-14b"/>
                                            <constraint firstAttribute="bottomMargin" secondItem="h2Z-Bt-MGu" secondAttribute="bottom" id="FxF-PE-6iW"/>
                                            <constraint firstItem="h2Z-Bt-MGu" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="gQY-hx-B2I" secondAttribute="leadingMargin" id="Wsa-eO-InO"/>
                                            <constraint firstItem="h2Z-Bt-MGu" firstAttribute="top" secondItem="gQY-hx-B2I" secondAttribute="topMargin" id="ZYU-hS-2Pl"/>
                                            <constraint firstItem="h2Z-Bt-MGu" firstAttribute="trailing" secondItem="gQY-hx-B2I" secondAttribute="trailingMargin" id="edX-9l-Ye2"/>
                                            <constraint firstItem="h2Z-Bt-MGu" firstAttribute="leading" secondItem="gQY-hx-B2I" secondAttribute="leadingMargin" id="s0J-rx-Rzx"/>
                                        </constraints>
                                    </tableViewCellContentView>
                                    <connections>
                                        <outlet property="bubbleView" destination="h2Z-Bt-MGu" id="uJ0-t9-ZQ9"/>
                                        <outlet property="messageLabel" destination="83w-nH-MFV" id="pEd-Gg-49x"/>
                                        <outlet property="myImageView" destination="Aio-Ve-8b1" id="qJQ-G9-Qc6"/>
                                        <outlet property="receivedConstraint" destination="s0J-rx-Rzx" id="fCI-9h-2V1"/>
                                        <outlet property="sentConstraint" destination="edX-9l-Ye2" id="KKm-FT-Zcq"/>
                                        <outlet property="timeLabel" destination="3MO-It-xQw" id="bxt-vZ-c2v"/>
                                    </connections>
                                </tableViewCell>
                                <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="bubble80Cell" rowHeight="260" id="DAc-Zm-a8s" customClass="BubbleCell" customModule="QuickTest" customModuleProvider="target">
                                    <rect key="frame" x="0.0" y="288" width="375" height="260"/>
                                    <autoresizingMask key="autoresizingMask"/>
                                    <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DAc-Zm-a8s" id="te5-Rn-aLa">
                                        <rect key="frame" x="0.0" y="0.0" width="375" height="260"/>
                                        <autoresizingMask key="autoresizingMask"/>
                                        <subviews>
                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lFi-s6-a3X">
                                                <rect key="frame" x="16" y="11" width="300" height="238"/>
                                                <subviews>
                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="HX0-dn-sRz">
                                                        <rect key="frame" x="8" y="12" width="284" height="214"/>
                                                        <subviews>
                                                            <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Xsw-i1-9mU">
                                                                <rect key="frame" x="0.0" y="0.0" width="284" height="165.5"/>
                                                            </imageView>
                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="750" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" text="Label" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6Zs-Af-RNQ">
                                                                <rect key="frame" x="0.0" y="171.5" width="284" height="20.5"/>
                                                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                                                <nil key="textColor"/>
                                                                <nil key="highlightedColor"/>
                                                            </label>
                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" text="13:50 - sent" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7nV-rU-i4W">
                                                                <rect key="frame" x="0.0" y="198" width="284" height="16"/>
                                                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
                                                                <nil key="textColor"/>
                                                                <nil key="highlightedColor"/>
                                                            </label>
                                                        </subviews>
                                                    </stackView>
                                                </subviews>
                                                <color key="backgroundColor" systemColor="systemGreenColor"/>
                                                <constraints>
                                                    <constraint firstItem="HX0-dn-sRz" firstAttribute="leading" secondItem="lFi-s6-a3X" secondAttribute="leading" constant="8" id="6Kk-f4-tiB"/>
                                                    <constraint firstItem="HX0-dn-sRz" firstAttribute="top" secondItem="lFi-s6-a3X" secondAttribute="top" constant="12" id="7H5-6V-Ag0"/>
                                                    <constraint firstAttribute="trailing" secondItem="HX0-dn-sRz" secondAttribute="trailing" constant="8" id="Czf-wx-QBQ"/>
                                                    <constraint firstAttribute="bottom" secondItem="HX0-dn-sRz" secondAttribute="bottom" constant="12" id="edZ-aP-M1N"/>
                                                </constraints>
                                            </view>
                                        </subviews>
                                        <color key="backgroundColor" red="0.897361517" green="0.8974906802" blue="0.89733326440000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                        <constraints>
                                            <constraint firstItem="lFi-s6-a3X" firstAttribute="trailing" secondItem="te5-Rn-aLa" secondAttribute="trailingMargin" id="Ua5-F4-YR5"/>
                                            <constraint firstAttribute="bottomMargin" secondItem="lFi-s6-a3X" secondAttribute="bottom" id="WKr-87-LJK"/>
                                            <constraint firstItem="lFi-s6-a3X" firstAttribute="leading" secondItem="te5-Rn-aLa" secondAttribute="leadingMargin" id="h4U-ie-xYq"/>
                                            <constraint firstItem="lFi-s6-a3X" firstAttribute="width" relation="lessThanOrEqual" secondItem="te5-Rn-aLa" secondAttribute="width" multiplier="0.8" id="sKr-Xc-5bn"/>
                                            <constraint firstItem="lFi-s6-a3X" firstAttribute="top" secondItem="te5-Rn-aLa" secondAttribute="topMargin" id="ueL-NI-qoX"/>
                                        </constraints>
                                    </tableViewCellContentView>
                                    <connections>
                                        <outlet property="bubbleView" destination="lFi-s6-a3X" id="5qj-C0-Uf3"/>
                                        <outlet property="messageLabel" destination="6Zs-Af-RNQ" id="YTU-M9-s7P"/>
                                        <outlet property="myImageView" destination="Xsw-i1-9mU" id="Unr-HS-Iv1"/>
                                        <outlet property="receivedConstraint" destination="h4U-ie-xYq" id="tCy-cc-uHa"/>
                                        <outlet property="sentConstraint" destination="Ua5-F4-YR5" id="Kgg-gY-JCv"/>
                                        <outlet property="timeLabel" destination="7nV-rU-i4W" id="GUX-T1-4Nc"/>
                                    </connections>
                                </tableViewCell>
                            </prototypes>
                            <connections>
                                <outlet property="dataSource" destination="t48-nh-14V" id="3oa-OS-rO8"/>
                                <outlet property="delegate" destination="t48-nh-14V" id="7Ua-sV-mwS"/>
                            </connections>
                        </tableView>
                        <navigationItem key="navigationItem" id="fpm-tv-1pb"/>
                    </tableViewController>
                    <placeholder placeholderIdentifier="IBFirstResponder" id="2On-8d-2Kp" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
                </objects>
                <point key="canvasLocation" x="-1447.2" y="-749.7751124437782"/>
            </scene>
        </scenes>
        <resources>
            <systemColor name="systemBackgroundColor">
                <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            </systemColor>
            <systemColor name="systemGreenColor">
                <color red="0.20392156862745098" green="0.7803921568627451" blue="0.34901960784313724" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            </systemColor>
        </resources>
    </document>
    

    Edit

    Worth noting -- if we replace these two lines in fillData():

        sentConstraint.isActive = msg.type == .sent
        receivedConstraint.isActive = msg.type == .received
        
    

    with these:

        sentConstraint.priority = msg.type == .sent ? .defaultHigh : .defaultLow
        receivedConstraint.priority = msg.type == .received ? .defaultHigh : .defaultLow
    

    we'll accomplish the same goal.

    Without seeing your actual layout (difficult to decipher the "description of all constraints" you posted), and without seeing the code you are using, it's tough to say why that wasn't working for you.