Reputation: 1421
Imagine I have a UIImageView
with a certain aspect ratio constraint (this handles the height/bottom constraint), pinned to superview
's top, leading, trailing with 5pt in a 100pt width screen in XIB/storyboard. This way, if the screen size changes, the image view can scale accordingly.
That is 5pt+5pt (10pt) of margin spacing in a 100pt width screen, with image taking up 90pt (90% of the screen).
However I would like the margin spacing to be scaled as well.
How can I make it such that, on a bigger phone, e.g. with width 1000pt (10x previous), the margin will now correspondingly be 50pt+50pt (100pt) with the image taking up the remaining 900pt of the screen, retaining 90%? (This could be further complicated if we are making the horizontal constraints different, e..g. leading be 15pt while the trailing be 45pt.)
If we do it 'normally' without scaling the margin pt, the constraints of leading/trailing would be 5pt (for a total of 10pt in a 1000pt or even a 1000000pt width phone!). This is not what we usually want.
How can we achieve this easily with auto layout? I thought of class sizes but that does not fit what I am trying to do (to cater to all devices of any size).
Upvotes: 1
Views: 1533
Reputation: 27620
ASFAIK there is no way to define leading or trailing constraints relatively to another view's width.
To achieve what you want you can set the following constraints on the UIImageView
:
When you do this, the horizontal margins of the image will always be 10% of the screen size.
EDIT:
When you also need a relative top margin you could put the UIImageView into another UIView (the one with the gray background) and use the following constraints:
If you want to have a top margin that is the same as the horizontal margins, you have to calculate it like this: verticalMultiplier = horizontalMultiplier / (2 * aspectRatio)
Upvotes: 2
Reputation: 77423
Whether laying this out in Storyboard / IB or doing it via code, probably the easiest way is to use a horizontal stack view, with a (clear) "spacer" view on each side.
What you have to decide in advance, though, is the desired proportion.
That is, we say the leading space should be 5-pts
based on a total width of 100-pts
... or 25-pts
based on 150-pts
... etc.
So our stack view will have 3 arranged subviews:
640:360
ratio)Constrain the Width of the LeftSpacer to the width of the stack view, with a multiplier of (left space)/100
.
Constrain the Width of the RightSpacer to the width of the stack view, with a multiplier of (right space)/100
.
Here are two examples.
The Top stack view has both left and right spacers set to 5/100
. The Bottom stack view has Left: 5/100
and Right: 45/100
. In both stack views, the image view is set to 640:360
aspect ratio (because that's the image I had handy).
This is how it looks with stack view Width: 100
:
Top example - both spacer views are 5-pts, image view is 90-pts
Bottom example - left spacer: 5-pts, right: 45-pts, image view: 50-pts
and stack view Width: 200
:
Top example - both spacer views are 10-pts, image view is 180-pts
Bottom example - left spacer: 10-pts, right: 90-pts, image view: 100-pts
and stack view Width: 300
:
Top example - both spacer views are 15-pts, image view is 270-pts
Bottom example - left spacer: 15-pts, right: 135-pts, image view: 150-pts
This is the Document Outline:
and the Storyboard source if you want to inspect it:
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<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>
<!--SomeVC-->
<scene sceneID="YpP-O8-TWi">
<objects>
<viewController id="P90-ov-dhs" customClass="SomeVC" customModule="TabBased" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Afn-4E-CtW">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="28" translatesAutoresizingMaskIntoConstraints="NO" id="EZB-9U-TcY">
<rect key="frame" x="10" y="20" width="300" height="264.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wTh-HR-1cy">
<rect key="frame" x="0.0" y="0.0" width="300" height="152"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WVJ-wZ-t9Z" userLabel="LeftSpacerView">
<rect key="frame" x="0.0" y="0.0" width="15" height="152"/>
<color key="backgroundColor" systemColor="systemYellowColor"/>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bkg640x360" translatesAutoresizingMaskIntoConstraints="NO" id="39U-sA-bbn">
<rect key="frame" x="15" y="0.0" width="270" height="152"/>
<constraints>
<constraint firstAttribute="width" secondItem="39U-sA-bbn" secondAttribute="height" multiplier="640:360" id="soT-yc-UP0"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Tww-ar-yol" userLabel="RightSpacerView">
<rect key="frame" x="285" y="0.0" width="15" height="152"/>
<color key="backgroundColor" systemColor="systemYellowColor"/>
</view>
</subviews>
<constraints>
<constraint firstItem="Tww-ar-yol" firstAttribute="width" secondItem="wTh-HR-1cy" secondAttribute="width" multiplier="5/100" id="DLh-N4-vfJ"/>
<constraint firstItem="WVJ-wZ-t9Z" firstAttribute="width" secondItem="wTh-HR-1cy" secondAttribute="width" multiplier="5/100" id="qYi-Rk-ZOI"/>
</constraints>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TsK-Kw-YRa">
<rect key="frame" x="0.0" y="180" width="300" height="84.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iw4-he-3pJ" userLabel="LeftSpacerView">
<rect key="frame" x="0.0" y="0.0" width="15" height="84.5"/>
<color key="backgroundColor" systemColor="systemYellowColor"/>
</view>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bkg640x360" translatesAutoresizingMaskIntoConstraints="NO" id="P4f-Bc-9F2">
<rect key="frame" x="15" y="0.0" width="150" height="84.5"/>
<constraints>
<constraint firstAttribute="width" secondItem="P4f-Bc-9F2" secondAttribute="height" multiplier="640:360" id="ZqY-ss-ToB"/>
</constraints>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Fs5-7r-dME" userLabel="RightSpacerView">
<rect key="frame" x="165" y="0.0" width="135" height="84.5"/>
<color key="backgroundColor" systemColor="systemYellowColor"/>
</view>
</subviews>
<constraints>
<constraint firstItem="iw4-he-3pJ" firstAttribute="width" secondItem="TsK-Kw-YRa" secondAttribute="width" multiplier="5/100" id="D5C-Yo-YOh"/>
<constraint firstItem="Fs5-7r-dME" firstAttribute="width" secondItem="TsK-Kw-YRa" secondAttribute="width" multiplier="45/100" id="ajJ-GY-bZW"/>
</constraints>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="width" constant="300" id="Hel-XG-PhB"/>
</constraints>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="Ur7-hW-kek"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="EZB-9U-TcY" firstAttribute="centerX" secondItem="Afn-4E-CtW" secondAttribute="centerX" id="5lZ-RK-pgy"/>
<constraint firstItem="EZB-9U-TcY" firstAttribute="top" secondItem="Ur7-hW-kek" secondAttribute="top" constant="20" id="bg6-Jx-gi9"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="ZhN-1l-tl9"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Rgh-e1-NRf" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-118.125" y="1365"/>
</scene>
</scenes>
<resources>
<image name="bkg640x360" width="640" height="360"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemYellowColor">
<color red="1" green="0.80000000000000004" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
Obviously, that would be straightforward to replicate in code.
If, for some reason, you don't want to use stack views, you could do essentially the same thing with leading/trailing and proportional width constraints on two "side spacer views" or, to make it just a little more light-weight, use ConstraintLayoutGuide
instead of a clear view.
Edit - for a little more clarification...
First, I should have specified that by ConstraintLayoutGuide I was referring to UILayoutGuide
- whoops :(
The task you've outlined:
If my view is 100-pts wide, I want the subview to have 5-pts "padding" on the left and right. But, as the view gets wider the padding should expand proportionally. So, if the view is 200-pts wide, the padding should be 10-pts on each side.
Could also be described as:
I want 5% "padding" on each side.
Auto-layout does not have a way to set the Leading anchor to a percentage of the superview's width.
So, traditionally, we used clear-background "spacer" views:
With iOS 9, Apple introduced UILayoutGuide
(docs):
Overview
Use layout guides to replace the placeholder views you may have created to represent inter-view spaces or encapsulation in your user interface.
These cannot be added in Storyboard / IB, but can be used in code in much the same way as a standard UIView
.
Here's a simple example that adds a "cyan" view and a UILayoutGuide
to a "container" view, providing 15% of "leading space" to the cyan view:
class LayoutGuideExampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let safeG = view.safeAreaLayoutGuide
// create a "container" view
let cView = UIView()
cView.translatesAutoresizingMaskIntoConstraints = false
cView.layer.borderWidth = 1
cView.layer.borderColor = UIColor.red.cgColor
view.addSubview(cView)
// constraints for the container view
NSLayoutConstraint.activate([
cView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 20.0),
cView.heightAnchor.constraint(equalToConstant: 40.0),
cView.widthAnchor.constraint(equalToConstant: 100.0),
cView.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
])
// create a "cyan" view
let cyanView = UIView()
cyanView.translatesAutoresizingMaskIntoConstraints = false
cyanView.backgroundColor = .cyan
// add it to the container
cView.addSubview(cyanView)
// constrain the cyan view Top / Bottom / Trailing to the container
NSLayoutConstraint.activate([
cyanView.topAnchor.constraint(equalTo: cView.topAnchor),
cyanView.bottomAnchor.constraint(equalTo: cView.bottomAnchor),
cyanView.trailingAnchor.constraint(equalTo: cView.trailingAnchor),
])
// we want the cyan view's leading-edge to be 15% from the
// leading-edge of the container
// but, we can't do that with a leadingAnchor
// so, let's add a UILayoutGuide
// create the guide
let leftGuide = UILayoutGuide()
// add it to the container view
cView.addLayoutGuide(leftGuide)
// now, we'll constrain the guide
NSLayoutConstraint.activate([
// Top / Bottom / Leading to the container
leftGuide.topAnchor.constraint(equalTo: cView.topAnchor),
leftGuide.bottomAnchor.constraint(equalTo: cView.bottomAnchor),
leftGuide.leadingAnchor.constraint(equalTo: cView.leadingAnchor),
// Width as 15% of the container Width
leftGuide.widthAnchor.constraint(equalTo: cView.widthAnchor, multiplier: 15.0 / 100.0),
// and cyan view Leading to the guide's Trailing
cyanView.leadingAnchor.constraint(equalTo: leftGuide.trailingAnchor),
])
}
}
The result - 15-pts (15%) for a 100-pt "container" view (red outline):
and, if we change the container width to 200-pts, we get 30-pts (15%) of leading space:
Upvotes: 1