skip to Main Content

I am trying to switch constraints in my layout. In my project I use UIKit with storyboards. For some reason, in iOS 18 simulator, form sheet presentation resets constraints which were setup in my storyboard.
I have two constraints where one of them is not installed in the storyboard. When I swap isActive state in both constraints and open a sheet, the flags switch back to their original states.

import UIKit

class ViewController: UIViewController {
    @IBOutlet var redViewTopConstraint: NSLayoutConstraint?
    @IBOutlet var redViewBottomConstraint: NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func openFormSheet() {
        let viewController = UIViewController()
        viewController.view.backgroundColor = .blue
        viewController.modalPresentationStyle = .formSheet
        present(viewController, animated: true)
    }

    @IBAction func toggleConstraints() {
        redViewTopConstraint?.isActive.toggle()
        redViewBottomConstraint?.isActive.toggle()
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23086.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
    <device id="retina6_12" orientation="portrait" appearance="light"/>
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23076"/>
        <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="tne-QT-ifu">
            <objects>
                <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Test_ios_18_bug" customModuleProvider="target" sceneMemberID="viewController">
                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                        <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                        <subviews>
                            <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="GhQ-n3-4eC">
                                <rect key="frame" x="91" y="423" width="211" height="31"/>
                                <subviews>
                                    <button opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lx5-r9-cxs">
                                        <rect key="frame" x="0.0" y="0.0" width="152" height="31"/>
                                        <state key="normal" title="Button"/>
                                        <buttonConfiguration key="configuration" style="plain" title="Open form sheet"/>
                                        <connections>
                                            <action selector="openFormSheet" destination="BYZ-38-t0r" eventType="touchUpInside" id="xne-NT-13P"/>
                                        </connections>
                                    </button>
                                    <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WB7-FG-wMT">
                                        <rect key="frame" x="162" y="0.0" width="51" height="31"/>
                                        <connections>
                                            <action selector="toggleConstraints" destination="BYZ-38-t0r" eventType="valueChanged" id="X2V-rJ-gEt"/>
                                        </connections>
                                    </switch>
                                </subviews>
                            </stackView>
                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Vfi-o4-jgR">
                                <rect key="frame" x="146.66666666666666" y="59" width="100" height="100"/>
                                <color key="backgroundColor" systemColor="systemRedColor"/>
                                <constraints>
                                    <constraint firstAttribute="width" constant="100" id="k3Q-RA-T6r"/>
                                    <constraint firstAttribute="height" constant="100" id="mvY-ej-r9M"/>
                                </constraints>
                            </view>
                        </subviews>
                        <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                        <constraints>
                            <constraint firstItem="Vfi-o4-jgR" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="3IO-WP-fQ2"/>
                            <constraint firstItem="Vfi-o4-jgR" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" id="G1w-zW-l6a"/>
                            <constraint firstItem="GhQ-n3-4eC" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="RUe-tf-C1w"/>
                            <constraint firstItem="GhQ-n3-4eC" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="UDd-dC-W8D"/>
                            <constraint firstItem="6Tk-OE-BBY" firstAttribute="bottom" secondItem="Vfi-o4-jgR" secondAttribute="bottom" id="w7S-f8-ghA"/>
                        </constraints>
                        <variation key="default">
                            <mask key="constraints">
                                <exclude reference="w7S-f8-ghA"/>
                            </mask>
                        </variation>
                    </view>
                    <connections>
                        <outlet property="redViewBottomConstraint" destination="w7S-f8-ghA" id="yug-ID-c0v"/>
                        <outlet property="redViewTopConstraint" destination="G1w-zW-l6a" id="uHZ-MY-ldM"/>
                    </connections>
                </viewController>
                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
            </objects>
            <point key="canvasLocation" x="697" y="77"/>
        </scene>
    </scenes>
    <resources>
        <systemColor name="systemBackgroundColor">
            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
        </systemColor>
        <systemColor name="systemRedColor">
            <color red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
        </systemColor>
    </resources>
</document>
Initial state Swapped state State after presenting a sheet
Initial state Swapped state State after presenting a sheet

If I use the constraints in code, I get the desired behavior:

import UIKit

class ViewController: UIViewController {
    var redViewTopConstraint: NSLayoutConstraint?
    var redViewBottomConstraint: NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .orange

        let button = UIButton()
        button.setTitle("Open form sheet", for: .normal)
        button.addTarget(self, action: #selector(openFormSheet), for: .touchUpInside)

        let redView = UIView()
        redView.backgroundColor = .red
        redView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(redView)

        let redViewTopConstraint = redView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
        self.redViewTopConstraint = redViewTopConstraint

        redViewBottomConstraint = redView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)

        let toggle = UISwitch()
        toggle.addTarget(self, action: #selector(toggleConstraints), for: .valueChanged)

        let stackView = UIStackView(arrangedSubviews: [button, toggle])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.spacing = 10
        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            redView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            redView.widthAnchor.constraint(equalToConstant: 100),
            redView.heightAnchor.constraint(equalToConstant: 100),
            redViewTopConstraint
        ])
    }

