activating constraints programmatically at runtime - ios

I want to change the positions of my view programmatically at runtime using constraints
I have following arrangement of views
|
viewA
|
viewB
|
viewC
say i have viewA with topAnchor constraints set to top of margin of parent view and viewB with topAnchorset to viewA and so on for viewC
And i want to change the position of these views on certain action at runtime
|
viewA
|
viewC
|
viewB
i have store two different constraints for viewB one with top of viewA and another with top of viewC
viewBTopConstraints = viewB.topAnchor.constraint(equalTo: viewA.bottomAnchor, constant: TOP_SPACE)
newViewBTopConstraints = viewB.topAnchor.constraint(equalTo: viewC.bottomAnchor, constant: TOP_SPACE)
In my toggle action method i have done something like this
viewBTopConstraints?.isActive = false
newViewBTopConstraints?.isActive = true
This works on first run of action however it fails on second time, on further debugging in view debugger i found out that it creates duplicate constraints rather that changing the original one.

Your requirements sounds similar to an app where I have some constraints that change based on landscape or portrait. There I set up three groups of constraints:
Those that always are isActive = true
Those that are isActive = true in portrait
Those that are isActive = true in landscape
The trick is to put the latter two into arrays, and activate/deactive the proper array at the right time. Here's a snippet of what I mean:
var p = [NSLayoutConstraint]()
var l = [NSLayoutConstraint]()
////// portrait layout....
// pin info button above imageLayout, right justified
p.append(info.topAnchor.constraint(equalTo: margins.topAnchor, constant: 20.0))
p.append(info.trailingAnchor.constraint(equalTo: margins.trailingAnchor))
////// landscape layout....
// pin info button above buttons, right justified
l.append(info.topAnchor.constraint(equalTo: margins.topAnchor, constant: 20.0))
l.append(info.trailingAnchor.constraint(equalTo: margins.trailingAnchor))
Note that I'm not setting any isActive = true. I'm just appending things to the two arrays as needed. Now, in viewWillLayoutSubviews() - depending on your needs, viewDidLayoutSubviews() may be the better override - you *activate/deactivate the correct array:
NSLayoutConstraint.deactivate(l)
NSLayoutConstraint.deactivate(p)
if self.bounds.width > self.bounds.height {
NSLayoutConstraint.activate(l)
} else {
NSLayoutConstraint.activate(p)
}
I've found that working with individual constraints, setting isActive, greatly increases the risks of conflicts.

Related

UISplitViewController - Expand & Collapse Master View in iPad Portrait

My universal app displays both master and detail views in iPad with preferredDisplayMode = .allVisible. I need to expand the master view into full screen and hide the detail view on a button click. I know there is functionality to expand detail into full screen hiding master. But couldn't find how to expand and collapse master view.
I tried in expand function as below.
self.splitViewController.preferredPrimaryColumnWidthFraction = 1.0
self.splitViewController.maximumPrimaryColumnWidth = self.splitViewController.view.bounds.size.width as! CGFloat
And collapse function as below.
self.splitViewController.preferredDisplayMode = .allVisible
self.splitViewController.preferredPrimaryColumnWidthFraction = 0.6
self.splitViewController.maximumPrimaryColumnWidth = self.splitViewController.view.bounds.size.width as! CGFloat
But they don't work. Any ideas on how to achieve this?
My setup uses a navigation bar, but the show/hide functionality would probably work without it. What I wanted to achieve is to have the user decide whether to show the "primary" view and have it shown no matter the orientation - something a UISplitViewController doesn't natively do.
I achieved this through activating/deactivating two arrays of constraints, pinning the secondary" view's leading anchor to the "primary's" trailing anchor. From there, all the constraint changes are with the "primary" view.
(I'm using quotation marks because these view's actually have much more logic that a UIView should have, so they are each UIViewControllers with one being a child of the other, but the constraints are view-related.)
Let's start with the static constraints:
primary.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
primary.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
primary.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
secondary.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
secondary.leadingAnchor.constraint(equalTo: toolBar.trailingAnchor).isActive = true
secondaary.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
secondary.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
Next, define/populate the two arrays that will show/hide the primary:
var isShowingPrimary = false
var showPrimary = [NSLayoutConstraint]()
var hidePrimary = [NSLayoutConstraint]()
showPrimary.append(primary.widthAnchor.constraint(equalToConstant: 300))
hidePrimary.append(primary.widthAnchor.constraint(equalToConstant: 0))
NSLayoutConstraint.activate(hidePrimary)
Notice that isActive is true for the static constraints, and I activated the hidePrimary constraints only.
Now all you need to do is wire up a UIBarButtonItem or UIButton to execute a toggle function that will show/hide the primary view, along with animating it:
func togglePrimary() {
if isShowingPrimary {
// hide primary view
NSLayoutConstraint.deactivate(showPrimary)
NSLayoutConstraint.activate(hidePrimary)
} else {
// show primary view
NSLayoutConstraint.deactivate(hidePrimary)
NSLayoutConstraint.activate(showPrimary)
}
// toggle flag and animate changes
isShowingPrimary.toggle()
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
}

