UIScrollView how do you constrain a sub view that acts a container to all the other views? - ios

so as you will see below I have a scrollview and I want to add it the the UIViewControllers root view. When I have it constrained to the top, right, bottom, and left I expect to see the red color take up the whole screen. This obviously works, but I want to add a subview to the scrollview that will wrap all the child views. How would I go about doing that?
I have added the view and I have set the same constraints except this time they are set from the wrapper view to the bounds of the UIScrollView, and the blue background color doesn't show anywhere. Also feel free to point out if this is a bad idea, but I thought I could just have it be constrained to the bottom and it will automatically extend the scrollviews content size as needed. This seems to work when I had all the subviews in the scrollview without a wrapper and the last view would extend the content size.
scrollView = UIScrollView(frame: view.bounds)
scrollView?.showsVerticalScrollIndicator = true
scrollView?.backgroundColor = .red
scrollView?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView!)
scrollView?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView?.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView?.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
//setup wrapper view
let subviewWrapper = UIView()
subviewWrapper.translatesAutoresizingMaskIntoConstraints = false
scrollView?.addSubview(subviewWrapper)
subviewWrapper.backgroundColor = .blue
subviewWrapper.topAnchor.constraint(equalTo: (scrollView?.topAnchor)!).isActive = true
subviewWrapper.leftAnchor.constraint(equalTo: (scrollView?.leftAnchor)!).isActive = true
subviewWrapper.rightAnchor.constraint(equalTo: (scrollView?.rightAnchor)!).isActive = true
subviewWrapper.bottomAnchor.constraint(equalTo: (scrollView?.bottomAnchor)!).isActive = true

Actually this is a very good idea. I always set up my scrollViews this way. I usually call the view contentView, but it is the same idea.
You're almost there. You haven't yet given Auto Layout anything to go on to figure out the size of your subviewWrapper. The constraints you've set so far pin the subviewWrapper to the edges of the scrollView's content area, but this just establishes the fact that as the subviewWrapper grows, the content size of the scrollView will expand. Currently your subviewWrapper has 0 width and 0 height which is why you see no blue.
Below are 3 examples of how you might establish the size of your subviewWrapper.
Note: Each of the following examples is completely independent. Look at each one separately and as you try them, remember to delete the constraints added by the previous example.
Example 1: Make subviewWrapper 1000 x 1000:
Set constraints to make your subviewWrapper 1000 x 1000 and you will see the blue and it will scroll in both directions.
subviewWrapper.widthAnchor.constraint(equalToConstant: 1000).isActive = true
subviewWrapper.heightAnchor.constraint(equalToConstant: 1000).isActive = true
Example 2: Vertical only scrolling with content size 2X of scrollView height:
If you set the width of your subviewWrapper to be equal to the width of the scrollView then it will only scroll vertically. If you set the height of subviewWrapper to 2X the height of scrollView, then your blue area will be twice the height of the scrollView.
subviewWrapper.widthAnchor.constraint(equalTo: scrollView!.widthAnchor, multiplier: 1.0).isActive = true
subviewWrapper.heightAnchor.constraint(equalTo: scrollView!.heightAnchor, multiplier: 2.0).isActive = true
Example 3: Size of subviewWrapper set by its subviews:
You can also establish the size of your subviewWrapper by adding subviews to it that are fully specified in size and connected in a chain from the top of subviewWrapper to the bottom, and from side to side. If you do this, Auto Layout will have enough information to compute the size of your subviewWrapper
In this example, I've added a yellow 600 x 600 square to the subviewWrapper and set it 100 points from each edge. Without having explicitly set a size for subviewWrapper, Auto Layout can figure out that it is 800 x 800.
let yellowSquare = UIView()
yellowSquare.translatesAutoresizingMaskIntoConstraints = false
yellowSquare.backgroundColor = .yellow
subviewWrapper.addSubview(yellowSquare)
yellowSquare.widthAnchor.constraint(equalToConstant: 600).isActive = true
yellowSquare.heightAnchor.constraint(equalToConstant: 600).isActive = true
yellowSquare.topAnchor.constraint(equalTo: subviewWrapper.topAnchor, constant: 100).isActive = true
yellowSquare.leadingAnchor.constraint(equalTo: subviewWrapper.leadingAnchor, constant: 100).isActive = true
yellowSquare.trailingAnchor.constraint(equalTo: subviewWrapper.trailingAnchor, constant: -100).isActive = true
yellowSquare.bottomAnchor.constraint(equalTo: subviewWrapper.bottomAnchor, constant: -100).isActive = true

