Treat NSLayoutConstraint constant as multiplier? - swift

I have three horizontally aligned UIViews within a container UIView. These three views should span the entire width of the above UIImageView. The problem, however, is that sometimes only one or two of the child views should be shown.
I set up my view hierarchy like so:
Since the child views are set to be equal to the width of the first child (which will always be shown), I simply set the width of the first child to be a fraction of the UIImageView width. So if three child views should be shown, the first child view would have a multiplier of 1/3 the width of the UIImageView. If two child views should be shown, the multiplier would be 1/2. If just one, the multiplier would be 1.
This seemed like a perfect solution, however the multiplier property is read only. My first attempt to solve this was by creating three different NSLayoutConstraints attached to the first child view, all with a different multiplier with 2/3 of them turned off. Then, on runtime, I would enable the appropriate constant with the appropriate multiplier based off of the number of views I wanted to show.
This resulted in a lot of ugly errors, and so did my second solution:
var constraint = NSLayoutConstraint(item: color1, attribute:NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: imageView, attribute: NSLayoutAttribute.Width, multiplier: 1/2, constant: 0)
view.addConstraint(constraint)
Where I would add a new constraint to the view based on the multiplier I wanted. This, of course, resulted in an error:
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled.
My question, therefore, is if I can treat the constant property like the multiplier property. My fear with doing this, however, is that if I set the constant for the width of the first child view, it would not update its width when the phone rotates.
Is there a better solution for all of this?

Firstly, In your question your were using IB but you seemed to suggest there may be a different number of views each time you moved to the view controller which is why I decided to create the NSLayoutConstraints programatically.
Secondly, my solution is fine provided you didn't intend to change the number of views whilst you were on the view controller. If you did, then this needs a bit more work.
In your view controller:
var viewWidthConstraints : [NSLayoutConstraint] = []
override func viewDidLoad() {
super.viewDidLoad()
let numberOfViews = 3
var previousView: UIView = self.view
for i in 0..<numberOfImages {
let myView = UIView()
myView.setTranslatesAutoresizingMaskIntoConstraints(false)
myView.backgroundColor = UIColor.grayColor().colorWithAlphaComponent(CGFloat(i) * (1/CGFloat(numberOfImages)) + 0.1)
self.view.addSubview(myView)
let heightConstraint = NSLayoutConstraint.constraintsWithVisualFormat("V:|[view]|",
options: .DirectionLeadingToTrailing,
metrics: nil,
views: ["view" : myView])
let widthConstraint = NSLayoutConstraint(item: myView,
attribute: .Width,
relatedBy: .Equal,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 1.0,
constant: self.view.frame.width / CGFloat(numberOfImages))
let attribute: NSLayoutAttribute = (i == 0) ? .Left : .Right
let leftConstraint = NSLayoutConstraint(item: myView,
attribute: .Left,
relatedBy: .Equal,
toItem: previousView,
attribute: attribute,
multiplier: 1.0,
constant: 0.0)
self.view.addConstraints(heightConstraint)
self.view.addConstraint(leftConstraint)
myView.addConstraint(widthConstraint)
viewWidthConstraints.append(widthConstraint)
previousView = myView
}
}
func updateWidthConstraints() {
if viewWidthConstraints.count > 0 {
let width = self.view.frame.width / CGFloat(viewWidthConstraints.count)
for constraint in viewWidthConstraints {
constraint.constant = width
}
}
}
override func viewWillLayoutSubviews() {
updateWidthConstraints()
}
In viewDidLoad you add the UIViews to the view and set up their constraints. The vertical constraint you could change to make the UIViews appear underneath your UIImageView. And change numberOfViews to increase or decrease the number of views.
Then in viewWillLayoutSubviews you update the width of each view using their width constraint. This will make sure, if the device is rotated, each view takes up the correct proportion of the screen.
This is what is looked like with horizontal orientation.
And vertical orientation.

Related

change views height in Keyboard extension (iPhone X)

I have following problem: I build a keyboard extension, a alternative keyboard to the system keyboard. In all older iPhone models it works fine, but at the iPhone X it doesn't. My problem is, the height of the keyboard's view is decreasing from 216px (iPhone 8 ,8+ , ...) to 141px (iPhone X). Because lower height, my keyboard buttons are now smaller, to small for good user usability.
I did used a .xib file to create the UI, I add all UI - Items programmatically.
my Question
Is it posible to get more space (height) for the keyboard extension view (specially for the iPhone X) ?
I solve my problem. I found following paragraph in the Apple documentation:
You can adjust the height of your custom keyboard’s primary view using Auto Layout. By default, a custom keyboard is sized to match the system keyboard, according to screen size and device orientation. A custom keyboard’s width is always set by the system to equal the current screen width. To adjust a custom keyboard’s height, change its primary view's height constraint.
The following code lines show how you might define and add such a constraint:
CGFloat _expandedHeight = 500;
NSLayoutConstraint *_heightConstraint =
[NSLayoutConstraint constraintWithItem: self.view
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 0.0
constant: _expandedHeight];
[self.view addConstraint: _heightConstraint];
Swift 4
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let heightConstraint = NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0.0, constant: 300)
self.view.addConstraint(heightConstraint)
}
Adding the constraints to ViewDidLoad method solves the issue.
override func viewDidLoad() {
super.viewDidLoad()
let heightConstraint = NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0.0, constant: 220)
self.view.addConstraint(heightConstraint)
setupKeyboard()
Note: Creating a subview generates a memory warning.

