How to store autolayout anchors mapped onto UIView classes - swift

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])
}
}

Related

Why are my subview bounds (0,0,0,0) after setting constraints?

I am using the latest version of swift and writing everything programmatically. I’m trying to create a UIView holderView that resides inside and is constrained to the bounds of the safe area of the top level view. This code returns
(0.0, 0.0, 414.0, 896.0)
(0.0, 0.0, 0.0, 0.0)
which suggests that the holderView is not constrained to the top level view. Can anyone please advise on how to proceed? Code below.
class WelcomeViewCon: UIViewController {
var holderView = UIView()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configure()
}
private func configure() {
view.backgroundColor = .systemRed
view.addSubview(holderView)
holderView.backgroundColor = .systemGray
let constraints = holderView.constraintsForAnchoringTo(boundsOf: view)
NSLayoutConstraint.activate(constraints)
print(view.bounds)
print(holderView.bounds)
}
}
extension UIView {
/// Returns a collection of constraints to anchor the bounds of the current view to the given view.
///
/// - Parameter view: The view to anchor to.
/// - Returns: The layout constraints needed for this constraint.
func constraintsForAnchoringTo(boundsOf view: UIView) -> [NSLayoutConstraint] {
return [
topAnchor.constraint(equalTo: view.topAnchor),
leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor)
]
}
}
It’s just a matter of timing. Constraints do not take effect until after layout. But you are applying constraint during layout (which is totally wrong; this is what updateConstraints is for, or just do it all once in viewDidLoad) and so you cannot measure the results until after the next layout.
Moreover layout happens many times so your code adds the subview and the constraints over and over. Dangerous stuff.

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.

Updating constraints on orientation change

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.

NSViewController story board with coded anchors collapses window

I'm having trouble mixing story boards and coded autolayout in Cocoa + Swift. It should be possible right?
I started with a NSTabViewController defined in a story board with default settings as dragged out of the toolbox. I added an NSTextField view via code. And I added anchors. Everything works as expected except the bottom anchor.
After adding the bottom anchor, the window and controller seem to collapse to the size of the NSTextField. I expected the opposite, that the text field get stretched to fill the height of the window.
What am I doing wrong? The literal Frame maybe? Or some option flag that I'm not setting?
class NSTabViewController : WSTabViewController {
var summaryView : NSTextField
required init?(coder: NSCoder) {
summaryView = NSTextField(frame: NSMakeRect(20,20,200,40))
summaryView.font = NSFont(name: "Menlo", size: 9)
super.init(coder: coder)
}
override func viewDidLoad() {
self.view.addSubview(summaryView)
summaryView.translatesAutoresizingMaskIntoConstraints = false
summaryView.topAnchor.constraintEqualToAnchor(self.view.topAnchor, constant: 5).active = true
summaryView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor, constant: 5).active = true
summaryView.rightAnchor.constraintEqualToAnchor(self.view.rightAnchor, constant: -5).active = true
summaryView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor, constant: -5).active = true
}
To prevent window from collapsing set lower priority for hugging:
summaryView.setContentHuggingPriority(249, forOrientation: .Vertical)
But you actually misuse tab view controller. It just manages views in common use... while you are adding text to the tab header area. There is a very good tutorial of how to use it correctly: https://www.youtube.com/watch?v=TS4H3WvIwpY

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