5

I am trying to create a UIView(A) containing 2 custom Views (B) in it

View B is setup using Autolayout Constraints and made in Interface Builder, including the constraints. A is added in the Nib of the viewController

B - UIImageView(Leading = 10, Trailing = 10, AlignVertically) - UITextField(Leading = 10, Trailing = 10, AlignVertically)

ViewController A(300x300, AlignHorizontally, AlignVertically)

In the ViewController I have A to be fixed at 300x300 and B1 and B2 has its Leading, Trailing, Top and Bottom pinned at 0. (which should make B1 and B2 to be 300x150, forgive me if I miss something out)

When loading View B I use the following code to load its Nib:

override func awakeAfterUsingCoder(aDecoder: NSCoder!) -> AnyObject! {
    if self.subviews.count == 0 {
        let bundle = NSBundle(forClass: self.dynamicType)
        var view = bundle.loadNibNamed("B", owner: nil, options: nil)[0] as B
        view.setTranslatesAutoresizingMaskIntoConstraints(false)
        let constraints = self.constraints()
        self.removeConstraints(constraints)
        view.addConstraints(constraints)
        return view
    }
    return self
}

But when I try to run this I get the following warning including a crash:

The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x7f897ad1acc0 V:[TestProject.B:0x7f897af73840(300)]>
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 have also tried adding the view as a property of View B and use the following code to add it to B

NSBundle.mainBundle().loadNibNamed("B", owner: self, options: nil)
self.addSubview(self.viewOfB);

the result of this is the view is added to the viewController, but it is not adopting any of the AutoLayoutConstraints from its own Nib.

Right now I have no idea how to add this view to the viewController's view including the constraints. What am I doing wrong? Is there a better way to do this?

PS: View A used to be custom too.

PPS: I am using Swift to do this, but I'm sure solutions in Objective-C works as well.

Nico Liu
  • 845
  • 2
  • 9
  • 14
  • take a look at this tutorial it should be helpful in order to use programmatically created constraint in swift http://makeapppie.com/2014/07/26/the-swift-swift-tutorial-how-to-use-uiviews-with-auto-layout-programmatically/ – LS_ Sep 26 '14 at 13:15

2 Answers2

5

First of all the error you are getting is because

  • you can't swap or move constraints between your views
  • the views must already be added in the hierarchy before adding new constraints
  • the constraints are usually added on the parent view that holds the views (or common ancestor).

    (for more details see AutoLayout Guide)

I would suggest having view A (CustomView : UIView) loading the contents (B Views) from the nib file and adding them as it's subviews.

Main View with CustomView A

The trick is that you need to layout you B views inside a content view so that your nib file has only one root object in it.

This way, when you add the content view as the view A subview, all constraints will transfer over(similar with how UITableViewCells use their contentView).

ContentView xib

Important Note: The content view should NOT be of the class type CustomView. If you want to drag outlets and actions just declare the File's Owner object of the class CustomView and link them there.

Your final view hierarchy should look like this:

MainView (ViewController's view)
  View A
    Content View
      B1 View
      B2 View

This way view A has it's layout configured in the ViewController's storyboard/xib and you B views will use the constraints from the nib file related to the content view.

So the only thing let is to make sure is that the content view will always have the same size with view A.

class CustomView: UIView
{
 @IBOutlet var _b1Label: UILabel!
 @IBOutlet var _b2Button: UIButton!

func loadContentView()
{
    let contentNib = UINib(nibName: "CustomView", bundle: nil)

    if let contentView = contentNib.instantiateWithOwner(self, options: nil).first as? UIView
    {
        self.addSubview(contentView)

        //  We could use autoresizing or manually setting some constraints here for the content view
        contentView.translatesAutoresizingMaskIntoConstraints = true
        contentView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
        contentView.frame = self.bounds
    }
}

override init(frame: CGRect)
{
    super.init(frame: frame)

    loadContentView()
}

required init(coder aDecoder: NSCoder)
{
    super.init(coder: aDecoder);

    loadContentView()
}

}
Kendall Helmstetter Gelner
  • 74,769
  • 26
  • 128
  • 150
Bogdan Onu
  • 129
  • 5
  • I tried a number of ways of adding constraints manually for the embedded view, but nothing worked as reliably as this approach - translating the mast and turning on translatesAutoresizingMaskIntoConstraints.. – Kendall Helmstetter Gelner Sep 22 '15 at 03:58
4

The problem is: constraints retrieved from self are referring to self.

Instead, you have to make new constraints referring to view and with same other properties. like this:

NSMutableArray *constraints = [NSMutableArray array];
for(NSLayoutConstraint *constraint in self.constraints) {
    id firstItem = constraint.firstItem;
    id secondItem = constraint.secondItem;
    if(firstItem == self) firstItem = view;
    if(secondItem == self) secondItem = view;
    [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                        attribute:constraint.firstAttribute
                                                        relatedBy:constraint.relation
                                                           toItem:secondItem
                                                        attribute:constraint.secondAttribute
                                                       multiplier:constraint.multiplier
                                                         constant:constraint.constant]];
}

/* if you also have to move subviews into `view`.
for(UIView *subview in self.subviews) {
    [view addSubview:subview];
}
*/

[view addConstraints:constraints];

And, I think you should copy a few more properties from self.

view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

Sorry for Obj-C code, I copied from this answer :)

Community
  • 1
  • 1
rintaro
  • 51,423
  • 14
  • 131
  • 139
  • Brilliant. Would have never figured out how to properly copy those constraints over from self correctly. – zmonteca Mar 04 '15 at 01:16