Search code examples
iosswiftuiviewscrollviewxib

Load view from XIB as a subview of a scrollview


I'm still a SO and Swift newbie, so please, be patient and feel free to skip this question :-)

In the body of a XIB's awakeFromNib, I want to load some views as subviews of a UIScrollView (basically, the XIB contains a scrollview, a label and a button).

The scrollview perfectly works if in a loop I load views I create on the fly, eg.

 let customView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 150))
 customView.frame = CGRect(x: i*300 , y: 0, width: 300, height: 150)
 customView.translatesAutoresizingMaskIntoConstraints = false
 scrollView.addSubview(customView)

But I have a different goal.

In another XIB I have an image view and a stackview with some labels. This XIB is connected in the storyboard to a class SingleEvent that extends UIView.

I want to do the following:

  1. use the XIB as a sort of "blueprint" and load the same view multiple times in my scrollview;
  2. pass to any instance some data;

Is this possible?

I tried to load the content of the XIB this way:

 let customView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as? SingleEvent

and this way:

let customView = SingleEvent()

The first one makes the app crash, while the second causes no issue, but I can't see any effect (it doesn't load anything).

The content of my latest SingleEvent is the following:

 import UIKit

    class SingleEvent: UIView {

        @IBOutlet weak var label:UILabel!
        @IBOutlet weak var imageView:UIImageView!

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

            loadViewFromNib()
        }

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

            loadViewFromNib()
        }

        func loadViewFromNib() -> UIView {
            let myView = Bundle.main.loadNibNamed("SingleEvent", owner: self, options: nil)?.first as! UIView
            return myView
        }
    }

Thanks in advance, any help is appreciated :-)


Solution

  • There are a number of approaches to loading custom views (classes) from xibs. You may find this method a bit easier.

    First, create your xib like this:

    enter image description here

    Note that the Class of File's Owner is the default (NSObject).

    Instead, assign your custom class to the "root" view in your xib:

    enter image description here

    Now, our entire custom view class looks like this:

    class SingleEvent: UIView {
    
        @IBOutlet var topLabel: UILabel!
        @IBOutlet var middleLabel: UILabel!
        @IBOutlet var bottomLabel: UILabel!
    
        @IBOutlet var imageView: UIImageView!
    
    }
    

    And, instead of putting loadNibNamed(...) inside our custom class, we create a UIView extension:

    extension UIView {
        class func fromNib<T: UIView>() -> T {
            return Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)![0] as! T
        }
    }
    

    To load and use our custom class, we can do this:

    class FromXIBViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // create an instance of SingleEvent from its xib/nib
            let v = UIView.fromNib() as SingleEvent
    
            // we're going to use auto-layout & constraints
            v.translatesAutoresizingMaskIntoConstraints = false
    
            // set the text of the labels
            v.topLabel?.text    = "Top Label"
            v.middleLabel?.text = "Middle Label"
            v.bottomLabel?.text = "Bottom Label"
    
            // set the image
            v.imageView.image = UIImage(named: "myImage")
    
            // add the SingleEvent view
            view.addSubview(v)
    
            // constrain it 200 x 200, centered X & Y
            NSLayoutConstraint.activate([
                v.widthAnchor.constraint(equalToConstant: 200.0),
                v.heightAnchor.constraint(equalToConstant: 200.0),
                v.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                v.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                ])
    
        }
    
    }
    

    With a result of:

    enter image description here

    And... here is an example of loading 10 instances of SingleEvent view and adding them to a vertical scroll view:

    class FromXIBViewController: UIViewController {
    
        var theScrollView: UIScrollView = {
            let v = UIScrollView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .cyan
            return v
        }()
    
        var theStackView: UIStackView = {
            let v = UIStackView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.axis = .vertical
            v.alignment = .fill
            v.distribution = .fill
            v.spacing = 20.0
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // add the scroll view to the view
            view.addSubview(theScrollView)
    
            // constrain it 40-pts on each side
            NSLayoutConstraint.activate([
                theScrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40.0),
                theScrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40.0),
                theScrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40.0),
                theScrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40.0),
                ])
    
            // add a stack view to the scroll view
            theScrollView.addSubview(theStackView)
    
            // constrain it 20-pts on each side
            NSLayoutConstraint.activate([
                theStackView.topAnchor.constraint(equalTo: theScrollView.topAnchor, constant: 20.0),
                theStackView.bottomAnchor.constraint(equalTo: theScrollView.bottomAnchor, constant: -20.0),
                theStackView.leadingAnchor.constraint(equalTo: theScrollView.leadingAnchor, constant: 20.0),
                theStackView.trailingAnchor.constraint(equalTo: theScrollView.trailingAnchor, constant: -20.0),
    
                // stackView width = scrollView width -40 (20-pts padding on left & right
                theStackView.widthAnchor.constraint(equalTo: theScrollView.widthAnchor, constant: -40.0),
                ])
    
    
            for i in 0..<10 {
    
                // create an instance of SingleEvent from its xib/nib
                let v = UIView.fromNib() as SingleEvent
    
                // we're going to use auto-layout & constraints
                v.translatesAutoresizingMaskIntoConstraints = false
    
                // set the text of the labels
                v.topLabel?.text    = "Top Label: \(i)"
                v.middleLabel?.text = "Middle Label: \(i)"
                v.bottomLabel?.text = "Bottom Label: \(i)"
    
                // set the image (assuming we have images named myImage0 thru myImage9
                v.imageView.image = UIImage(named: "myImage\(i)")
    
                theStackView.addArrangedSubview(v)
    
            }
    
        }
    
    }
    

    Result:

    enter image description here