Updating constraints on orientation change - swift

Below is the view in portrait mode (Image 1) and in landscape I wanted to show as (Image 2). I am facing issue to show in it properly in landscape.
Image 1:
I have setup constraints in storyboard.
greenView: top: 0, leading: 0, trailing: 0, width: equal to superview.width, height: equal to superview.height/2
Image 2:
I tried modifying constraints but when I turn device to landscape, greenView becomes 1/4 of the screen. below is the code.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
} else {
}
}

Instead of headache of creating constraints for this problem insert both views in a UIStackView (vertical) , and inside viewWillTransition change axis to horizontal if the orientation is isLandscape
plus adding these constraints
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
will make a conflict as the old ones are not removed
//
func shared () {
if UIDevice.current.orientation == .portrait {
self.stView.axis = .vertical
}
else {
self.stView.axis = .horizontal
}
}
Call the above method in viewDidLoad & viewWillTransition

isActive flag is highly misunderstood option. This flag does not change constraint's state, it completely adds or removes a constraint.
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
The above code will add multiple constraint on your view. Every time you rotate your device a new width, height constraint gets added to your view which will result in your view having multiple height and width constraints. To add/remove same constraint, store its reference then use isActive on it.
I'm not sure why you are setting height constraint to 0?
Now coming to what you want to do. I can think of 2 approaches
1st Approach
Add two more constraints in addition to existing constraints in your storyboard but keep their priority low(<1000):
1. greenView.bottom = safeArea.bottom
2. greenView.width = superView.width/2
Make IBOutlet of greenView.height = superview.height/2 and greenView.trailing = superView.trailing. The outlets should be of those constraints which have high priority. Make sure your Outlets are not weak otherwise their outlet will become nil when you set isActive false. Now all you have to do is set this when device changes to landscape mode:
highPriorityGreenViewConstraint.isActive = false
highPriorityHeightConstraint.isActive = false
2nd Approach
Use size classes to set your constraint. All size classes are mentioned here.
Example - Install greenView.bottom = safeArea.bottom,greenView.width = superView.width/2 constraints for compact width compact height size class only. You will have to put more constraints in this approach as landscape size class is different even among iPhone models.

Related

Add layoutMargins to one element in a UIStackView

I would like to create a vertical stackview with 3 elements in it.
I want a bit more space only between the 2nd and the last element. So I thought about adding to the last element :
mylastelement.layoutMargins = UIEdgeInsets(top:30, left:0,bottom:0, right:0)
But the layoutmargins are not applied in my stackview. Is there any easy way to achieve that (Id like to avoid to modify the last element inner height).
EDIT : I just tried to increase 2nd element height (+50) within its frame by doing :
my2ndElementLabel.sizeToFit()
my2ndElementLabel.frame = CGRect(x:my2ndElementLabel.frame.origin.x,y:lmy2ndElementLabel.frame.origin.y,
width:my2ndElementLabel.frame.width, height:my2ndElementLabel.frame.height + 50)
but it has no effect.
EDIT2 : I tried to add a random view to my UIStackView, but the the view is just ignored ! May have missed something in understanding how UIKit work ?... :
let v = UIView(frame:CGRect(x:0,y:0,width:100,height:400))
v.backgroundColor = .red
myStackView.addArrangedSubview(v)
//...
Here is an extension I made that helps to achieve fast such margins :
extension UIStackView {
func addArrangedSubview(_ v:UIView, withMargin m:UIEdgeInsets )
{
let containerForMargin = UIView()
containerForMargin.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: containerForMargin.topAnchor, constant:m.top ),
v.bottomAnchor.constraint(equalTo: containerForMargin.bottomAnchor, constant: m.bottom ),
v.leftAnchor.constraint(equalTo: containerForMargin.leftAnchor, constant: m.left),
v.rightAnchor.constraint(equalTo: containerForMargin.rightAnchor, constant: m.right)
])
addArrangedSubview(containerForMargin)
}
}
What you could do is set a custom spacing between the second and third element.
myStackView.setCustomSpacing(30.0, after: my2ndElementLabel)
In the same general vein, you can constrain the top (or bottom) anchor of your view relative to the corresponding edge of any view in which it's embedded. What's ugly being somewhat a matter of taste, I find autolayout constraints easy to use and easy to reason about.
A simple example from Mac OS rather than iOS:
let button = ControlFactory.labeledButton("Filter")
addSubview(button)
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20).isActive = true
button.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
This particular code lives in the view initializer, and positions a button in the middle of a view, 20 points up from the bottom.
I found myself : It looks like UIStackView doesn't work at all with old sizing system (with .frame). It seems you have to constraint height and width, and StackView will constraint left/top/right/bottom position for you when you add the arrangedSubview.
My second view was a label : I wanted a margin of 40, under the text. So i first computed the label height into its .frame property, and constraint the height at frame.height + 40(= my margin)
labelDesc.sizeToFit()
labelDesc.heightAnchor.constraint(equalToConstant:40).isActive = true
I find my own solution utterly ugly though. I'm sure UIKit provide a better way to achieve such a simple goal, without having to make these kind of DIY solutions. So please if you're used to work with UIKit, tell me if there is any better solution.
Consider adding a "margin" by inserting a correctly-sized UIView within the Stack View as needed.
If you need a 40px margin between 2 specific elements... add a UIView with a height constraint of 40px. Assign a clearColor background to make it invisible.
You can add IBOutlets to this view and hide it as you would any other item in the Stack View.

