iosswiftstoryboardibdesignable

Designable view not rendered at correct position in Storyboard


I have a custom designable view class that looks like this:

@IBDesignable
class AuthInputView: UIView {
    static let nibName = "AuthInputView"
    
    @IBOutlet weak var mainContainerView: UIView!
    @IBOutlet weak var mainStackView: UIStackView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var errorLabel: UILabel!
    
    override  func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        fromNib()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        fromNib()
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
}

and a corresponding nib called AuthInputView that has its File's Owner set to AuthInputView.

And I have have a view controller designed in storyboard that has a view, who's class is set to AuthInputView. When I run an application it renders fine, but when I look at it in a storyboard, it looks like this:

Storyboard view

Designables are also up to date :

Identity Inspector view

but as can be seen, a custom view is rendered in an incorrect position (top left corner).

The code I use to load from nib and to attach required constraints after a content of a nib is added to a specified view looks like this:

extension UIView {
    
    @discardableResult
    func fromNib<T : UIView>() -> T? {
        guard let contentView = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? T else {
            
            return nil
        }
        self.addSubview(contentView)   
        contentView.translatesAutoresizingMaskIntoConstraints = false
       
        contentView.layoutAttachAll(to: self)
        return contentView
    }
    func layoutAttachAll(to childView:UIView)
    {
        var constraints = [NSLayoutConstraint]()
        
        childView.translatesAutoresizingMaskIntoConstraints = false
        constraints.append(NSLayoutConstraint(item: childView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: childView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0))
        
        childView.addConstraints(constraints)
    }
}

what is causing this misplacement in a storyboard view?


Solution

  • While many people like to use "layout helper" functions, it's easy to get confused...

    You are calling your layoutAttachAll func with:

    contentView.layoutAttachAll(to: self)
    

    but in that function, you are doing this:

    func layoutAttachAll(to childView:UIView)
    {
        var constraints = [NSLayoutConstraint]()
        
        constraints.append(NSLayoutConstraint(item: childView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0))
        ...
    

    but you've passed self as childView, so you're constraining self to self.

    If you put your constraint code "inline":

    func fromNib<T : UIView>() -> T? {
        guard let contentView = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? T else {
            return nil
        }
        self.addSubview(contentView)
    
        var constraints = [NSLayoutConstraint]()
        
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        constraints.append(NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 0))
        constraints.append(NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 0))
        
        self.addConstraints(constraints)
        
        return contentView
    }
    

    you should no longer get the "misplaced" view.

    If you really want to use your layoutAttachAll function, you want to call it with:

    self.layoutAttachAll(to: contentView)
    

    and change the last line:

        // adding to wrong view
        //childView.addConstraints(constraints)
        self.addConstraints(constraints)
    

    Maybe worth noting, you can vastly simplify your "helper" extension to:

    extension UIView {
        @discardableResult
        func fromNib<T : UIView>() -> T? {
            guard let contentView = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? T else {
                return nil
            }
            self.addSubview(contentView)
    
            contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            contentView.frame = bounds
            
            return contentView
        }
    }