Constraints on Child Container

I am adding a child view using the View.addChild method
The containing view is clearly 350 pixels. However, the child view takes up ALL the space of the containing view....so my idea is to force the child view to be smaller than its parent...but my code does not work. I can tell you that if I uncomment the two lines it almost works, but then the child view does not occupy the size that I want it to and it blocks other elements. Here is where I am:
child.view.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.layoutMarginsGuide
//child.view.topAnchor.constraint(equalTo: tableContainer.topAnchor).isActive = true
// child.view.bottomAnchor.constraint(equalTo: tableContainer.bottomAnchor).isActive = true
child.view.leftAnchor.constraint(equalTo: tableContainer.leftAnchor).isActive = true
child.view.rightAnchor.constraint(equalTo: tableContainer.rightAnchor).isActive = true
child.view.heightAnchor.constraint(equalToConstant: 250).isActive = true
self.addChild(child)
Let me state very clearly, my goal is to get the child view to 250 pixels. Thank you.
Your solution might look something like this.
child.view.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.layoutMarginsGuide
//Your left and right anchors tell the compiler exactly how wide the view should be.
//If you have it set to equal both then the view MUST be exactly the width of parent.
child.view.leftAnchor.constraint(equalTo: tableContainer.leftAnchor).isActive = true
child.view.rightAnchor.constraint(equalTo: tableContainer.rightAnchor).isActive = true
//Since you're defining your own height here you need to explicitly state where the
//view starts at. If you don't define a top, center, bottom or some other constraint
//it's impossible to know where to put the view.
child.view.heightAnchor.constraint(equalToConstant: 250).isActive = true
child.view.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
self.addChild(child)
The reason for this is that when creating anchors, you MUST have an X & Y anchors for all views, even logically. For example if you define a width of 100 and a height of 100 you've created a square, but where does that square go? You don't have an X or a Y in that example. However, if you define a width to match the parent view using the left and right anchor then it knows the width of the view just by the anchors on the left and right. The same principle applies to a top and bottom anchor. If you define a top and bottom anchor then it will be the size of the view (Given that you set them equalTo) and it will know the height.
In your instance, you've defined height of 250 however it doesn't know where to start at. Does it start at the top, middle, bottom, it doesn't have a clue because you haven't set it. The IDE is very literal with constraints and no obscurity will work.

Centering between two anchors

