Search code examples
iosautolayout

How to make a stack view with subviews that has different alignments?


My goal

I am making a UI that has an image at the top, and four buttons below it, arranged vertically. The height of the image depends on its width, but it shouldn't be too tall that there isn't enough space for the buttons. After the image is positioned, the buttons can fill up the rest of the space. Horizontally, The image should try to take up as much of the width of the screen as possible, but the buttons should have a constant width of 300 and be centered horizontally. I understand that this is a bad idea for localisation, but I don't plan on localising this app.

A picture is worth 1000 words:

enter image description here

I have only used the stack overflow logo to replace the actual image that I will be using. I am not making a Stack Overflow-related app.

Also, the image view should be on the left of the buttons when the vertical size class is compact (the buttons are still vertically arranged).

I think I basically wanted a stack view whose subviews are aligned differently - the image view is aligned with "Fill", and the buttons are aligned with "Center". However, I don't know how to do that, so I tried to use nested stack views to work around this.

How to reproduce:

The outer stack view top/bottom/leading/trailing all are pinned to the VC's view, alignment set to "Fill", distribution set to "Fill Proportionally". I chose "Fill Proportionally" because I found that works the best if I use a large enough image. I added a variation on the outer stack view's axis, so that it is set to horizontal when vertical size class is compact.

The outer stack view has 2 arranged subviews - the image view and the inner stack view, whose distribution is set to "Fill", alignment is set to "Center".

The inner stack view then has those buttons. Each button has a constant width constraint.

I thought that should do the job. When I run the app, I see some "unable to satisfy constraints" warnings:

(
    "<NSLayoutConstraint:0x6000038d2b20 UIButton:0x7f82d250e3c0'BUTTON1'.width == 300   (active)>",
    "<NSLayoutConstraint:0x6000038d98b0 'fittingSizeHTarget' UIStackView:0x7f82d250e230.width == 0   (active)>",
    "<NSLayoutConstraint:0x6000038d8cd0 'UISV-canvas-connection' UIStackView:0x7f82d250e230.leading == _UILayoutSpacer:0x6000024e01e0'UISV-alignment-spanner'.leading   (active)>",
    "<NSLayoutConstraint:0x6000038d9130 'UISV-canvas-connection' UIStackView:0x7f82d250e230.centerX == UIButton:0x7f82d250e3c0'BUTTON1'.centerX   (active)>",
    "<NSLayoutConstraint:0x6000038d8e10 'UISV-spanning-boundary' _UILayoutSpacer:0x6000024e01e0'UISV-alignment-spanner'.leading <= UIButton:0x7f82d250e3c0'BUTTON1'.leading   (active)>"
)
Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x6000038d2b20 UIButton:0x7f82d250e3c0'BUTTON1'.width == 300   (active)>

There's a set of those for each button! Other than that, the layout seem to work fine, but I don't know when this will actually break something further down the line though.

