skip to Main Content

It doesn’t seem possible to create nested custom subviews using .xib’s created with Interface Builder. For example I want to create a custom BarView and nested inside of that a custom FooView where the contents of each view is created in IB and stored in
a .xib file:

enter image description here

The BarView is loaded from its .xib file and added as a subview in the ViewController‘s viewDidLoad() method:

class ViewController: UIViewController {
    var barView : BarView?
    override func viewDidLoad() {
        super.viewDidLoad()
        self.barView = 
          (UINib(nibName: "BarView", bundle:Bundle.main).
             instantiate(withOwner: nil, options: nil).first as? BarView)
        self.view.addSubview(self.barView!)
    }
}

I can create and connect to an outlet for the UIButton created by IB:

class BarView: UIView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        NSLog("BarView init:NSCoder, barButton set: (barButton != nil)")
    }

    public override func awakeFromNib() {
        super.awakeFromNib()
        NSLog("BarView: awakeFromNib(), barButton set: (barButton != nil)")
    }

    @IBOutlet weak var barButton: UIButton!    
}

I can can also connect outlets and actions to the nest FooView:

class FooView: UIView {

   required init?(coder aDecoder: NSCoder) {
       super.init(coder: aDecoder)
       NSLog("FooView init:NSCoder, fooButton set: (fooButton != nil)")
   }

    public override func awakeFromNib() {
        super.awakeFromNib()
        NSLog("FooView: awakeFromNib(), fooButton set: (fooButton != nil)")
    }

    @IBAction func doFoo(_ sender: Any) {
        NSLog("doFoo")
    }

    @IBOutlet weak var fooButton: UIButton!
}

I can see the init?(NSCoder) and awakeFromNib() methods are indeed being called. I don’t know why BarView‘s method are called
twice:

... BarView  init:NSCoder, barButton set: false
... BarView: awakeFromNib(), barButton set: false
... FooView init:NSCoder, fooButton set: false
... BarView init:NSCoder, barButton set: false
... BarView: awakeFromNib(), barButton set: true
... FooView: awakeFromNib(), fooButton set: false

The contents of the FooView (i.e. the fooButton) does not show up and is nil in the FooView both callbacks. Is nesting custom view’s created with independent .xib files supported? If so, what is the trick to accomplishing this?

The various suggestions in this post are not quite right or don’t work.

2