    @objc func openFormSheet() {
        let viewController = UIViewController()
        viewController.view.backgroundColor = .blue
        viewController.modalPresentationStyle = .formSheet
        present(viewController, animated: true)
    }

    @objc func toggleConstraints() {
        redViewTopConstraint?.isActive.toggle()
        redViewBottomConstraint?.isActive.toggle()
    }
}

Is it a bug or documented behavior? Is there a parameter which prevent this to happen? Should I fix this or Apple will fix it when they release iOS 18?

I use Xcode 16 beta 3 and iOS 18.0 simulator.

2

Answers


  1. Trying to use constraints that have Installed un-checked in Storyboard has (from my experience) always been a hit-or-miss proposition.

    I don’t have Xcode 16 beta 3 and iOS 18.0 simulator installed, but my assumption would be that something in the controller life-cycle has changed enough that the Storyboard settings are being re-applied.

    Another option would be to keep both constraints installed, but manipulate their priorities instead.

    Start with the bottom constraint set to Priority: Low (250) and change the toggle to this:

    @IBAction func toggleConstraints() {
        //redViewTopConstraint?.isActive.toggle()
        //redViewBottomConstraint?.isActive.toggle()
    
        redViewTopConstraint?.priority = (redViewTopConstraint?.priority == .required) ? .defaultLow : .required
        redViewBottomConstraint?.priority = (redViewTopConstraint?.priority == .required) ? .defaultLow : .required
    }
    

    You’ll get warnings in the debug console because during that process there will be constraint conflicts. You can safely ignore those warnings.

    To get rid of them, set the top constraint priority to 999 in Storyboard, and then use UILayoutPriority(999) (or .required - 1) instead of .required in code.

    Login or Signup to reply.
  2. Part of the problem here is that your code is wrong. If redViewTopConstraint is not active and redViewBottomConstraint is active, and you call toggleConstraints, you’re going to get an autolayout conflict, because for a moment both of them will be active at the same time. The code should read:

        if redViewTopConstraint?.isActive ?? false {
            redViewTopConstraint?.isActive.toggle()
            redViewBottomConstraint?.isActive.toggle()
        } else {
            redViewBottomConstraint?.isActive.toggle()
            redViewTopConstraint?.isActive.toggle()
        }
    

    After that change, I couldn’t reproduce any issue using Xcode 15. If you test with Xcode 15 and you don’t get the issue but you do get it with Xcode 16 beta, please file a bug immediately, as this is just the sort of thing Apple wants to know about during this beta period.

    Having said all that, I would also say that using the "Installed" checkbox to mean isActive seems to me to be a bit dicey. To be on the safe side, I would suggest adding these lines to your viewDidLoad:

        redViewBottomConstraint?.isActive = false
        redViewTopConstraint?.isActive = true
    

    That way, you start life in a known state.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search