Search code examples
iosswiftnslayoutconstraint

IB — Initialize specific custom UIView depending on conditions


I have the following classes:

/**
 * This is basically an empty class and exists for the purpose of being able to 
 * define IBOutlets as BaseHeading, regardless of which specific heading is used in IB.
 */
class BaseHeading: UIView { }


/**
 * This class has a .xib where the layout is defined.
 * This view should be used as navigationBar for modally-presented ViewControllers.
 */
class HeadingIconRight: BaseHeading { } // This class has a .xib. 

/**
 * This class has a .xib where the layout is defined.
 * This view should be used as navigationBar for pused ViewControllers.
 */
class HeadingIconLeft: BaseHeading { } // This class has a .xib.

In MyViewController's .xib, I have a custom UIView of type BaseHeading. In its Swift-file I have an @IBOutlet private weak var heading: BaseHeading!, connected to this.

In the Swift file I know which specific header (HeadingIconRight / HeadingIconLeft) should be used, I'm just unsure how to achieve this in code.

What I want:

  • Initialize a specific HeadingIconXXX
  • Replace IBOutlet's reference to be that view
  • Replace the view in the view hierarchy
  • It needs to have the same constraints (typically 0 to leading/ trailing/ superview.top, 0 to the first view below it)

What I've tried so far:

let headingIndex = self.view.subviews.firstIndex { $0 == self.heading }
let newHeading   = HeadingIconRight()

// Loop through 'old' heading's constraints and convert them to the new heading.
self.heading.constraints.forEach { constraint in
    
    let firstItem:  AnyObject? = constraint.firstItem as! AnyHashable  == self.heading as AnyHashable ? newHeading : constraint.firstItem
    let secondItem: AnyObject? = constraint.secondItem as! AnyHashable == self.heading as AnyHashable ? newHeading : constraint.secondItem
    
    newHeading.addConstraint(
        NSLayoutConstraint(item: firstItem,
                           attribute: constraint.firstAttribute,
                           relatedBy: constraint.relation,
                           toItem: secondItem,
                           attribute: constraint.secondAttribute,
                           multiplier: constraint.multiplier,
                           constant: constraint.constant)
    )
}

self.heading.removeFromSuperview()
self.view.insertSubview(newHeading, at: headingIndex!)
self.heading = newHeading

Of course I'm going to try more, but instead of building something very complicated I'd like to have some guidance in the right direction to come to a good solution.

If it would somehow be possible to override the lifecycle function where the Xib's views are initialized, and replace this one with the specific header, that would be a nice solution (if it doesn't violate the UIViewController API).


Solution

  • I went with @Paulw11's suggestion and created the view at runtime, in code.

    Instead of adding a BaseHeading-view in IB, I constrain the next top-most view to the top of the ViewController and make a reference to this constraint (e.g. topMostConstraint).

    I added the following function to BaseHeading:

    public func constrainToTop(in container: UIView, above otherView: UIView, bottomMargin: CGFloat = 0, disableConstraint: NSLayoutConstraint? = nil)
    {
        container.addSubview(self)
        
        self.translatesAutoresizingMaskIntoConstraints = false
        
        self.topAnchor.constraint(equalTo: container.topAnchor).isActive                             = true
        self.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive                     = true
        self.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive                   = true
        self.bottomAnchor.constraint(equalTo: otherView.topAnchor, constant: -bottomMargin).isActive = true
        
        disableConstraint?.isActive = false
    }
    

    What this does is:

    • Add self to the containerView
    • Create necessary constraints
    • Add a bottomMargin to the next (below) view if desired
    • Disable the old constraint, which is constraining another view to the top of the screen

    In my ViewController's viewDidLoad() I can now do the following:

    public class MyViewController: UIViewController
    {
        private var heading: BaseHeading?
    
        @IBOutlet private weak var topMostConstraint: NSLayoutConstraint!
        @IBOutlet private weak var anyOtherView:      UIStackView!
    
        override func viewDidLoad()
        {
            super.viewDidLoad()
        
            self.heading = HeadingIconRight() // Or any other BaseHeading-subclass.
            self.heading?.constrainToTop(in: self.view,
                                         above: self.anyOtherView,
                                         bottomMargin: 16,
                                         disableConstraint: self.topMostConstraint)
        }
    }
    

    This seems to do the job, and ended up less complicated than I anticipated.