Answers


  1. It doesn’t seem possible to create nested custom subviews using .xib’s created with Interface Builder.

    Bear in mind that you’re swimming against the current here; storyboards were introduced exactly to get away from having lots of separate .xib files.

    I can see the init?(NSCoder) and awakeFromNib() methods are indeed being called. I don’t know why BarView’s method are called twice

    They’re called twice because you’re creating two instances. One is in Main.storyboard, and the other is in BarView.xib.

    The contents of the FooView (i.e. the fooButton) does not show up and is nil in the FooView both callbacks

    This sounds like it’s probably another case of mistaken identity, similar to the problem above. There’s an instance of Foo in BarView.xib that’s different from the one in FooView.xib.

    Is nesting custom view’s created with independent .xib files supported? If so, what is the trick to accomplishing this?

    Not directly, as far as I remember. You can certainly load a view from a .xib file and add it to your view graph, though. You could even create a UIView subclass whose job is to load the view in a specified .xib and set it as its own subview. One thing you’ll want to pay attention to is the File’s Owner proxy object in the .xib file; be sure to understand how you can use it to connect objects in your .xib file to outlets in the file’s owner.

    Login or Signup to reply.
  2. It is certainly possible to embed FooView.xib in BarView.xib — the bigger question, though, is: why?

    Here’s a quick example…

    FooView.xib

    enter image description here

    FooView.xib source

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
        <device id="retina3_5" orientation="portrait" appearance="light"/>
        <dependencies>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="System colors in document resources" minToolsVersion="11.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <objects>
            <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="FooView" customModule="BarFooXIBs" customModuleProvider="target"/>
            <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
            <view contentMode="scaleToFill" id="scQ-53-cS2">
                <rect key="frame" x="0.0" y="0.0" width="320" height="165"/>
                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                <subviews>
                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Gm-YT-3U5">
                        <rect key="frame" x="80" y="65.5" width="160" height="34"/>
                        <color key="backgroundColor" systemColor="systemBlueColor"/>
                        <constraints>
                            <constraint firstAttribute="width" constant="160" id="5ue-te-ayT"/>
                        </constraints>
                        <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
                        <state key="normal" title="Foo Button"/>
                        <state key="highlighted">
                            <color key="titleColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        </state>
                        <connections>
                            <action selector="btnTap:" destination="-1" eventType="touchUpInside" id="acw-XK-os0"/>
                        </connections>
                    </button>
                </subviews>
                <viewLayoutGuide key="safeArea" id="eoT-BN-I0x"/>
                <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                <constraints>
                    <constraint firstItem="9Gm-YT-3U5" firstAttribute="centerX" secondItem="scQ-53-cS2" secondAttribute="centerX" id="7jV-qQ-R9k"/>
                    <constraint firstItem="9Gm-YT-3U5" firstAttribute="centerY" secondItem="scQ-53-cS2" secondAttribute="centerY" id="UOQ-gZ-0ew"/>
                </constraints>
                <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
                <point key="canvasLocation" x="354.375" y="445.625"/>
            </view>
        </objects>
        <resources>
            <systemColor name="systemBackgroundColor">
                <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            </systemColor>
            <systemColor name="systemBlueColor">
                <color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            </systemColor>
        </resources>
    </document>
    

    BarView.xib

    enter image description here

    BarView.xib source

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
        <device id="retina3_5" orientation="portrait" appearance="light"/>
        <dependencies>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="System colors in document resources" minToolsVersion="11.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <objects>
            <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="BarView" customModule="BarFooXIBs" customModuleProvider="target"/>
            <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
            <view contentMode="scaleToFill" id="heW-ya-dwr">
                <rect key="frame" x="0.0" y="0.0" width="320" height="336"/>
                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                <subviews>
                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4PF-6J-JzA">
                        <rect key="frame" x="40" y="20" width="240" height="34"/>
                        <color key="backgroundColor" systemColor="systemRedColor"/>
                        <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
                        <state key="normal" title="Bar Button"/>
                        <state key="highlighted">
                            <color key="titleColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                        </state>
                        <connections>
                            <action selector="btnTap:" destination="-1" eventType="touchUpInside" id="lzM-kB-YyV"/>
                        </connections>
                    </button>
                    <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="SRX-fO-B8d" customClass="FooView" customModule="BarFooXIBs" customModuleProvider="target">
                        <rect key="frame" x="20" y="74" width="280" height="242"/>
                        <color key="backgroundColor" systemColor="systemYellowColor"/>
                    </view>
                </subviews>
                <viewLayoutGuide key="safeArea" id="hy0-ij-uLz"/>
                <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                <constraints>
                    <constraint firstItem="SRX-fO-B8d" firstAttribute="top" secondItem="4PF-6J-JzA" secondAttribute="bottom" constant="20" id="95H-Gc-glY"/>
                    <constraint firstItem="4PF-6J-JzA" firstAttribute="top" secondItem="hy0-ij-uLz" secondAttribute="top" constant="20" id="SvQ-5U-H90"/>
                    <constraint firstItem="SRX-fO-B8d" firstAttribute="leading" secondItem="hy0-ij-uLz" secondAttribute="leading" constant="20" id="Syg-Rc-Uw8"/>
                    <constraint firstItem="hy0-ij-uLz" firstAttribute="trailing" secondItem="SRX-fO-B8d" secondAttribute="trailing" constant="20" id="iAU-bn-oAu"/>
                    <constraint firstItem="4PF-6J-JzA" firstAttribute="leading" secondItem="hy0-ij-uLz" secondAttribute="leading" constant="40" id="mjS-As-tx9"/>
                    <constraint firstItem="hy0-ij-uLz" firstAttribute="bottom" secondItem="SRX-fO-B8d" secondAttribute="bottom" constant="20" id="pYK-Yb-uk1"/>
                    <constraint firstItem="hy0-ij-uLz" firstAttribute="trailing" secondItem="4PF-6J-JzA" secondAttribute="trailing" constant="40" id="tMQ-Ka-JDb"/>
                </constraints>
                <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
                <point key="canvasLocation" x="120" y="70"/>
            </view>
        </objects>
        <resources>
            <systemColor name="systemBackgroundColor">
                <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            </systemColor>
            <systemColor name="systemRedColor">
                <color red="1" green="0.23137254901960785" blue="0.18823529411764706" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            </systemColor>
            <systemColor name="systemYellowColor">
                <color red="1" green="0.80000000000000004" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            </systemColor>
        </resources>
    </document>
    

    Storyboard

    enter image description here

    Storyboard source

    <?xml version="1.0" encoding="UTF-8"?>
    <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
        <device id="retina5_9" orientation="portrait" appearance="light"/>
        <dependencies>
            <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
            <capability name="Safe area layout guides" minToolsVersion="9.0"/>
            <capability name="System colors in document resources" minToolsVersion="11.0"/>
            <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
        </dependencies>
        <scenes>
            <!--View Controller-->
            <scene sceneID="tne-QT-ifu">
                <objects>
                    <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="BarFooXIBs" customModuleProvider="target" sceneMemberID="viewController">
                        <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                            <rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
                            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                            <subviews>
                                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ixt-jj-akZ" customClass="BarView" customModule="BarFooXIBs" customModuleProvider="target">
                                    <rect key="frame" x="40" y="84" width="295" height="240"/>
                                    <color key="backgroundColor" systemColor="systemGreenColor"/>
                                    <constraints>
                                        <constraint firstAttribute="height" constant="240" id="YsU-Yu-Czr"/>
                                    </constraints>
                                </view>
                            </subviews>
                            <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                            <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                            <constraints>
                                <constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="Ixt-jj-akZ" secondAttribute="trailing" constant="40" id="Zi8-LJ-vJH"/>
                                <constraint firstItem="Ixt-jj-akZ" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="40" id="qx9-u2-f4a"/>
                                <constraint firstItem="Ixt-jj-akZ" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="40" id="tar-CY-7ai"/>
                            </constraints>
                        </view>
                    </viewController>
                    <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
                </objects>
                <point key="canvasLocation" x="43" y="118"/>
            </scene>
        </scenes>
        <resources>
            <systemColor name="systemBackgroundColor">
                <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
            </systemColor>
            <systemColor name="systemGreenColor">
                <color red="0.20392156862745098" green="0.7803921568627451" blue="0.34901960784313724" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
            </systemColor>
        </resources>
    </document>
    

    Couple "helper" extensions

    public protocol NibLoadable {
        static var nibName: String { get }
    }
    
    public extension NibLoadable where Self: UIView {
        static var nibName: String {
            return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
        }
        static var nib: UINib {
            let bundle = Bundle(for: Self.self)
            return UINib(nibName: Self.nibName, bundle: bundle)
        }
        func setupFromNib() {
            guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading (self) from nib") }
            addSubview(view)
            view.backgroundColor = .clear
            view.frame = self.bounds
            view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        }
    }
    
    extension NSObject {
        var className: String {
            return String(describing: type(of: self))
        }
        class var className: String {
            return String(describing: self)
        }
    }
    

    FooView class

    class FooView: UIView, NibLoadable {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
            commonInit()
        }
        private func commonInit() {
            setupFromNib()
        }
        
        @IBAction func btnTap(_ sender: Any) {
            print(#function, "in", self.className)
        }
    }
    

    BarView class

    class BarView: UIView, NibLoadable {
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        override func prepareForInterfaceBuilder() {
            super.prepareForInterfaceBuilder()
            commonInit()
        }
        private func commonInit() {
            setupFromNib()
        }
        
        @IBAction func btnTap(_ sender: Any) {
            print(#function, "in", self.className)
        }
    
    }
    

    When running, it looks like this:

    enter image description here

    Tapping either button will call the @IBAction func (in the associated class) and output to the debug console:

    btnTap(_:) in BarView
    btnTap(_:) in FooView
    

    Note that we can replicate that via code, rather than Storyboard.

    This, with an empty view controller, will give you the exact same result:

    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            
            let bar = BarView()
            bar.translatesAutoresizingMaskIntoConstraints = false
            bar.backgroundColor = .systemGreen
            view.addSubview(bar)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                bar.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                bar.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                bar.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
                bar.heightAnchor.constraint(equalToConstant: 240.0),
            ])
        }
    
    }
    

    We can mark these as @IBDesignable — however, from my experience, Xcode is pretty sketchy when it comes to that. Frequently get "error: IB Designables: Failed to render and update auto layout status for UIView…"

    As a general rule, embedding a xib in a xib in a xib etc is just asking for trouble. You end up with absurdly tightly coupled elements which will be prone to errors.

    If you have a legitimate reason for doing so, great, it’s your code / project.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search