I want to set a centerYAnchor between two anchors. Similar to this:
centeredLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
However, I don't want it to be centered relative to the screen. I want it to be right in between two other anchors on the screen. Like if I have a toolbar at the top like this:
toolbar.topAnchor.constraint(equalTo: view.topAnchor),
Then I have a button at the bottom like this:
button.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: -20)
is there a way I can center centeredLabel's y constraint to be right between the bottomanchor of toolbar and the top anchor of button?
is there a way I can center centeredLabel's y constraint to be right between the bottomanchor of toolbar and the top anchor of button?
Yes, there is. The simple way is to use a transparent spacer view whose top is anchored to the upper anchor and whose bottom is anchored to the lower anchor. Now you center-anchor your label to the center of the spacer view.
However, although that is simple, it is not the best way. The best way is to create, instead of a transparent spacer view, a custom UILayoutGuide. Unfortunately this can be done only in code, not in the storyboard (whereas the spacer view and label can be configured entirely in the storyboard). But it has the advantage that it doesn't burden the rendering tree with an additional view.
Here's your situation, more or less, using a button as the upper view and a button as the lower view. The label is centered vertically between them:
Here's the code that generated that situation. b1 and b2 are the buttons (and it doesn't matter how they are created and positioned):
let g = UILayoutGuide()
self.view.addLayoutGuide(g)
g.topAnchor.constraint(equalTo: b1.bottomAnchor).isActive = true
g.bottomAnchor.constraint(equalTo: b2.topAnchor).isActive = true
g.leadingAnchor.constraint(equalTo:b1.leadingAnchor).isActive = true
g.trailingAnchor.constraint(equalTo:b1.trailingAnchor).isActive = true
let lab = UILabel()
lab.text = "Label"
lab.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(lab)
lab.leadingAnchor.constraint(equalTo:g.leadingAnchor).isActive = true
lab.centerYAnchor.constraint(equalTo:g.centerYAnchor).isActive = true
Although #matt's solution works, here is a simpler one that uses autolayout without the need to create a UILayoutGuide.
iOS 10 introduced a simple way of doing this through NSLayoutXAxisAnchor.anchorWithOffset(to:) and NSLayoutYAxisAnchor.anchorWithOffset(to:).
Here are convenience methods which wrap up this logic.
For X axis centering
extension NSLayoutXAxisAnchor {
func constraint(between anchor1: NSLayoutXAxisAnchor, and anchor2: NSLayoutXAxisAnchor) -> NSLayoutConstraint {
let anchor1Constraint = anchor1.anchorWithOffset(to: self)
let anchor2Constraint = anchorWithOffset(to: anchor2)
return anchor1Constraint.constraint(equalTo: anchor2Constraint)
}
}
For Y axis centering
extension NSLayoutYAxisAnchor {
func constraint(between anchor1: NSLayoutYAxisAnchor, and anchor2: NSLayoutYAxisAnchor) -> NSLayoutConstraint {
let anchor1Constraint = anchor1.anchorWithOffset(to: self)
let anchor2Constraint = anchorWithOffset(to: anchor2)
return anchor1Constraint.constraint(equalTo: anchor2Constraint)
}
}
To do what you need you can call:
centeredLabel.centerYAnchor.constraint(between: toolbar.bottomAnchor, and: button.topAnchor)

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.

Changing the x position of button elements inside its container in swift 3

I have a scroller at the bottom of my view controller which contains buttons. Buttons are not always active, they change as the user adds them to his favourites. Which means I have to decide on the x position of the buttons programmatically.
What I have done is created outlets of all the buttons from the view controller in the storyboard to the script and I am attempting to access them like this (simplified code):
var Items = [String : Bool]
#IBOutlet weak var item1: UIButton!
...
if Items[item] == true {
item1.isEnabled = true
\\ here comes a line of code where i should position item1's x position but I haven't managed to find the right way
}
The question is how to change the x position of UIButton programmatically, and are there better ways to handle the problem? I am using Xcode 8 and swift 3
EDIT :
Method for constraints:
func activeConstraint(element: UIButton, constant: Int) {
let widthConstraint100 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 100)
let widthConstraint0 = NSLayoutConstraint(item: element,
attribute:NSLayoutAttribute.width,
relatedBy:NSLayoutRelation.equal,
toItem:nil,
attribute:NSLayoutAttribute.notAnAttribute,
multiplier:1.0,
constant: 0)
if constant == 0 {
element.removeConstraint(widthConstraint100)
element.addConstraint(widthConstraint0)
} else {
element.removeConstraint(widthConstraint0)
element.addConstraint(widthConstraint100)
}
element.updateConstraints()
}
Example of call to the function:
if Items[item] == true {
activeConstraint(element: item, constant: 100)
} else {
activeConstraint(element: item, constant: 0)
}
EDIT 2:
Although I have seen many questions here referring UIStackView (like this one), none of the solutions worked for me. I have an horizontal scroller at the bottom of the screen with many buttons that get activated or deactivated programmatically with their width constraint set to 0 or 100. Here is the final result that works in my case (XCode 8.0) :
Hierarchy:
image
UIScrollView HAS to have one element: View (lets call it Container View) that holds all the contents of the scroller. Container view needs to have 4 constraints - top, bottom, leading and trailing set towards superView (UIScrollView). (preferably all 4 set to 0). Also set vertical constraint to center horizontally from Container view to superView - UIScrollView.
StackView holds all the buttons - it needs to have all 4 constraints (top, bottom, leading, trailing) set towards superView which is Container View.
All buttons need to have their width constraint set to a constant of desire.

Resources