How to store autolayout anchors mapped onto UIView classes

I'm currently working on a very interactive UI. Views need to be moved based on userInput. I've got about six views all with child UI elements stored as separate classes. In my main viewController I want to handle the resetting of these view's constraints. In order to do so I need to store the NSLayout constraints somewhere. Piling them all into my viewController just doesn't feel right.
I've gone through quite a few iterations already but every time it seems like I need to manually create and individually store all these NSlayoutconstraint properties on the parent Viewcontroller.
What i'm not looking for is a UI extension with a function that sets the constraints and stores them a properties on that class so I can deactivate or change them at will.
extension UIView {
func anchorFixedHeight(
top: NSLayoutAnchor<NSLayoutYAxisAnchor>,
lead: NSLayoutAnchor<NSLayoutXAxisAnchor>,
trail: NSLayoutAnchor<NSLayoutXAxisAnchor>,
height: CGFloat,
Ypadding: CGFloat,
Xpadding: CGFloat)
{
self.topAnchor.constraint(equalTo: top, constant: Ypadding)
self.heightAnchor.constraint(equalToConstant: height)
self.leadingAnchor.constraint(equalTo: lead, constant: Xpadding)
self.trailingAnchor.constraint(equalTo: trail, constant: -Xpadding)
}
}
But this wont work because NSlayouconstrain.activate() takes in array of constraints and once you set an achor it turns into an anchor constraint.
I've been trying to store them as properties, but swift wont allow stored properties in extensions. But I also do not want to but I also do not want to duplicate all these constraint properties in all my Views
You may need something like this
class CusView:UIView {
var top:NSLayoutConstraint!
var lead:NSLayoutConstraint!
var tra:NSLayoutConstraint!
var heigh:NSLayoutConstraint!
override func didMoveToSuperview() {
super.didMoveToSuperview()
self.translatesAutoresizingMaskIntoConstraints = false
top = self.topAnchor.constraint(equalTo:self.superview!.topAnchor, constant: 12)
heigh = self.heightAnchor.constraint(equalToConstant: 12)
lead = self.leadingAnchor.constraint(equalTo:self.superview!.leadingAnchor, constant: 12)
tra = self.trailingAnchor.constraint(equalTo:self.superview!.trailingAnchor, constant: -12)
NSLayoutConstraint.activate([top,lead,tra,heigh])
}
}

How can I avoid specifying potentially conflicting constraints