is there more than one layer to constrain on a button in Xcode?

I'm attempting to remove a UIImageView from my viewController in a swift Xcode project, if a certain condition exists (if a user has purchased "remove ads"). I am able to do so by setting the images' .image to nil and the constraint that sets it's size to nil. however when I do this, the buttons go to the correct location at the bottom of the view, but the button's pressable area is no longer the entire button. I can press it if I touch the bottom left corner of it.
if I am able to present the appearance of the button in the proper place, how do I affect the functional area it?
You should check the button new frame or any view in front of the button that catch the touch event.
Normally, UIButton touch area is its frame area.
Here is the code I used, instead of just nil for the image and banner view.
self.adBannerView.removeFromSuperview()
self.bottomGreyArea.removeFromSuperview()
self.view.addConstraint(NSLayoutConstraint(item: nextButton, attribute: .bottom, relatedBy: .equal, toItem: self.bottomLayoutGuide, attribute:.top, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: backButton, attribute: .bottom, relatedBy: .equal, toItem: self.bottomLayoutGuide, attribute:.top, multiplier: 1, constant: 0))
self.nextButton.translatesAutoresizingMaskIntoConstraints = false
self.backButton.translatesAutoresizingMaskIntoConstraints = false
removing from the superview cleared out all the constraints, so I then rebuilt the relevant constraints for the buttons (which previously depended on the bottomGreyArea. all is well in the world again!

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

Swift, Constraint, The view hierarchy is not prepared for the constraint

I'm trying to add an image inside the navigation constroller and center it. So I do have an imageView and a UINavigationBar and I want to add the image view to that navigation bar and center it horizontally. The UINavigationBar comes from the UINavigationController. I keep getting the following error:
The view hierarchy is not prepared for the constraint:
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.
I tried different approaches but the only one that was working was if I was setting the imageView within self.view and adding the constraint to self.view.
let bar: UINavigationBar = self.navigationController!.navigationBar
let logo = UIImage(named: "logo-horizontal")
let imageView:UIImageView = UIImageView(image:logo)
imageView.setTranslatesAutoresizingMaskIntoConstraints(false)
imageView.frame.size.width = 100;
imageView.frame.size.height = 31;
bar.addSubview(imageView)
imageView.addConstraint(NSLayoutConstraint(
item: imageView,
attribute: NSLayoutAttribute.CenterX,
relatedBy: NSLayoutRelation.Equal,
toItem: bar,
attribute: NSLayoutAttribute.CenterX,
multiplier: 1,
constant: 0
))
So as said in my comment, it seems to be a lot easier if you create a UIImageView and set the titleView of navigationItem to that image view. In order for the size to be displayed correctly you have to set the contentMode to ScaleAspectFit and set the height to something like "30" or whatever height you like. Below you can see how i did it.
let logoView:UIImageView = UIImageView(image: UIImage(named: "logo-filename"))
logoView.contentMode = UIViewContentMode.ScaleAspectFit
logoView.frame.size.height = 30
self.navigationItem.titleView = logoView
Ps. I guess you don't have to set the width because of ScaleAspectFit.

NSLayoutConstraint won't work

I want to move 2 button to center of width of screen.
It's should looks like: |<-(100)FirstButton(50)->SecondButton(100)->|
I started from first button.
var const = NSLayoutConstraint(item: firstButton,
attribute: NSLayoutAttribute.Left,
relatedBy: NSLayoutRelation.Equal,
toItem: view.superview,
attribute: NSLayoutAttribute.Left,
multiplier: 1.0,
constant: 100)
Why it doesn't work?
It seem that firstButton have not set the property translatesAutoresizingMaskIntoConstraints to false
Apple document description of translatesAutoresizingMaskIntoConstraints:
/* By default, the autoresizing mask on a view gives rise to constraints that fully determine
the view's position. This allows the auto layout system to track the frames of views whose
layout is controlled manually (through -setFrame:, for example).
When you elect to position the view using auto layout by adding your own constraints,
you must set this property to NO. IB will do this for you.
*/
#available(iOS 6.0, *)
open var translatesAutoresizingMaskIntoConstraints: Bool // Default YES
So you must set firstButton's translatesAutoresizingMaskIntoConstraints to false when you adding your own constraints