I looked at which constraints are conflicting and which got broken. Apparently there's a StackView.width = 0 constraint that conflicts with the button1.width = 300 constraint, and the latter got broken. I don't know where the StackView.width = 0 constraint came from, nor do I know which stack view it refers to :(

How can I prevent the constraints from breaking?

If my steps to reproduce weren't clear enough, here is the storyboard code for the VC:

    <scene sceneID="N06-uN-k0T">
        <objects>
            <viewController id="BdK-0G-Xl3" userLabel="Probelm VC" sceneMemberID="viewController">
                <view key="view" contentMode="scaleToFill" id="OpP-9z-h5Y">
                    <rect key="frame" x="0.0" y="0.0" width="667" height="375"/>
                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                    <subviews>
                        <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="nvX-HI-4ox">
                            <rect key="frame" x="16" y="16" width="635" height="343"/>
                            <subviews>
                                <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="soLogo" translatesAutoresizingMaskIntoConstraints="NO" id="1RQ-jl-D6x">
                                    <rect key="frame" x="0.0" y="0.0" width="327" height="343"/>
                                </imageView>
                                <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="ROW-bu-Xw7">
                                    <rect key="frame" x="335" y="0.0" width="300" height="343"/>
                                    <subviews>
                                        <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hpX-JI-GT0">
                                            <rect key="frame" x="0.0" y="0.0" width="300" height="78.5"/>
                                            <color key="backgroundColor" red="0.23137254900000001" green="0.4823529412" blue="0.23137254900000001" alpha="1" colorSpace="calibratedRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="width" constant="300" id="Ogw-Xr-6DE"/>
                                            </constraints>
                                            <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                            <state key="normal" title="BUTTON1"/>
                                        </button>
                                        <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dJT-f3-AJM">
                                            <rect key="frame" x="0.0" y="88.5" width="300" height="78"/>
                                            <color key="backgroundColor" red="0.23137254900000001" green="0.4823529412" blue="0.23137254900000001" alpha="1" colorSpace="calibratedRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="width" constant="300" id="AII-kQ-4cH"/>
                                            </constraints>
                                            <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                            <state key="normal" title="BUTTON2"/>
                                        </button>
                                        <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NZx-LS-e5l">
                                            <rect key="frame" x="0.0" y="176.5" width="300" height="78.5"/>
                                            <color key="backgroundColor" red="0.23137254900000001" green="0.4823529412" blue="0.23137254900000001" alpha="1" colorSpace="calibratedRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="width" constant="300" id="rDS-bU-7lE"/>
                                            </constraints>
                                            <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                            <state key="normal" title="BUTTON3"/>
                                        </button>
                                        <button opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="onC-ie-E6z">
                                            <rect key="frame" x="0.0" y="265" width="300" height="78"/>
                                            <color key="backgroundColor" red="0.23137254900000001" green="0.4823529412" blue="0.23137254900000001" alpha="1" colorSpace="calibratedRGB"/>
                                            <constraints>
                                                <constraint firstAttribute="width" constant="300" id="4K7-7z-YHF"/>
                                            </constraints>
                                            <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                            <state key="normal" title="BUTTON4"/>
                                        </button>
                                    </subviews>
                                </stackView>
                            </subviews>
                            <variation key="heightClass=compact" axis="horizontal"/>
                        </stackView>
                    </subviews>
                    <viewLayoutGuide key="safeArea" id="wgl-gV-tap"/>
                    <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                    <constraints>
                        <constraint firstItem="nvX-HI-4ox" firstAttribute="top" secondItem="wgl-gV-tap" secondAttribute="top" constant="16" id="XjW-5g-rZt"/>
                        <constraint firstItem="nvX-HI-4ox" firstAttribute="leading" secondItem="wgl-gV-tap" secondAttribute="leading" constant="16" id="eYR-DM-EMX"/>
                        <constraint firstItem="wgl-gV-tap" firstAttribute="trailing" secondItem="nvX-HI-4ox" secondAttribute="trailing" constant="16" id="xhu-Xr-XWi"/>
                        <constraint firstItem="wgl-gV-tap" firstAttribute="bottom" secondItem="nvX-HI-4ox" secondAttribute="bottom" constant="16" id="z5a-h7-F7G"/>
                    </constraints>
                </view>
            </viewController>
            <placeholder placeholderIdentifier="IBFirstResponder" id="MTA-xX-zRK" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
        </objects>
        <point key="canvasLocation" x="-183" y="865"/>
    </scene>

Solution

  • OK - based on the image you posted, I'm going to use a 2:1 aspect ratio for your "Logo Image"...

    Start with a "basic" layout:

    enter image description here

    Here's the constraints:

    enter image description here

    the Outer StackView properties:

    enter image description here

    and the Buttons StackView properties:

    enter image description here

    At this point, we should be "good to go" with "portrait" layout.

    Let's add some trait variations...

    To get this layout:

    enter image description here

    We'll add `Width: Any / Height: Compact" for the Axis and Alignment of the OuterStack:

    enter image description here

    and we'll add `Width: Any / Height: Compact" for the Alignment of the ButtonsStack:

    enter image description here

    If we run that (iPhone 12 sim), we get this:

    enter image description here

    enter image description here

    We're close, but... we get a whole mess of auto-layout warning / error messages in the debug console.

    That's because (from my experience) auto-layout needs to make multiple "passes" to fully evaluate the layout, particularly when using Aspect-Ratio constraints mixed with Stack Views.

    To get rid of that, we'll give the Image View's Aspect-Ratio constraint a less-than-required Priority:

    enter image description here

    This, effectively, allows auto-layout to break constraints during its multiple layout passes.

    Here's an entire Storyboard source so you can more closely inspect things:

    <?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="2ea-UR-he0">
        <device id="retina4_7" orientation="portrait" appearance="light"/>
        <dependencies>
            <deployment identifier="iOS"/>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="System colors in document resources" minToolsVersion="11.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <scenes>
            <!--View Controller-->
            <scene sceneID="Kgd-qU-TMb">
                <objects>
                    <viewController id="2ea-UR-he0" sceneMemberID="viewController">
                        <view key="view" contentMode="scaleToFill" id="oLK-rW-5I7">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
                            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                            <subviews>
                                <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="bhh-Ra-9sa" userLabel="OuterStack">
                                    <rect key="frame" x="16" y="16" width="343" height="635"/>
                                    <subviews>
                                        <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="soLogo" translatesAutoresizingMaskIntoConstraints="NO" id="zgv-cZ-TeJ">
                                            <rect key="frame" x="0.0" y="0.0" width="343" height="171.5"/>
                                            <constraints>
                                                <constraint firstAttribute="width" secondItem="zgv-cZ-TeJ" secondAttribute="height" multiplier="2:1" priority="750" id="0U3-Il-GO7"/>
                                            </constraints>
                                        </imageView>
                                        <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="bq4-eZ-cNR" userLabel="ButtonsStack">
                                            <rect key="frame" x="0.0" y="171.5" width="343" height="463.5"/>
                                            <subviews>
                                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xKR-aa-qSg">
                                                    <rect key="frame" x="21.5" y="0.0" width="300" height="108.5"/>
                                                    <color key="backgroundColor" red="0.16262620689999999" green="0.55341011289999997" blue="0.26840737460000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                    <constraints>
                                                        <constraint firstAttribute="width" constant="300" id="Omy-Kc-rR2"/>
                                                    </constraints>
                                                    <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                                    <state key="normal" title="Button 1"/>
                                                </button>
                                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KTN-zM-mhK">
                                                    <rect key="frame" x="21.5" y="118.5" width="300" height="108.5"/>
                                                    <color key="backgroundColor" red="0.16262620689999999" green="0.55341011289999997" blue="0.26840737460000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                    <constraints>
                                                        <constraint firstAttribute="width" constant="300" id="7cU-at-nc8"/>
                                                    </constraints>
                                                    <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                                    <state key="normal" title="Button 2"/>
                                                </button>
                                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jP5-6h-Pb5">
                                                    <rect key="frame" x="21.5" y="237" width="300" height="108"/>
                                                    <color key="backgroundColor" red="0.16262620689999999" green="0.55341011289999997" blue="0.26840737460000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                    <constraints>
                                                        <constraint firstAttribute="width" constant="300" id="9qP-Dd-5pW"/>
                                                    </constraints>
                                                    <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                                    <state key="normal" title="Button 3"/>
                                                </button>
                                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="jhr-47-0CJ">
                                                    <rect key="frame" x="21.5" y="355" width="300" height="108.5"/>
                                                    <color key="backgroundColor" red="0.16262620689999999" green="0.55341011289999997" blue="0.26840737460000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                                    <constraints>
                                                        <constraint firstAttribute="width" constant="300" id="c8c-CT-Sz5"/>
                                                    </constraints>
                                                    <fontDescription key="fontDescription" name="ChalkboardSE-Regular" family="Chalkboard SE" pointSize="52"/>
                                                    <state key="normal" title="Button 4"/>
                                                </button>
                                            </subviews>
                                            <variation key="heightClass=compact" alignment="fill"/>
                                        </stackView>
                                    </subviews>
                                    <variation key="heightClass=compact" alignment="center" axis="horizontal"/>
                                </stackView>
                            </subviews>
                            <viewLayoutGuide key="safeArea" id="guT-8A-oyH"/>
                            <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                            <constraints>
                                <constraint firstItem="bhh-Ra-9sa" firstAttribute="leading" secondItem="guT-8A-oyH" secondAttribute="leading" constant="16" id="6me-RD-7Uz"/>
                                <constraint firstItem="guT-8A-oyH" firstAttribute="trailing" secondItem="bhh-Ra-9sa" secondAttribute="trailing" constant="16" id="7oW-d7-Lhf"/>
                                <constraint firstItem="guT-8A-oyH" firstAttribute="bottom" secondItem="bhh-Ra-9sa" secondAttribute="bottom" constant="16" id="9Kz-Ok-e52"/>
                                <constraint firstItem="bhh-Ra-9sa" firstAttribute="top" secondItem="guT-8A-oyH" secondAttribute="top" constant="16" id="DY2-wl-5Y8"/>
                            </constraints>
                        </view>
                    </viewController>
                    <placeholder placeholderIdentifier="IBFirstResponder" id="b0R-on-Vlz" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
                </objects>
                <point key="canvasLocation" x="1055" y="206"/>
            </scene>
        </scenes>
        <resources>
            <image name="soLogo" width="230" height="115"/>
            <systemColor name="systemBackgroundColor">
                <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            </systemColor>
        </resources>
    </document>