I am trying to create all of my views in my UICollectionViewCell programatically. The views currently lay out correctly.
I have a vertical stackView that contains two UILabels and a UIImageView.
The views do not have a fixed content size and are all variable.
If you see in my code, I am setting the leading and trailing constraints of my UIStackView with a constant of 8.0 from the safeAreaLayoutGuide. Because of this, I have to set the widthAnchor to be -16.0 (otherwise the cell does not lay out correctly). Is there a way I can avoid specifying this in two places? For example, if I set the leading to be 14.0 points from the leading, then I will have to make sure I remember to change my width to be -20.0, otherwise the contents will spill over.
override func updateConstraints() {
/// Check if views already have constraints
if !viewsHaveConstraints {
viewsHaveConstraints = true
/*
Here I set the hugging and compression priority for the 3 views in the UIStackView
*/
let margins = safeAreaLayoutGuide
/* Set vstackView constraints */
/// vStackView width anchor equal to width of margin less constant
vStackView.widthAnchor.constraint(equalTo: margins.widthAnchor, constant: -16.0).isActive = true
/// vStackView leading anchor constraint
vStackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 8.0).isActive = true
/// vStackView trailing anchor constraint
vStackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 8.0).isActive = true
vStackView.heightAnchor.constraint(equalTo: margins.heightAnchor).isActive = true
}
super.updateConstraints()
}

Setting constraints on UITableView inside UIView

I have a UIViewController that has some base controls and in the center has a UIView (as container of swappable ui controls). I swap out the UI controls in this center UIView (container) depending on what the user is trying to do. All of the UI controls that go in to the UIView container are defined programmatically and I use programatic constraints to place them inside the UIView container.
This works fine for all of the sets of UI controls I have done so far. Now I am adding a set of controls to the UIView container that includes a UITableView
I cant figure out how to get the TableView to show up inside the UIView programatically. I can define say a button and label and run the app and see the container with the button and the label. If I add the UITableView as below then the container just does not show up at all.
// tableView
tableView.leftAnchor.constraint(equalTo: inputsContainerView.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: inputsContainerView.bottomAnchor).isActive = true
tableView.widthAnchor.constraint(equalTo: inputsContainerView.widthAnchor).isActive = true
tableView.heightAnchor.constraint(equalTo: inputsContainerView.heightAnchor, multiplier: 1/2).isActive = true
prior to the above code I have already added all of the needed controls to the container subview ...
// Add controls to the view
inputsContainerView.addSubview(listTextView)
inputsContainerView.addSubview(listImageButton)
inputsContainerView.addSubview(listImageClear)
inputsContainerView.addSubview(tableView)
If I leave off the tableview then the container shows up with the other three fields. If I add the tableview then the container and all the other three controls are gone.
How do I add the tableView to the UIView and have it show up?
Here is how I defined the UITable view
let tableView: UITableView = {
let tv = UITableView()
return tv
}()
as a compare, when I define others controls like this they show up fine after adding to the subview and setting the constraints programatically
e.g.
// DEFINE
let listTextView: UITextView = {
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.text = ""
textView.textColor = defaultTextColor
textView.font = subtitleFont
textView.layer.borderColor = UIColor.black.cgColor
textView.layer.borderWidth = 1
textView.textAlignment = NSTextAlignment.left
return textView
}()
then later
// Place - with constraints
// listTextView
listTextView.leftAnchor.constraint(equalTo: inputsContainerView.leftAnchor, constant: padFromLeft).isActive = true
listTextView.topAnchor.constraint(equalTo: inputsContainerView.topAnchor, constant: 10).isActive = true
listTextView.widthAnchor.constraint(equalTo: inputsContainerView.widthAnchor, multiplier: 9/16).isActive = true
listTextView.heightAnchor.constraint(equalTo: inputsContainerView.widthAnchor, multiplier: 5/16).isActive = true
Just added my comment as detailed answer, so others can see the solution faster and benefit from it.
So taken from the apple documentation, to set your own constraints programmatically, you need to set translatesAutoresizingMaskIntoConstraints to false:
Note that the autoresizing mask constraints fully specify the view’s size and position; therefore, you cannot add additional constraints to modify this size or position without introducing conflicts. If you want to use Auto Layout to dynamically calculate the size and position of your view, you must set this property to false, and then provide a non ambiguous, nonconflicting set of constraints for the view.
So in your case you miss to set it for your table view, when you define it. Just add this line to it:
tv.translatesAutoresizingMaskIntoConstraints = false

Updating existing constraints does not work, wrong use of .active property?

