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

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.

Related

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

iOS8 UITableViewCell with ImageView autolayout cell stretch

I am trying to implement AutoLayout on a tableview in iOS 8 with Swift and have it so the cell that holds an image automatically enlarges to fit the image in the correct aspect ratio(aspect fit). I can get it so if I select an image from the image picker and it will display the picture properly. But if I then open the app and expect the image to show the same, it shows are a tiny square. I know it's something to do with my constraints not being set right but I can figure out where the issue lies. The console is complaining about the constraints as well. See the pictures and the code I'm using.
Link to what it looks like:
http://i.stack.imgur.com/5jSBV.png
Link to the constraints setup:
http://i.stack.imgur.com/q5xCR.png
And the code I use to control the image aspect:
func showImage(image: UIImage) {
let aspect = image.size.width / image.size.height
aspectConstraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: imageView, attribute: NSLayoutAttribute.Height, multiplier: aspect, constant: 0.0)
imageView.image = image
imageView.hidden = false
addPhotoLabel.hidden = true
}
Your side edge constraints are what's messing it up.
Remove those, and instead apply an aspect ratio constraint. if you force margins on one axis (top/bottom OR left/right), it will scale the image proportionally to fit.

Treat NSLayoutConstraint constant as multiplier?

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.

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