Related

UIStackView subviews don't recalculate height when something changes

In my app, I have one UIStackView that contains two views. I want that, when one of those views disappears or grows in height, the other one adapts by occupying all the remaining space. But what happens is that is height and position remains unchanged even if I remove the other view from the UIStackView.
My views are declared this way:
fileprivate func initLayout() {
subView1 = UIStackView()
subView1.axis = .vertical
// same for subView2
container = UIStackView()
view.addSubview(container!)
container?.translatesAutoresizingMaskIntoConstraints = false
subView1?.translatesAutoresizingMaskIntoConstraints = false
subView2?.translatesAutoresizingMaskIntoConstraints = false
container?.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
container?.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
container?.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
container?.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
container?.addArrangedSubview(subView1)
container?.addArrangedSubview(subView2)
}
Initially, their height is correct no matter what their content is (subView1 occupies all the space that subView2 leaves). But then if I call
container?.removeArrangedSubview(subView2)
subView2 disappears, but subView1 keeps the same height and position, leaving a blank space below it. How can I make it adapt when subView2 changes?
It also happens when subView2 grows in size. Sometimes I need it to add more options. So what I was doing is adding the new views to subView2 and then calling
container?.removeArrangedSubview(subView1)
container?.removeArrangedSubview(subView2)
container?.addArrangedSubview(subView1)
container?.addArrangedSubview(subView2)
It doesn't work neither. subView1 stays the same position and height, so they overlap.

Profile Image cuts off in the UITableViewCell

I have a simple custom UITableViewCell which has profile image on the left, title and detailsLabel on the right. I have used Auto Layout constraints to set all the views on the screen. But the detailsLable text is short and profile image cuts off.
Let me know how to fix it. I could make the image small which is shorter than the height of title and label combined, but I want the big image.
// adding subviews
contentView.addSubview(profileImageView)
contentView.addSubview(nameLabel)
contentView.addSubview(jobTitleDetailedLabel)
// constraint for the views
profileImageView.topAnchor.constraint(equalTo:self.contentView.topAnchor, constant:10).isActive = true
profileImageView.leadingAnchor.constraint(equalTo:self.contentView.leadin gAnchor, constant:10).isActive = true
profileImageView.widthAnchor.constraint(equalToConstant:50).isActive = true
profileImageView.heightAnchor.constraint(equalToConstant:50).isActive = true
nameLabel.topAnchor.constraint(equalTo:self.contentView.topAnchor, constant:10).isActive = true
nameLabel.leadingAnchor.constraint(equalTo:self.profileImageView.trailingAnchor, constant:10).isActive = true
nameLabel.trailingAnchor.constraint(equalTo:self.contentView.trailingAnchor).isActive = true
jobTitleDetailedLabel.topAnchor.constraint(equalTo:self.nameLabel.bottomAnchor).isActive = true
jobTitleDetailedLabel.leadingAnchor.constraint(equalTo:self.profileImageView.trailingAnchor, constant:10).isActive = true
jobTitleDetailedLabel.trailingAnchor.constraint(equalTo:self.contentView.trailingAnchor).isActive = true
jobTitleDetailedLabel.bottomAnchor.constraint(equalTo:self.contentView.bottomAnchor, constant:-10).isActive = true
Just add jobTitleDetailedLabel height constrain to be greaterThanOrEqualToConstant profileImageView height + 10 for Margin
because if jobTitleDetailedLabel hight is less than Image hight it will make it small cell row
self.jobTitleDetailedLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
Make sure to constrain the cell's contentView's bottom anchor to be greaterThanOrEqualTo both the profileImageView's bottom anchor and the jobTitleDetailedLabel's bottom anchor. The cell will expand according to whichever is larger.
So you need to remove the jobTitleDetailedLabel's bottom anchor constraint and add:
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: profileImageView.bottomAnchor, constant: 10).isActive = true
contentView.bottomAnchor.constraint(greaterThanOrEqualTo: jobTitleDetailedLabel.bottomAnchor, constant: 10).isActive = true
Also set the contentHuggingPriority of the nameLabel to high in order to ensure that the nameLabel keeps its intrinsic height (the height the text takes up).
nameLabel.setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical)
Finally set the cell's row height to UITableviewAutomaticDimension in the storyboard or through code - tableView.rowHeight = UITableViewAutomaticDimension

