在故事板中使用 Xib 时,Xib 的 IBOutlets 返回 nil

Xib's IBOutlets returning nil when using Xib in storyboard

我使用 Interface Builder 作为 xib 创建了一个视图。我这样做是因为我想让用户可以选择将任意数量的这些视图递增到视图控制器。我创建了一个 Xib 视图(名为 "TimeRangeView.xib",创建了一个自定义 class(名为 "TimeRangeView.swift")并将 xib 的 class 设置为此自定义 class,已连接将情节提要元素添加到 TimeRangeView.swift 的 IBOutlets,将 xib 的文件所有者设置为 TimeRangeView.swift,并在视图控制器中添加其 class 设置为我的 TimeRangeView.swift 的视图。

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) {

    }
}

我在其中使用的故事板 UIViewController 有一个 Interface Builder 视图作为子视图;此视图的 class 被指定为 TimeRangeView.swift。此视图作为 IBOutlet 连接到视图控制器:

@IBOutlet var timeRangeView: TimeRangeView!

在视图控制器的 viewDidLoad() 方法中,我想更改 TimeRangeView 的一些子视图(正如我们已经介绍的那样,它们作为 IBOutlets 连接),但是当我尝试更改一些时它们返回 nil他们的属性:

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 没有返回 nil;它正在加载。但是当尝试访问 timeRangeView 的子视图(startDatePicker、endDatePicker、maxTimeLabel)时,它们都返回 nil。

我收到的错误是:

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

This is happening Because the xib was never loaded

所以你可以在这里做的是,首先在 viewController 中添加一个简单的 UIView 而不是 TimeRangeView

@IBOutlet var timeRangeContainerView: UIView!

现在在 viewDidLoad 中添加 Fetch the xib by using

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

并将其添加为 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
}

约束是可选的

标记的答案是正确的,肯定很棒。 我只是想为此目的提供一个更简洁的代码。

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
    }
}

此处重构的答案:

将此作为新文件添加到您的项目中:

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
  }
}

然后,将其添加为扩展名:

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
    }
}

最后一步,用法:

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)
    }
}

不要忘记将 xib 文件命名为与自定义视图相同的名称 class 文件