I have a storyboard that contains :
A "tab bar" on the left with 5 tabs
A container on the right side of the tab bar that contains 6 image views that share the same space so it looks like this :
Each image view is configured to occupy 1/3 of the container's width and 1/2 of its height.
However, different ratios can be provided at runtime (from a JSON file) so that for example, the 1st image view's height become 70% of its container's height and 50% of its width (therefore, the 2nd and 3rd image views widths occupy 25% of the container's width and the 4th image view, 2nd line column 1 has a height of 30% of the image view).
To do so here is what I tried :
-create 2 arrays of outlets (width and height constraints from my image views) :
// Height constraints
#IBOutlet var heightConstraints: [NSLayoutConstraint]!
// Width constraints
#IBOutlet var widthConstraints: [NSLayoutConstraint]!
-create an outlet for the container of the image views
// Drawings - Container
#IBOutlet weak var drawingView: UIView!
-create stored properties to update my constraints (these are not outlets)
// Drawing height property
var drawingHeightConstraint: NSLayoutConstraint?
// Drawing width property
var drawingWidthConstraint: NSLayoutConstraint?
And here comes the job : I've overridden updateViewConstraints(), is this a mistake ? It seemed the best place to update constraint but I saw people use ViewWillAppear...
This method is called every time I click on a tab (and then load new drawings with new ratios)
public override func updateViewConstraints() {
super.updateViewConstraints()
// Activate default height and width constraints
PapooHomePageViewController.activateConstraint(constraints: heightConstraints)
PapooHomePageViewController.activateConstraint(constraints: widthConstraints)
// Deactivate temporarily created new height and width constraints
drawingHeightConstraint?.active = false
drawingWidthConstraint?.active = false
// Display drawings
let drawingElements:[(String, Double, Double)] = papooBrain.getDrawings(forSection: currentSection)
for (index, (drawing, heightRatio, widthRatio)) in drawingElements.enumerate() {
drawingViews[index].image = UIImage(named: drawing)
// update height constraint if ratio is different than defaut ratio of 1/2
if heightRatio != Double(heightConstraints[index].multiplier) {
heightConstraints[index].active = false
drawingHeightConstraint = NSLayoutConstraint(item: drawingViews[index], attribute: .Height, relatedBy: .Equal, toItem: drawingView, attribute: .Height, multiplier: CGFloat(heightRatio), constant: 0)
drawingHeightConstraint!.active = true
}
if widthRatio != Double(widthConstraints[index].multiplier) {
widthConstraints[index].active = false
drawingWidthConstraint = NSLayoutConstraint(item: drawingViews[index], attribute: .Width, relatedBy: .Equal, toItem: drawingView, attribute: .Width, multiplier: CGFloat(widthRatio), constant: 0)
drawingWidthConstraint!.active = true
}
}
// Which one should I or should i NOT call, in what order ?
drawingView.setNeedsUpdateConstraints()
drawingView.setNeedsLayout()
drawingView.layoutIfNeeded()
}
-Here is the code of the little helper to activate my constraints. NOTE: This my be a problem. I try to activate a constraint previously deactivated (it comes from my array of outlets) but I don't want it to be duplicated
class func activateConstraint(constraints constraints: [NSLayoutConstraint]) {
for constraint in constraints {
constraint.active = true
}
}
-Finally, here is a piece of my JSON so you see what I parse...
"drawings": [
{
"image": "01-01-drawing.png",
"height-ratio": "0.5",
"width-ratio": "0.33",
etc.
The problem(s)
If I change my configuration file (json) to say "Okay, image view 1's height ratio is O.7 and so image view 4's height ratio is 0.3" : I have conflicting constraints (it seems that after all, the "active" property did not deactivate properly my constraint
So when I am debugging, I see all my constraints of width and height getting duplicated, causing a nightmare.
The same happens for the height etc.
Many questions here
Did I use correctly (in terms of lifecycle etc.) updateViewConstraints() ? Is this good to call the super at the beginning ?
What is the correct use of setNeedsLayout, layoutIfNeeded, setNeedsUpdateConstraints...
When I set an outlet NSLayoutConstraint's active property to false. Then active = true afterwards, did it get the correct reference to my default height/width from the storyboard ?
Thank you for reading