简体   繁体   中英

Xib's IBOutlets returning nil when using Xib in storyboard

I've created a view using Interface Builder as a xib. I've done this as I want to give the user the option to increment any number of these views to the view controller. I've created an Xib view (named "TimeRangeView.xib", created a custom class (named "TimeRangeView.swift") and set the xib's class to this custom class, connected the storyboard elements to the TimeRangeView.swift's IBOutlets, set the xib's file owner to TimeRangeView.swift, and added the view with its class set to my TimeRangeView.swift in the view controller. 在此处输入图片说明 在此处输入图片说明

TimeRangeView.swift:

import UIKit

class TimeRangeView: UIView {
    @IBOutlet var startTimeLabel: UILabel!
    @IBOutlet var startDatePicker: UITextField!
    @IBOutlet var endTime: UILabel!
    @IBOutlet var endDatePicker: UITextField!
    @IBOutlet var addButton: UIButton!
    @IBOutlet var maxTimeLabel: UILabel!



    @IBAction func addButtonPressed(_ sender: UIButton) {

    }
}

The storyboard UIViewController I am using this in has an Interface Builder view as a subview; this view's class is assigned as TimeRangeView.swift. This view is connected to the view controller as an IBOutlet:

@IBOutlet var timeRangeView: TimeRangeView!

Within the view controller's viewDidLoad() method, I am wanting to change some of TimeRangeView's subviews (which, as we've covered, are connected as IBOutlets), but they are returning nil when I try to change some of their properties:

override func viewDidLoad() {
    super.viewDidLoad()
    timeRangeView.maxTimeLabel.text = "Max duration: \(timeString ?? "n/a")"

    //Change the text fields' input views
    let startTimeDatePickerToolBar = UIToolbar().ToolbarPicker(mySelect: #selector(dismissPicker))

    let endTimeDatePickerToolBar = UIToolbar().ToolbarPicker(mySelect: #selector(dismissPicker))

    startTimeDatePicker.setDate(Date(timeIntervalSince1970: NSDate().timeIntervalSince1970 + 60), animated: false)

    endTimeDatePicker.setDate(Date(timeIntervalSince1970: NSDate().timeIntervalSince1970 + 120), animated: false)

    timeRangeView.startDatePicker.inputView = startTimeDatePicker
    timeRangeView.startDatePicker.inputAccessoryView = startTimeDatePickerToolBar
    timeRangeView.startDatePicker.delegate = self

    timeRangeView.endDatePicker.inputView = endTimeDatePicker
    timeRangeView.endDatePicker.inputAccessoryView = endTimeDatePickerToolBar
    timeRangeView.endDatePicker.delegate = self
}

timeRangeView is not returning nil; it is loading. But when trying to access timeRangeView's subviews (startDatePicker, endDatePicker, maxTimeLabel), they are all returning nil.

The error I am receiving is:

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

This is happening Because the xib was never loaded

So what you can do here is, First add a simple UIView in the viewController rather than TimeRangeView

@IBOutlet var timeRangeContainerView: UIView!

Now in viewDidLoad add Fetch the xib by using

let timeRangeView = Bundle.main.loadNibNamed("TimeRangeView", owner: self, options: nil)?[0] as? TimeRangeView

And add this as a subview of timeRangeContainerView

override func viewDidLoad() {
    super.viewDidLoad()
    let timeRangeView = Bundle.main.loadNibNamed("TimeRangeView", owner: self, options: nil)?[0] as? TimeRangeView

    timeRangeView.frame = CGRect(origin: CGPoint.zero, size: self.timeRangeContainerView.frame.size)

    self.timeRangeContainerView.insertSubview(timeRangeView, at: 0)

    //  Add constraints
    timeRangeView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10).isActive = true
    timeRangeView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0).isActive = true
    timeRangeView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
    timeRangeView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
    timeRangeView.translatesAutoresizingMaskIntoConstraints = false  


    //Add the rest of your code here
}

The constraints are optional

The marked answer is correct and surely great. I just thought of providing a cleaner code for that aim.

Link to github

// Assuming NavigationArrow is the class of your nib and navigationContainerView is the view container:
navigationView = NavigationArrow.loadNibInside(ofType: NavigationArrow.self, containerView: navigationContainerView)

.. ..

extension UIView: NibReusable {

     class func loadNibInside<T: UIView>(ofType: T.Type, containerView: UIView) -> T {
        let view = T.instantiate() as! T
        view.fill(in: containerView)
        return view
    }

    public class func instantiate<T: UIView>() -> T {

        return nib.instantiate(withOwner: nil, options: nil).first as! T
    }

    @discardableResult
    public func fill(in view: UIView) -> (left: NSLayoutConstraint, right: NSLayoutConstraint, top: NSLayoutConstraint, bottom: NSLayoutConstraint) {

        if view.subviews.contains(self) == false {
            view.addSubview(self)
        }
        translatesAutoresizingMaskIntoConstraints = false
        let left = leadingAnchor.constraint(equalTo: view.leadingAnchor)
        left.isActive = true
        let right = trailingAnchor.constraint(equalTo: view.trailingAnchor)
        right.isActive = true
        let top = topAnchor.constraint(equalTo: view.topAnchor)
        top.isActive = true
        let bottom = bottomAnchor.constraint(equalTo: view.bottomAnchor)
        bottom.isActive = true
        return (left, right, top, bottom)
    }
}



public protocol NibReusable: Reusable {

    static var nib: UINib { get }
}

public extension NibReusable {

    public static var nib: UINib {
        let name = NSStringFromClass(self).components(separatedBy: ".").last!
        return UINib(nibName: name, bundle: Bundle(for: self))
    }
}


public protocol Reusable: class {

    static var reuseIdentifier: String { get }
}

public extension Reusable {

    public static var reuseIdentifier: String {
        let name = NSStringFromClass(self).components(separatedBy: ".").last!
        return name
    }
}

A refactored answer here:

Add this as a new file to your project:

import UIKit

class CustomSubviewFromNib {
  static func add(subview: UIView, into parentView: UIView) {
    subview.frame = CGRect(origin: CGPoint.zero, size: parentView.frame.size)
    
    parentView.insertSubview(subview, at: 0)
    
    //  Add constraints
    subview.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 0).isActive = true
    subview.trailingAnchor.constraint(equalTo: parentView.trailingAnchor, constant: 0).isActive = true
    subview.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 0).isActive = true
    subview.bottomAnchor.constraint(equalTo: parentView.bottomAnchor, constant: 0).isActive = true
    subview.translatesAutoresizingMaskIntoConstraints = false
  }
}

Then, add this as an extension:

import UIKit

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

Last step, usage:

final class ProfileViewController {

    private weak var profilePhotoView: ProfilePhotoView!

    override func viewDidLoad() {
      super.viewDidLoad()
        
      setupProfilePhotoView()
    }
      
    func setupProfilePhotoView() {
      let profilePhotoView: ProfilePhotoView = UIView.fromNib()
      self.profilePhotoView = profilePhotoView
      CustomSubviewFromNib.add(subview: profilePhotoView, into: profileContainerView)
    }
}

Don't forget to name your xib file as same as your custom view class file

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM