Xcode 12, "Vary for Traits" does not work (as expected)

Viewed 747

I have a, relatively, simple layout, which is divided into two main sections, which I've encompassed with two UIViews to make it easier to manage.

My goal was to provide two different layouts, one for portrait and one for landscape.

My first idea was to make use of "Vary for Traits" for both "width" and "height", so, I set the storyboard in portrait mode, clicked on "Vary for Traits", I applied the traits I wanted in portrait mode, clicked "Done", switched to landscape mode and repeated the process.

As you can see, switching between the orientations in the storyboard works just fine

Storyboard

Except, when I run it on a device ... it doesn't work ... as expected

Actual device

Okay , so I thought, I go back and do it again, but this time just vary for the "width" and yet again, it doesn't work.

So, I then thought, I'd go old school. I'd add each constraint and set a variant for the specific size class...

Constraints

(I have different constraints for both orientations, so a single constraint doesn't have a variant for both)

And ... again, this doesn't work. Before some tells me that "Installed" needs to be ticked for both orientations, no, it doesn't, that just makes Xcode angry and spit lots of nasty comments in the console.

The "question"

So, the "simple" question is, what am I doing wrong? Have I missed something bleedingly obvious which would make this work, or is having a separate constraint for both portrait and landscape just a really dumb idea (which I though what "Vary for Traits" was basically doing)

A "hacky" workaround

So, after spending WAY to much time on this, I fell back to an old "workflow" I've used in the past for manually created UIs

Starting from the point of the manually created constraints for each variant, I essentially, placed all of the landscape and portrait constraints into individual IBOutlet collections and when viewWillTransition was called, manually de/activated the required group of constraints...

class ViewController: UIViewController {

    @IBOutlet var landscapeConstraints: [NSLayoutConstraint]!
    @IBOutlet var portraitConstraints: [NSLayoutConstraint]!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    enum Orientation {
        case landscape
        case portrait
        case unknown
    }
    
    fileprivate var lastOrientation: Orientation = .unknown
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        updateCurrentConstraints(toSize: view.bounds.size)
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate { (ctx) in
            self.updateCurrentConstraints(toSize: size)
        } completion: { (ctx) in
        }
    }
    
    func updateCurrentConstraints(toSize size: CGSize) {
        var orientation: Orientation = .portrait
        
        if size.width > size.height {
            orientation = .landscape
        }
        
        guard orientation != lastOrientation else { return }
        lastOrientation = orientation

        guard let portraitConstraints = portraitConstraints, let landscapeConstraints = landscapeConstraints else {
            print("No constraits")
            return
        }
        
        var activeConstraints = portraitConstraints
        var inactiveConstraints = landscapeConstraints

        if orientation == .landscape {
            activeConstraints = landscapeConstraints
            inactiveConstraints = portraitConstraints
        }

        for constraint in inactiveConstraints {
            constraint.isActive = false
        }
        for constraint in activeConstraints {
            constraint.isActive = true
        }
        self.view.setNeedsLayout()
        self.view.layoutIfNeeded()
    }
}

This works, I wish it didn't and I'm hoping I've just done something seriously stupid ... other then the hacky workaround

If, you don't have much to do and want to muck around with the it, there is a source repo which I used to test the concept and come up with my "super, awesome" workaround, as I appreciate any kind of storyboard related question is a complete pain in the ... code to work with

1 Answers

The problem you're hitting is that you've set Trait Variations only for wC hR and wR hC ... so your constraints don't know what to do when you hit a combination such as wC hC (iPhone 8 / iPhone SE 2 / etc) or wR hR (iPads).

What you want to do is vary your traits only for the Height class.

Here is how it can look in Storyboard:

enter image description here

enter image description here

If I select the RedView's width = 175 constraint, the Attributes Inspector shows:

enter image description here

compared to RedView's width = 250 constraint:

enter image description here

Here's the Storyboard source for this layout ... it will handle the device rotation properly, with NO code needed:

