Refer to GK100's answer but let's extend that with below Q/A(s).
How to make constant auto-resize in Storyboard?
Percent based constant is not supported yet, and people try different workarounds, like:
- Creating invisible
UIView
(s) as spacers with width/height and specifying a multiplier against the Safe Area
size.
- People even manually override Storyboard constraints in code:
NSLayoutConstraint(item: myView, attribute: .top,
relatedBy: .equal, toItem: myOtherView, attribute: .bottom,
multiplier: 1,
constant: UIScreen.main.bounds.width * 0.05)
While both of above are a headache, my solution's downside is that you need to hardcode at least shortDimensionInDesignTime
to your preferred Interface Builder
's preview-size (but far better than my PercentConstraint
class).
Usage:
- Add below
ResponsiveConstraint
class's code to your project.
- Select your constraint in
Storyboard
editor.
- In
Identity Inspector
, use ResponsiveConstraint
as class.
import Foundation
/// Auto-Resize based Layout-Constraint.
/// Later at run-time, resizes initial constant to screen's short dimension.
///
/// For Mac's Catalyst. remember to set size-restrictions to something like:
/// ```
/// let portrait: CGSize = ResponsiveConstraint.maxSize;
/// scene.sizeRestrictions?.maximumSize = portrait;
/// scene.sizeRestrictions?.minimumSize = portrait;
/// ```
///
/// WARNING: supports only Storyboard (by use as class in Identity Inspector),
/// but there is no harm in using static provided field and methods.
///
public class ResponsiveConstraint: NSLayoutConstraint {
var actualConstant: CGFloat = 0;
public var isValid: Bool { actualConstant > 0 }
/// Preset shorter dimension of screen, within Design-Time only
/// (i.e. it's your Interface-Builder's preview size).
public static let shortDimensionInDesignTime: CGFloat = 414;
public static let longDimensionInDesignTime: CGFloat = 896;
/// Normally same as "UIScreen.main.bounds.size" variable (i.e. screen size).
/// But for Computers, we limit to a portrait-size (to look same as Mobile).
public static var maxSize: CGSize {
#if targetEnvironment(macCatalyst)
return .init(
width: Self.shortDimensionInDesignTime,
height: Self.longDimensionInDesignTime);
#else
return UIScreen.main.bounds.size;
#endif
}
public static var shortDimension: CGFloat {
let screenSize: CGSize = Self.maxSize;
return screenSize.width < screenSize.height
? screenSize.width : screenSize.height;
}
public static var longDimension: CGFloat {
let screenSize: CGSize = Self.maxSize;
return screenSize.height > screenSize.width
? screenSize.height : screenSize.width;
}
/// Does same as "init()" would, but for Storyboard.
override public func awakeFromNib() {
super.awakeFromNib()
// Backup real constant (and continue if it's valid).
self.actualConstant = self.constant
guard isValid else { return }
// Listens to screen rotate.
NotificationCenter.default.addObserver(self,
selector: #selector(onLayoutChange),
name: UIDevice.orientationDidChangeNotification,
object: nil)
// Apply for current screen size.
self.onLayoutChange();
}
deinit {
guard isValid else { return }
NotificationCenter.default.removeObserver(self)
}
/// Recalculates constant.
@objc public func onLayoutChange() {
guard isValid else { return }
self.constant = Self.resize(self.actualConstant)
}
/// Converts constant from DesignTime to fit current max view-size.
public static func resize(_ constant: CGFloat) -> CGFloat {
return constant / Self.shortDimensionInDesignTime * Self.shortDimension;
}
}
How to achieve percent based constant in Storyboard?
Below solution's downside is need to run App before seeing result (which even that may not be required in future Xcode versions).
Usage:
- Add below
PercentConstraint
class's code to your project.
- Select your constraint in
Storyboard
editor.
- In
Identity Inspector
, use PercentConstraint
as class.
- In
Attribute Inspector
, set Percent of Screen
field.
- Optionally, Run App to see result.
import Foundation
/// Percent based Layout-Constraint.
/// Later at run-time, replaces constant with percentage of screen.
///
/// WARNING: supports only Storyboard (used as class in Identity Inspector).
///
class PercentConstraint: NSLayoutConstraint {
@IBInspectable public var percentOfScreen: CGFloat = 0
var isValid: Bool { percentOfScreen > 0 }
var screenSize: CGSize { UIScreen.main.bounds.size }
override func awakeFromNib() {
super.awakeFromNib()
guard isValid else { return }
NotificationCenter.default.addObserver(self,
selector: #selector(onLayoutChange),
name: UIDevice.orientationDidChangeNotification,
object: nil)
self.onLayoutChange();
}
deinit {
guard isValid else { return }
NotificationCenter.default.removeObserver(self)
}
/// Recalculates constant.
@objc func onLayoutChange() {
guard isValid else { return }
switch self.firstAttribute {
case .height, .top, .topMargin, .bottom, .bottomMargin,
.centerY, .centerYWithinMargins,
.firstBaseline, .lastBaseline:
self.constant = screenSize.height * percentOfScreen / 100.0;
case .width, .leading, .leadingMargin, .trailing, .trailingMargin,
.left, .leftMargin, .right, .rightMargin,
.centerX, .centerXWithinMargins:
self.constant = screenSize.width * percentOfScreen / 100.0;
default:
break;
}
}
}
Note that the constant
is overwritten at runtime,
but you may want to set it anyway (to get an idea how it looks like before running).
addSubview:
method of the root view. – Dehydrogenate