Relative positioning using auto layout and SnapKit - ios

I have a label which I want to have the same relative position on the screen, no matter what device is used. E.g. the label is positioned 10% off from the views top margin and 30% off from the views left margin.
A constant will always do the positioning e.g. 150 px off from the views margin and will therefore be greater for devices with a small resolution, while devices with a bigger resolution will only have a smaller distance...
Is there a way to realize this programmatically e.g. with the help of SnapKit?
My code currently looks like this:
import UIKit
import SnapKit
class worldViewController: UIViewController {
lazy var correctFieldNew = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(correctFieldNew)
correctFieldNew.backgroundColor = UIColor.blueColor()
correctFieldNew.snp_makeConstraints { (make) -> Void in
make.size.equalTo(CGSizeMake(90, 30))
}
}
}
I feel like I have to use the multiplier here, but the label does not move an inch when I write something like:
make.top.equalTo(self.view).multipliedBy(0.1)

The correct way is to use:
make.top.equalTo(self.view.snp_bottom).multipliedBy(0.1)

Related

Autolayout Can I combine CenterY with Top and Bottom Constraints?

I'm trying to make this layout somehow dynamic. The options here are dynamic (unknown count), so we can easily put these options in a tableView, collectionView, or just simply scrollView.
The problem is that I wanna make this white container small if possible and centering vertically. And when I combine centerY constraint with Top+Bottom insets, only the top and bottom constraints seem to be activated.
And when the options are quite long, options can be scrollable, BUT maintaining the fact that there are top and bottom insets.
I have already some ideas in mind, such as observing if the height of the container view exceeds the device height.
I use snapKit, but the constraints should be understandable. Here's my current layout:
func setupUI() {
self.view.backgroundColor = .clear
self.view.addSubviews(
self.view_BGFilter,
self.view_Container
)
self.view_BGFilter.snp.makeConstraints {
$0.edges.equalToSuperview()
}
self.view_Container.snp.makeConstraints {
$0.centerY.equalToSuperview().priority(.high)
//$0.top.bottom.greaterThanOrEqualToSuperview().inset(80.0).priority(.medium)
$0.leading.trailing.equalToSuperview().inset(16.0)
}
// Setup container
self.view_Container.addSubviews(
self.label_Title,
self.stackView,
self.button_Submit
)
self.label_Title.snp.makeConstraints {
$0.top.equalToSuperview().inset(40.0)
$0.leading.trailing.equalToSuperview().inset(16.0)
}
self.stackView.snp.makeConstraints {
$0.top.equalTo(self.label_Title.snp.bottom).offset(29.0)
$0.leading.trailing.equalToSuperview().inset(24.0)
}
self.button_Submit.snp.makeConstraints {
$0.height.equalTo(52.0)
$0.top.equalTo(self.stackView.snp.bottom).offset(30.0)
$0.bottom.leading.trailing.equalToSuperview().inset(24.0)
}
self.generateButtons()
}
My answer your question - CenterY with Top and Bottom constraints? - comes with just a little commentary...
I know that SnapKit is popular, and I'm sure at times it can be very helpful, especially if you use it all the time.
However... when using it you can never be absolutely sure what it's doing. And, in my experience, folks who use SnapKit often don't really understand what constraints are or how they work (not implying that's the case with you ... just an observation from looking at various questions).
In this specific case, either SnapKit has a bit of a bug, or this particular line is not quite right for the desired result:
$0.top.bottom.greaterThanOrEqualToSuperview().inset(80.0)
You can confirm it with a simple test:
class TestViewController: UIViewController {
let testView: UIView = {
let v = UIView()
v.backgroundColor = .red
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(testView)
testView.snp.makeConstraints {
$0.centerY.equalToSuperview()
// height exactly 200 points
$0.height.equalTo(200.0)
// top and bottom at least 80 points from superview
$0.top.bottom.greaterThanOrEqualToSuperview().inset(80.0)
$0.leading.trailing.equalToSuperview().inset(16.0)
}
}
}
This is the result... along with [LayoutConstraints] Unable to simultaneously satisfy constraints. message in debug console:
If we replace that line as follows:
// replace this line
//$0.top.bottom.greaterThanOrEqualToSuperview().inset(80.0)
// with these two lines
$0.top.greaterThanOrEqualToSuperview().offset(80.0)
$0.bottom.lessThanOrEqualToSuperview().offset(-80.0)
which certainly seems to be doing the same thing, we get what we expected:
So, something in SnapKit is fishy.
That will fix your issue. Change your view_Container constraint setup like this:
self.view_Container.snp.makeConstraints {
$0.centerY.equalToSuperview().priority(.required)
// replace this line
//$0.top.bottom.greaterThanOrEqualToSuperview().inset(80.0)
// with these two lines
$0.top.greaterThanOrEqualToSuperview().offset(80.0)
$0.bottom.lessThanOrEqualToSuperview().offset(-80.0)
$0.leading.trailing.equalToSuperview().inset(16.0)
}
Before the change:
After the change:
As to adding scrolling when you have many Option Buttons, I can give you an example for either scrolling all the content or scrolling only the Option Buttons.

iOS interface builder auto constraints problem

I currently trying to learn iOS development, however I’ve got stuck in interface builder. What I am trying to do is actually quite basic view. Unfortunately I feel pretty confused about „Less Than or Equal” relations. I thought that if a constraint is set as less than or equal, it will mean that it will have max size stetted in constant when there will be a plenty of space, otherwise it will be smaller. Turns out that no matter what, it always have the biggest size, which is not what I am trying to achieve.
On iPhone 11 interface looks like this:
On iPhone 8 interface looks like this:
For sure I don’t have all necessary knowledge about auto constraints right now, but maybe someone know where is a problem in this case? Also I would appreciate any good tutorials about interface builder or some good habits.
Thanks
Peter
So, this seems to be the sort of thing you're after. Here it is on a 6s:
And here it is on an iPhone 11:
And here it is, for good measure, on the iPhone 6s rotated:
As you can see, there are four "groups" - the two labels, the label-and-text-field, the second label-and-text-field, and the button. They are evenly distributed from top to bottom on both screens.
That's the right idea, isn't it?
So how is that done? Simple. One vertical stack view filling the screen, with distribution set to Equal Spacing. Inside that, a UIView containing each group (except the button which is on its own), and each UIView given a fixed height by its own height constraint. There's a little more to it but that's the heart of the matter. Once you have that, you can tweak further as desired, of course.
Absolutely no code; the whole thing was configured in a few moments in Xcode's nib editor ("interface builder").
It sounds like you are trying to create spacing between inputs based on the size of the device? I don't know if it will look like you expect, but I've had to do this before in different situations. You can use something like this NSLayoutConstraint subclass to accommodate to accomplish what you want. Essentially, depending on whether the constraint is vertical or horizontal, this class will calculate the screen size at runtime and modify the constraint size based on the percentage value you give it.
/// Layout constraint to calculate size based on multiplier.
class PercentLayoutConstraint: NSLayoutConstraint {
#IBInspectable var marginPercent: CGFloat = 0
var screenSize: (width: CGFloat, height: CGFloat) {
return (UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height)
}
override func awakeFromNib() {
super.awakeFromNib()
guard marginPercent > 0 else { return }
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(layoutDidChange),
name: UIDeviceOrientationDidChangeNotification,
object: nil)
}
/**
Re-calculate constant based on orientation and percentage.
*/
func layoutDidChange() {
guard marginPercent > 0 else { return }
switch firstAttribute {
case .Top, .TopMargin, .Bottom, .BottomMargin:
constant = screenSize.height * marginPercent
case .Leading, .LeadingMargin, .Trailing, .TrailingMargin:
constant = screenSize.width * marginPercent
default: break
}
}
deinit {
guard marginPercent > 0 else { return }
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
source: https://basememara.com/percentage-based-margin-using-autolayout-storyboard/

Add "constant" value in xib in auto layout ios

I have successfully set AutoLayout for iPhone8+,iPhone X,iphone 7 but there is problem for iPhone SE i.e Reduced the space between two labels, so I need to change "constant value" in constraints but I don't want to add outlet add to constant value is there any possibility for add constant value for iPhone SE ?
storyboard solution:
screen width (or super view's width)
⬇︎
base view's leading
⬇︎
target view's spacing between base view.
use left button as the base reference, set right or target button's trailing constraint with left button's leading(or something else horizontal), and set target button's
constraint with multiplier.
change multiplier value based on storyboard's virtual view, that's easy.
set left button's leading based on its' super view's trailing, also with multiplier, so left button's leading based on super view's width, and the right
button based on super view's width too, the spacing between them will grow or shrink with the actual screen size.
case all these iPhone devices are (wC, hR), so, so can not set size classes in storyboard, but, you can still check screen size and make your own logic to layout based on screen sizes in code.
hope this post may help you. :)
Create a class and following code in it.
import UIKit
class DynamicVerticalConstraint: NSLayoutConstraint {
override init() {
super.init()
updateConstant()
}
override func awakeFromNib() {
super.awakeFromNib()
updateConstant()
}
//It will send contraint constant according to devices. For this you have to set your contraint according to iPhone6
func updateConstant(){
self.constant = self.constant * (UIScreen.main.bounds.height / 667)
}
}
class DynamicHorizontalConstraint: NSLayoutConstraint {
override init() {
super.init()
updateConstant()
}
override func awakeFromNib() {
super.awakeFromNib()
updateConstant()
}
//It will send contraint constant according to devices. For this you have to set your contraint according to iPhone6
func updateConstant(){
self.constant = self.constant * (UIScreen.main.bounds.height / 375)
}
}
Now set your constraint's constant value according to iphone 6.
After that you have to assign class to specific contrant.

How to make text size relative to view in IOS?

In the XCode storyboard, I have set up my ViewController as a bunch of stackviews and everything is relative -- view dimensions are expressed as fractions of other view dimensions... lots of constraints, etc.
This is to make sure it looks decent on all IOS devices (phones and Ipads, anyway).
It does look acceptable in different aspect ratios, but I've noticed that the font size of my UILabels and TextViews are NOT changing -- not getting LARGER along with their containing views.
So, for example, if I switch from an iPhone to an iPad preview, a UILabel size may increase drastically and yet the text that it contains stays the same... so it's tiny text in a big box.
SO... the question is:
Is there a way to express font/text sizes as relative to the view that contains the text?
Something like this:
text.height = 0.7 * container.height
text.width = maintain aspect ratio with height
Thanks.
The same problem i had when i was designing an application for both iPhone and iPad and i tried this solution which is working fine but took a little efforts to manage. You need to create a custom label class which will inherits from UILabel class. There in your awakeFromNib function you can check the device and you can multiply whatever the font size with a ratio you feel ok for iPhone and iPad. You can also add checking for different iPhone sizes. If you wish to use different ratios for different label, make a IBDesignable property named dynamicRatio in your custom class and take that value to increase font. You can play around this. The effort is assigning the class to all your labels and setting properties which i use to do parallel during designing.
Below are the set of code which am using.
import UIKit
class MyLabel: UILabel {
#IBInspectable var autoFont: Bool = false
#IBInspectable var fontSize: CGFloat = 0
override open func awakeFromNib() {
super.awakeFromNib()
let baseHeight: CGFloat = (((UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad)) ?1024.0:568.0)
if autoFont == true {
if (isDevice() == DEVICES.iPhoneX) {
let size: CGFloat = 667.0 * (fontSize / baseHeight)
self.font = UIFont(name: self.font!.fontName, size: size)
} else {
let size: CGFloat = UIScreen.main.bounds.size.height * (fontSize / baseHeight)
self.font = UIFont(name: self.font!.fontName, size: size)
}
}
}
}
Hope this idea helps.
Increase text font size of UILabels or UITextFields as you can, then set MinimumFontScale or MinimumFontSize attribute for them in "Attributes Inspector" tab, now font size increases as the UITextFields or UILabels size increases.
UILabel
UITextField

ios autolayout dynamic UILabel height

Say I have three UILabels whose positions are like below:
[Label1] [Label2]
[Label3]
Label1 and Label2 are in the same row and Label3 is below them. All the labels will have a fixed width and will contain dynamic text, so their height will vary.
How do I make the Label3 10 points below the label which has a higher height using AutoLayout?
For example, if Label1's height is 100 points, Label2's height is 120 points (their Y positions are the same), then Label3 should be 10 points below Label2, but if Label1 is 120 points high and Label2 is 100 points high, then Label3 should be 10 points below Label1.
You simply make constraints between both Label3->Label1 and Label3->Label2. Use inequality constraints. There will be only one way to satisfy both!
You will also need a top constraint for Label3; its constant should be very small and its priority should be very low. This will give the two inequality constraints something to "aim at".
Here is an example. This as achieved entirely without code - the buttons have code to add text to the labels, of course, but the constraints are configured entirely in Interface Builder; the labels are resizing, and the bottom label is moving down, automatically. (You can construct the same layout in code if you want to, naturally.)
I suggest you to wrap top two labels to UIView and setup constraints so these labels fit all space inside that view. Then you simple add vertical spacing constraint to bottom label3 with constant = 10. In that case top view will have size of larger label and will satisfy your conditions
I thought this would be an interesting exercise so I create a little test project. The gist of the code is below. You can just copy/paste it in the standard Single View iOS template.
(Note that I use SnapKit for programmatic Auto Layout because it is so much simpler than the UIKit API. I find it even much simpler than doing things in Xcode.)
The result is exactly the same as Matt's great screencast.
// ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController
{
override func viewDidLoad() {
super.viewDidLoad()
let leftLabel = UILabel()
leftLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
leftLabel.userInteractionEnabled = true
view.addSubview(leftLabel)
leftLabel.numberOfLines = 0
leftLabel.text = "All the world's a stage, and all the men and women merely players: they have their exits and their entrances; and one man in his time plays many parts, his acts being seven ages."
leftLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.left.equalTo(self.view)
make.right.equalTo(self.view.snp_centerX)
}
let rightLabel = UILabel()
rightLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "addText:"))
rightLabel.userInteractionEnabled = true
view.addSubview(rightLabel)
rightLabel.numberOfLines = 0
rightLabel.text = "There is a tide in the affairs of men, Which taken at the flood, leads on to fortune. Omitted, all the voyage of their life is bound in shallows and in miseries. On such a full sea are we now afloat. And we must take the current when it serves, or lose our ventures."
rightLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(40)
make.right.equalTo(self.view)
make.left.equalTo(self.view.snp_centerX)
}
let bottomView = UIView()
view.addSubview(bottomView)
bottomView.backgroundColor = UIColor.redColor()
bottomView.snp_makeConstraints { (make) -> Void in
make.height.equalTo(20)
make.left.right.equalTo(self.view)
make.top.greaterThanOrEqualTo(leftLabel.snp_bottom)
make.top.greaterThanOrEqualTo(rightLabel.snp_bottom)
}
}
#objc func addText(recognizer: UIGestureRecognizer) {
if let label = recognizer.view as? UILabel {
label.text = label.text! + " I like cheese."
}
}
}
Updated the code to add some additional text to the labels when tapped.
First of all remove height constraints and set all 3 labels vertical Content Compression Resistance Priority to 1000. This is the most important part.
Then add vertical space from Label3 to Label 1, and set instead of Equal, Greater Than or Equal with priority say 500. Add same space constraint to Label2.
Last add constraint from Label3 to Top = 0, but set priority to 1.

Resources