<?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="5gV-rv-xrH">
    <device id="retina6_1" orientation="landscape" appearance="light"/>
    <dependencies>
        <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="HjL-cd-83j">
            <objects>
                <viewController id="5gV-rv-xrH" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="hIj-xH-n33">
                        <rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Q3p-xw-14f" userLabel="LeftButton">
                                <rect key="frame" x="64" y="5.5" width="46" height="30"/>
                                <color key="backgroundColor" red="0.97625964880000005" green="0.85085996409999998" blue="0.83597719680000004" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                                <state key="normal" title="Button"/>
                            </button>
                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="This is a title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="35a-vA-spC" userLabel="TitleLabel">
                                <rect key="frame" x="118" y="0.0" width="660" height="41"/>
                                <color key="backgroundColor" red="0.83216959239999999" green="0.98548370600000001" blue="0.47333085539999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                                <fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
                                <nil key="textColor"/>
                                <nil key="highlightedColor"/>
                            </label>
                            <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="OqS-4d-vWh" userLabel="RightButton">
                                <rect key="frame" x="786" y="5.5" width="46" height="30"/>
                                <color key="backgroundColor" red="0.97625964880000005" green="0.85085996409999998" blue="0.83597719680000004" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                                <state key="normal" title="Button"/>
                            </button>
                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TjE-LU-Mo5" userLabel="RedView">
                                <rect key="frame" x="60" y="49" width="175" height="219"/>
                                <color key="backgroundColor" red="0.96804410220000003" green="0.21863630410000001" blue="0.1990364194" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                                <constraints>
                                    <constraint firstAttribute="width" constant="175" id="FPV-f9-dlF"/>
                                    <constraint firstAttribute="width" constant="250" id="NhS-uQ-oMA"/>
                                    <constraint firstAttribute="width" secondItem="TjE-LU-Mo5" secondAttribute="height" multiplier="0.8" id="PY6-e6-WhI"/>
                                </constraints>
                                <variation key="default">
                                    <mask key="constraints">
                                        <exclude reference="NhS-uQ-oMA"/>
                                    </mask>
                                </variation>
                                <variation key="heightClass=regular">
                                    <mask key="constraints">
                                        <exclude reference="FPV-f9-dlF"/>
                                        <include reference="NhS-uQ-oMA"/>
                                    </mask>
                                </variation>
                            </view>
                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Y45-dN-gv7" userLabel="YellowView">
                                <rect key="frame" x="243" y="49" width="609" height="344"/>
                                <color key="backgroundColor" red="0.98267000910000002" green="0.80788773300000005" blue="0.061180472370000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                            </view>
                        </subviews>
                        <viewLayoutGuide key="safeArea" id="9Rq-n0-LVs"/>
                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                        <constraints>
                            <constraint firstItem="Y45-dN-gv7" firstAttribute="leading" secondItem="9Rq-n0-LVs" secondAttribute="leading" id="6IU-UQ-CvM"/>
                            <constraint firstItem="TjE-LU-Mo5" firstAttribute="top" secondItem="35a-vA-spC" secondAttribute="bottom" constant="8" id="BpX-Pw-rla"/>
                            <constraint firstItem="35a-vA-spC" firstAttribute="leading" secondItem="Q3p-xw-14f" secondAttribute="trailing" constant="8" id="FyN-qU-7qE"/>
                            <constraint firstItem="35a-vA-spC" firstAttribute="top" secondItem="9Rq-n0-LVs" secondAttribute="top" id="G1f-Jl-psr"/>
                            <constraint firstItem="Y45-dN-gv7" firstAttribute="top" secondItem="35a-vA-spC" secondAttribute="bottom" constant="8" id="KOB-Fp-exO"/>
                            <constraint firstItem="35a-vA-spC" firstAttribute="centerX" secondItem="hIj-xH-n33" secondAttribute="centerX" id="W6m-QY-p2X"/>
                            <constraint firstItem="Y45-dN-gv7" firstAttribute="leading" secondItem="TjE-LU-Mo5" secondAttribute="trailing" constant="8" id="XLC-WY-foP"/>
                            <constraint firstItem="9Rq-n0-LVs" firstAttribute="bottom" secondItem="Y45-dN-gv7" secondAttribute="bottom" id="esx-I8-2Kb"/>
                            <constraint firstItem="TjE-LU-Mo5" firstAttribute="centerX" secondItem="hIj-xH-n33" secondAttribute="centerX" id="j57-y5-TqD"/>
                            <constraint firstItem="9Rq-n0-LVs" firstAttribute="trailing" secondItem="Y45-dN-gv7" secondAttribute="trailing" id="lLd-bZ-wF2"/>
                            <constraint firstItem="Q3p-xw-14f" firstAttribute="centerY" secondItem="35a-vA-spC" secondAttribute="centerY" id="m4s-gF-g1G"/>
                            <constraint firstItem="OqS-4d-vWh" firstAttribute="leading" secondItem="35a-vA-spC" secondAttribute="trailing" constant="8" id="quQ-DP-nmi"/>
                            <constraint firstItem="9Rq-n0-LVs" firstAttribute="trailing" secondItem="OqS-4d-vWh" secondAttribute="trailing" constant="20" id="uDD-sD-XqW"/>
                            <constraint firstItem="Q3p-xw-14f" firstAttribute="leading" secondItem="9Rq-n0-LVs" secondAttribute="leading" constant="20" id="wgq-II-OyY"/>
                            <constraint firstItem="OqS-4d-vWh" firstAttribute="centerY" secondItem="35a-vA-spC" secondAttribute="centerY" id="wyP-rt-5dh"/>
                            <constraint firstItem="Y45-dN-gv7" firstAttribute="top" secondItem="TjE-LU-Mo5" secondAttribute="bottom" constant="8" id="xQD-Ne-8vP"/>
                            <constraint firstItem="TjE-LU-Mo5" firstAttribute="leading" secondItem="9Rq-n0-LVs" secondAttribute="leading" constant="16" id="zcb-xN-hZU"/>
                        </constraints>
                        <variation key="default">
                            <mask key="constraints">
                                <exclude reference="j57-y5-TqD"/>
                                <exclude reference="xQD-Ne-8vP"/>
                                <exclude reference="6IU-UQ-CvM"/>
                            </mask>
                        </variation>
                        <variation key="heightClass=regular">
                            <mask key="constraints">
                                <exclude reference="zcb-xN-hZU"/>
                                <include reference="j57-y5-TqD"/>
                                <exclude reference="KOB-Fp-exO"/>
                                <include reference="xQD-Ne-8vP"/>
                                <exclude reference="XLC-WY-foP"/>
                                <include reference="6IU-UQ-CvM"/>
                            </mask>
                        </variation>
                    </view>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="AZb-ld-i79" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="74.400000000000006" y="908.09595202398805"/>
        </scene>
    </scenes>
    <resources>
        <systemColor name="systemBackgroundColor">
            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
    </resources>
</document>
Related