0
votes

I'm trying to implement a basic layout using a main horizontal UIStackView, with 2 children vertical UIStackViews.

enter image description here

Here is what I want:

  • the red view has 2 constraints: width = height = 50
  • the left vertical stackview(with the green label) should be as wide as its largest child, and should center all children horizontally.
  • the right vertical stackview(with the yellow label) should take up all available space.

Setting the alignment of the left vertical stack to fill expands the yellow label but do makes the red and green views have the same width. Changing the alignment to center make the left stack not hug it's children (just like in the image).

enter image description here

Any ideas how this should be done?

2

2 Answers

1
votes

The problem is that a vertical UIStackView with Alignment: Center has no intrinsic width.

To get the layout you want, you'll need to embed the RedView in either a clear UIView or another stack view, with its alignment set to Center.

Here's the approach with a stack view, as we need a couple fewer constraints than with a "container" view:

enter image description here

Since the RedView has width and height constraints, it will always be 50 x 50.

The "outer" stack view - HorizontalStack - is constrained to all four sides, with 8-pts "padding". Its properties are:

Axis: Horizontal
Alignment: Center
Distribution: Fill
Spacing: 0

The LeftVerticalStack properties are:

Axis: Vertical
Alignment: Fill
Distribution: Fill
Spacing: 0

And the RedContainerStack properties are:

Axis: Vertical
Alignment: Center
Distribution: Fill
Spacing: 0

One last necessary setting - the Green Label needs to hug its content and resist compression:

enter image description here

The result (with a few different strings in the labels):

enter image description here

Here's the source for MyCollCell.xib:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" 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="16087"/>
        <capability name="collection view cell content view" 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"/>
        <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="p9p-j5-QAK" customClass="MyCollCell" customModule="TableAdd" customModuleProvider="target">
            <rect key="frame" x="0.0" y="0.0" width="309" height="122"/>
            <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
            <collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="7Um-4C-5Kc">
                <rect key="frame" x="0.0" y="0.0" width="309" height="122"/>
                <autoresizingMask key="autoresizingMask"/>
                <subviews>
                    <stackView opaque="NO" contentMode="scaleToFill" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="Ezy-2A-s1y" userLabel="HorizontalStack">
                        <rect key="frame" x="8" y="8" width="293" height="106"/>
                        <subviews>
                            <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="63E-WY-6Uf" userLabel="LeftVerticalStack">
                                <rect key="frame" x="0.0" y="18" width="92.5" height="70.5"/>
                                <subviews>
                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="HgJ-l5-geV" userLabel="RedContainerStack">
                                        <rect key="frame" x="0.0" y="0.0" width="92.5" height="50"/>
                                        <subviews>
                                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="od9-kp-hKh" userLabel="RedView">
                                                <rect key="frame" x="21.5" y="0.0" width="50" height="50"/>
                                                <color key="backgroundColor" red="0.90437477830000002" green="0.1580897272" blue="0.20071530339999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                <constraints>
                                                    <constraint firstAttribute="height" constant="50" id="R5P-yj-c9R"/>
                                                    <constraint firstAttribute="width" constant="50" id="zOV-Xo-aVe"/>
                                                </constraints>
                                            </view>
                                        </subviews>
                                    </stackView>
                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Green Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzN-ta-GJB">
                                        <rect key="frame" x="0.0" y="50" width="92.5" height="20.5"/>
                                        <color key="backgroundColor" red="0.1673075259" green="0.65853333469999997" blue="0.26840484139999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                        <nil key="textColor"/>
                                        <nil key="highlightedColor"/>
                                    </label>
                                </subviews>
                            </stackView>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Yellow Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ReT-76-3Fl">
                                <rect key="frame" x="92.5" y="43" width="200.5" height="20.5"/>
                                <color key="backgroundColor" red="0.99657744169999996" green="0.79237681630000001" blue="0.0001898294868" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                <nil key="textColor"/>
                                <nil key="highlightedColor"/>
                            </label>
                        </subviews>
                    </stackView>
                </subviews>
                <color key="backgroundColor" red="0.46202266219999999" green="0.83828371759999998" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                <constraints>
                    <constraint firstAttribute="trailing" secondItem="Ezy-2A-s1y" secondAttribute="trailing" constant="8" id="FlO-FQ-5GG"/>
                    <constraint firstItem="Ezy-2A-s1y" firstAttribute="top" secondItem="7Um-4C-5Kc" secondAttribute="top" constant="8" id="ct6-Mf-RVp"/>
                    <constraint firstAttribute="bottom" secondItem="Ezy-2A-s1y" secondAttribute="bottom" constant="8" id="dsT-ps-AIz"/>
                    <constraint firstItem="Ezy-2A-s1y" firstAttribute="leading" secondItem="7Um-4C-5Kc" secondAttribute="leading" constant="8" id="wAj-T5-Vzs"/>
                </constraints>
            </collectionViewCellContentView>
            <size key="customSize" width="309" height="122"/>
            <connections>
                <outlet property="greenLabel" destination="xzN-ta-GJB" id="1Tu-hu-JOb"/>
                <outlet property="yellowLabel" destination="ReT-76-3Fl" id="zTt-ML-CsA"/>
            </connections>
            <point key="canvasLocation" x="165.94202898550725" y="145.98214285714286"/>
        </collectionViewCell>
    </objects>
