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>
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
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: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 useUILayoutPriority(999)
(or.required - 1
) instead of.required
in code.Part of the problem here is that your code is wrong. If
redViewTopConstraint
is not active andredViewBottomConstraint
is active, and you calltoggleConstraints
, 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: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 yourviewDidLoad
:That way, you start life in a known state.