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/
Related
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.
Swift 4 + Xcode 9.
I have been working on this problem for weeks, trying to solve it myself. I would appreciate any help that anyone could provide. I'm not posting any code in my initial question because it's proprietary. I will be happy to provide pieces of it if it's required to help solve my problem.
I have a UITableView with a custom cell, which contains a very complex layout of subviews, some of which are hidden or shown (using 1000-priority height=0 constraints which are added and removed during cellForRowAt) depending on the data. There is also an ImageView which should always be full width, and should change height to match the image, which is loaded via Kingfisher.shared.retrieveImage(). Once I have the image, I update the aspectRatio constraint on the image for that cell, and the cells display. This works perfectly for the first 15-20 cells, but as I scroll through more rows, it simply stops functioning. The images are small and centered, certain data fields are not updated, etc. If I keep scrolling, sometimes a cell will behave correctly here and there, but nearly all do not.
Now for the interesting part: If I scroll BACK UP, every single cell reformats itself automatically to look as it should, and after that, every cell is perfect. The code obviously works - and I feel like this may be a bug in the platform, but before I assume that, I wanted to see if anyone else had run into something like this.
Again, thank you very much for any help you can provide - I'm very anxious to solve this.
UPDATE: To answer a couple of the questions, here is a snippet of code that is part of the custom cell class. This is how I set the cell's image (which includes updating the aspect ratio constraint), and how I reset the cell for re-use.
internal var aspectConstraint : NSLayoutConstraint? {
didSet {
if oldValue != nil {
imageView.removeConstraint(oldValue!)
}
if aspectConstraint != nil {
imageView.addConstraint(aspectConstraint!)
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
aspectConstraint = nil
imageView.image = nil
for view in subviews {
for c in view.constraints {
if c.firstAttribute == NSLayoutAttribute.height && c.constant == 0 {
view.removeConstraint(c)
}
}
}
}
func setCustomImage(image : UIImage) {
let aspect = image.size.width / image.size.height
let constraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: imageView, attribute: NSLayoutAttribute.height, multiplier: aspect, constant: 0.0)
constraint.priority = UILayoutPriority(999)
aspectConstraint = constraint
imageView.image = image
}
For anyone who comes to this question, due to complete lack of resolution on this issue, I have concluded that UIKit is unsuitable for my purposes. I went with Texture (formerly AsyncDisplayKit), and my progress has resumed.
I'm learning swift with cs193p and I have a problem with UITextView.sizeThatFits(...). It should return a recommended size for popover view to display an [int] array as a text. As you can see in Paul Hegarty's example (https://youtu.be/gjl2gc70YHM?t=1h43m17s), he gets perfectly-fit popover window without scrollbar. I'm using almost the same code that was in this lecture, but instead i've got this:
the text string equals [100], but the sizeThatFits() method is returning a size that is too small to display it nicely, even though there is plenty of free space.
It is getting a bit better after I've added some text, but still not precise and with the scrollbar:
Here is the part of the code where the size is being set:
override var preferredContentSize: CGSize {
get {
if textView != nil && presentingViewController != nil {
// I've added these outputs so I can see the exact numbers to try to understand how this works
print("presentingViewController!.view.bounds.size = \(presentingViewController!.view.bounds.size)")
print("sizeThatFits = \(textView.sizeThatFits(presentingViewController!.view.bounds.size))")
return textView.sizeThatFits(presentingViewController!.view.bounds.size)
} else { return super.preferredContentSize }
}
set { super.preferredContentSize = newValue }
}
What should I do so this will work in the same way as in the lecture?
It looks like there are 16 pt margins between the label and its parent view. You need to take that into account when returning the preferred size of the popover.
You should try both of the following:
Add 32 to the width that's returned from preferredContentSize
In Interface Builder, clear the layout constraints on your UILabel, then re-add top, bottom, leading, and trailing constraints and make sure that "Constrain to Margins" option is not enabled.
Finally, instead of overriding preferredContentSize, you can simply set the preferredContentSize when your view is ready to display, and you can ask Auto Layout to choose the best size:
override func viewDidLayoutSubviews() {
self.preferredContentSize = self.view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
}
If your layout is configured correctly, systemLayoutSizeFitting(UILayoutFittingCompressedSize) will return the smallest possible size for your view, taking into account all of the margins and sub-views.
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)
Before the switch to auto layout, applying animations to a view was easy - you'd just change the frame in UIView.layoutWithDuration:. When constraints come into the picture, things get more complicated. The most common method of animating a view that uses auto layout is to keep a reference to the constraint(s) you want to change, then set the constant value, but this is very difficult to design around, and can affect other views if their constraints depend on the position of the view you want to animate.
There must be a better way to do this. I'd love to be able to do something like view.translateFrom(direction: .left, distance: 30, duration: 0.3, delay: 0).
Ultimately you will want to use constraints to achieve simple animations. The overriding reason for this is -
"Choosing any other way is simply swimming against the stream"
If you don't want to 'litter' your classes with #IBOutlets for the constraints you wish to animate then you can in most cases obtain a reference to a pertinent constraint in code:
Handy extension
import UIKit
extension UIView {
func constraint(attribute: NSLayoutAttribute) -> NSLayoutConstraint? {
var constraint : NSLayoutConstraint? = .none
for potentialCenterXConstraint in self.constraints {
if potentialCenterXConstraint.firstAttribute == attribute {
constraint = potentialCenterXConstraint
break
}
}
return constraint
}
}
client code use
if let centerXConstraint = someView.constraint(attribute: .centerX) {
// Do something funky with centerXConstraint
}