</document>

And here is example code I used to produce the output shown above:

import UIKit

private let reuseIdentifier = "Cell"

// cell sizing extension and systemLayoutSizeFitting implementation
// is not mine, but can be found here:
// https://www.robertpieta.com/autosizing-full-width-cells/
extension UICollectionView {
    var widestCellWidth: CGFloat {
        let insets = contentInset.left + contentInset.right
        return bounds.width - insets
    }
}

class MyCollCell: UICollectionViewCell {
    
    @IBOutlet var greenLabel: UILabel!
    @IBOutlet var yellowLabel: UILabel!

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize,
        withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {
        
        // Replace the height in the target size to
        // allow the cell to flexibly compute its height
        var targetSize = targetSize
        targetSize.height = CGFloat.greatestFiniteMagnitude
        
        // The .required horizontal fitting priority means
        // the desired cell width (targetSize.width) will be
        // preserved. However, the vertical fitting priority is
        // .fittingSizeLevel meaning the cell will find the
        // height that best fits the content
        let size = super.systemLayoutSizeFitting(
            targetSize,
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        )
        
        return size
    }

}

class MyCollectionViewController: UICollectionViewController {

    let myData: [[String]] = [
        ["Green Label", "Yellow Label"],
        ["G", "Yellow Label"],
        ["Longer Green Label", "Yellow Label"],
        ["Green Label", "Yellow Label with too much text to fit here."],
        ["Green Label", "Yellow Label with .numberOfLines set to 0 will wrap when there's too much text."],
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Register cell classes
        self.collectionView.register(UINib(nibName: "MyCollCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)

        let layout = collectionView.collectionViewLayout
        if let flowLayout = layout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(
                width: collectionView.widestCellWidth,
                // Make the height a reasonable estimate to
                // ensure the scroll bar remains smooth
                height: 100
            )
        }

    }

    // MARK: UICollectionViewDataSource

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData.count
    }
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MyCollCell
    
        cell.greenLabel.text = myData[indexPath.row][0]
        cell.yellowLabel.text = myData[indexPath.row][1]
        
        cell.yellowLabel.numberOfLines = indexPath.item == 4 ? 0 : 1

        return cell
    }

}
0
votes

With reference to the photo I uploaded below, I placed the left vertical stack view in a UIView (Purple) and then embedded them in a horizontal stack view with the yellow label. I set the distribution to be equal spacing with a spacing of 0 so that there would be no spacing.

I set the constraints for all sides of the left vertical stack view to be 0 (so that the left vertical stack view would take the whole purple UIView). Set the alignment to center also so that the child views are in the center horizontally.

Inside the left vertical stack view, I added a constraint of 0 for the trailing, leading and bottom so that the left stack view is as "wide" as the largest children (which is the green label). I changed the label text to be center so that the text is at the center.

Hope this answers your question!

Check the image here