Stack view - but with "proportional" gaps

Imagine a stack view with four items, filling something. (Say, filling the screen).
Notice there are three gaps, ABC.
(Note - the yellow blocks are always some fixed height each.)
(Only the gaps change, depending on the overall height available to the stack view.)
Say UISV is able to draw everything, with say 300 left over. The three gaps will be 100 each.
In the example, 9 is left over, so A B and C are 3 each.
However.
Very often, you want the gaps themselves to enjoy a proportional relationship.
Thus - your designer may say something like
If the screen is too tall, expand the spaces at A, B and C. However. Always expand B let's say 4x as fast as the gaps at A and B."
So, if "12" is left over, that would be 2,8,2. Whereas when 18 is left over, that would be 3,12,3.
Is this concept available in stack view? Else, how would you do it?
(Note that recently added to stack view, you can indeed specify the gaps individually. So, it would be possible to do it "manually", but it would be a real mess, you'd be working against the solver a lot.)
You can achieve that by following workaround. Instead of spacing, for each space add a new UIView() that would be a stretchable space. And then just add constraints between heights of these "spaces" that would constrain their heights together based on the multipliers you want, so e.g.:
space1.heightAnchor.constraint(equalTo: space2.heightAnchor, multiplier: 2).isActive = true
And to make it work I think you'd have to add one constraint that would try to stretch those spaces in case there is free space:
let stretchingConstraint = space1.heightAnchor.constraint(equalToConstant: 1000)
// lowest priority to make sure it wont override any of the rest of constraints and compression resistances
stretchingConstraint.priority = UILayoutPriority(rawValue: 1)
stretchingConstraint.isActive = true
The "normal" content views would have to have intrinsic size or explicit constraints setting their heights to work properly.
Here is an example:
class MyViewController: UIViewController {
fileprivate let stack = UIStackView()
fileprivate let views = [UIView(), UIView(), UIView(), UIView()]
fileprivate let spaces = [UIView(), UIView(), UIView()]
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stack)
// let stack fill the whole view
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: self.view.topAnchor),
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
stack.leftAnchor.constraint(equalTo: self.view.leftAnchor),
stack.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
stack.alignment = .fill
// distribution must be .fill
stack.distribution = .fill
stack.spacing = 0
stack.axis = .vertical
for (index, view) in views.enumerated() {
stack.addArrangedSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
// give it explicit height (or use intrinsic height)
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.backgroundColor = .orange
// intertwin it with spaces
if index < spaces.count {
stack.addArrangedSubview(spaces[index])
spaces[index].translatesAutoresizingMaskIntoConstraints = false
}
}
// constraints for 1 4 1 proportions
NSLayoutConstraint.activate([
spaces[1].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 4),
spaces[2].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 1),
])
let stretchConstraint = spaces[0].heightAnchor.constraint(equalToConstant: 1000)
stretchConstraint.priority = UILayoutPriority(rawValue: 1)
stretchConstraint.isActive = true
}
}
Remarkably, #MilanNosáľ 's solution works perfectly.
You do not need to set any priorities/etc - it works perfectly "naturally" in the iOS solver!
Set the four content areas simply to 50 fixed height. (Use any intrinsic content items.)
Simply don't set the height at all of "gap1".
Set gap2 and gap3 to be equal height of gap1.
Simply - set the ratios you want for gap2 and gap3 !
Versus gap1.
So, gap2 is 0.512 the height of gap1, gap3 is 0.398 the height of gap1, etc.
It does solve it in all cases.
Fantastic!!!!!!!!!!
So: in the three examples (being phones with three different screen heights). In fact the relative heights of the gaps, is always the same. Your design department will rejoice! :)
Created: a gist with a storyboard example
The key here is Equal Heights between your arranged views and your reference view:
And then change the 'Multiplier` to your desired sizes:
In this example I have 0.2 for the main view sizes (dark grey), 0.05 within the pairs (black), and 0.1 between the pairs (light grey)
Then simply changing the size of the containing view will cause the views to re-size proportionally:
This is entirely within the storyboard, but you could do the same thing in code.
Note that I'm using only proportions within the StackView to avoid having an incorrect total size, (and making sure they add up to 1.0), but it should be possible to also have some set heights within the StackView if done correctly.

