Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding a subclassed UIView to a Nib with its auto layout constraints

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.

like image 340
Nico Liu Avatar asked Sep 21 '14 14:09

Nico Liu


2 Answers

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

}
like image 155
Bogdan Onu Avatar answered Oct 13 '22 01:10

Bogdan Onu


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 :)

like image 38
rintaro Avatar answered Oct 12 '22 23:10

rintaro