How can I get the height of a view if I set its anchors

I am currently trying to access the height of a view which I previously set anchors for. So for example, I set the left, right, top, and bottom anchors of searchTable using the following in my ViewController:
searchTable.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
searchTable.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
searchTable.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
searchTable.bottomAnchor.constraint(equalTo: menuBar.topAnchor).isActive = true
searchTable is an object of a class that I created that inherits from UIView and has a UIImageView in it. I constrain the UIImageView by using the following in the init function:
imageView.topAnchor.constraint(equalTo: topContainer.topAnchor, constant: 10).isActive = true
imageView.leftAnchor.constraint(equalTo: topContainer.leftAnchor, constant: 10).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: topContainerMultiplier * imageProfileMultiplier).isActive = true
imageView.widthAnchor.constraint(equalTo: self.heightAnchor, multiplier: topContainerMultiplier * imageProfileMultiplier).isActive = true
where:
let topContainerMultipler: Double = 1 / 7
let imageProfileMultipler: Double = 1 / 5
Inside the init function of searchTable, I try want to be able to set the corner radius to be half the image size. I tried to use self.frame.height, self.frame.size.height, self.bounds.height and also getting the .constant value of the self.heightAnchor constraint, but all returns 0.
I would think that there is a solution to get around this, but I haven't been able to find it.
You are querying the height of your view's frame after defining your constraints, but before layout has actually resolved those constraints into a frame rectangle. One approach would be to call layoutIfNeeded to force layout to happen immediately, then use view.frame.size.height. The init method is probably not an appropriate place to do this, as opposed to perhaps inside viewDidLoad on the controller. Or you might do this in viewDidLayoutSubviews to recalculate the corner radius every time your view's layout is updated (in which case you won't need layoutIfNeeded).
Don't use a height constraint if you already use top, bottom, leading, and trailing.
Try to visually debug your view by adding a background color, or using the debug view hierarchy button.
To get the height of a view you should use view.frame.size.height
EDIT (OP question edited)
Your problem is that you try to access the object height when it's just initialized but not displayed yet. You should create a function that update your UI and call it after your view is loaded.

Swift how to set UIView's height constraint based on it's content

I have a UIView in my swift code
let profile_inf_wrapper: UIView = {
let v = UIView()
v.backgroundColor = .red
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
profile_inf_wrapper.topAnchor.constraint(equalTo: view.topAnchor, constant:64).isActive = true
profile_inf_wrapper.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
profile_inf_wrapper.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
profile_inf_wrapper.heightAnchor.constraint(equalToConstant: view.frame.height/4).isActive = true
backgroundImageView.topAnchor.constraint(equalTo: profile_inf_wrapper.topAnchor).isActive = true
backgroundImageView.leftAnchor.constraint(equalTo: profile_inf_wrapper.leftAnchor).isActive = true
backgroundImageView.rightAnchor.constraint(equalTo: profile_inf_wrapper.rightAnchor).isActive = true
backgroundImageView.bottomAnchor.constraint(equalTo: profile_inf_wrapper.bottomAnchor).isActive = true
profileImage.centerYAnchor.constraint(equalTo: profile_inf_wrapper.centerYAnchor).isActive = true
profileImage.leftAnchor.constraint(equalTo: view.leftAnchor, constant:25).isActive = true
profileImage.widthAnchor.constraint(equalToConstant: 110).isActive = true
profileImage.heightAnchor.constraint(equalToConstant: 110).isActive = true
usernameLabel.topAnchor.constraint(equalTo: profile_inf_wrapper.topAnchor, constant:40).isActive = true
usernameLabel.leftAnchor.constraint(equalTo: profileImage.rightAnchor, constant:20).isActive = true
countryIcon.topAnchor.constraint(equalTo: usernameLabel.bottomAnchor, constant:10).isActive = true
countryIcon.leftAnchor.constraint(equalTo: profileImage.rightAnchor, constant:20).isActive = true
countryIcon.widthAnchor.constraint(equalToConstant: 25).isActive = true
countryIcon.heightAnchor.constraint(equalToConstant: 25 ).isActive = true
countryName.topAnchor.constraint(equalTo: usernameLabel.bottomAnchor, constant:5).isActive = true
countryName.leftAnchor.constraint(equalTo: countryIcon.rightAnchor, constant:10).isActive = true
countryName.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
All these elements are the subviews of profile_inf_wrapper.Sometimes view.frame.height/4 is too small and i want to be able to resize the UIView based on it's content
There's a property in UIView called intrinsicContentSize. It returns the smallest size that the view would need show all of it's content.
While the default implementation is not very useful because a UIView doesn't have any content on it's own, all of the default subclasses implement it.
A UILabel will return a size that fits the text perfectly, and a UIButton will return a size that fits it's contents plus whatever spacing you've added. You get the gist of it.
You can take advantage of this property by only constraining either width or height of a view, not both. If you constrain the width of a UILabel and add more text, it will grow vertically.
Finally, when you add subviews to a UIView, and you add constraints to both margins of an axis (top and bottom or left and right), as long as there's a "chain" of constraints and views, and the view doesn't have any constraints on the size, it will expand to fit.
For example, if you have a view with a label and a button, vertically arranged, if the label is constrained to the top, then constrained to the button, and the button is constrained to the bottom, as long as the container view doesn't have a height constraint, it will expand to fit the two views plus the margins perfectly.
Your goal should always be to use the least amount of constraints to express your design, without removing useful constraints. Make sure you take advantage of the intrinsicContentSize.
For setting the height of uiview dynamically you have to add height/bottom constraint to the view in your problem it might be
profile_inf_wrapper.heightAnchor.constraint(equalToConstant: view.frame.height/4+countryName.frame.size.height).isActive = true
you also need the view size to fit to get actual updated size
like
countryName.sizeToFit()
And then update layout if needed to get all affect
The first thing you want to do is make a reference to the height constraint of profile_inf_wrapper.
var profile_inf_wrapper_height_constraint: NSLayoutConstraint?
I don't know the details of your content, but when the view needs resized, you can check that with a conditional in your viewController,
if contentRequiresResizing {
profile_inf_wrapper_height_constraint.constant = view.frame.width/3
else {
profile_inf_wrapper_height_constraint.constant = view.frame.width/4
}
By referencing constraints, it allows you to support dynamic UI changes easily.
As a side note, I would recommend renaming your UIView variable name so that the reference constraint isn't so long. The Swift 3 API guidelines also support lowerCamelCase, as opposed to underscore naming.

Resources