3
votes

IBOutlets de Xib retournant nil lors de l'utilisation de Xib dans le storyboard

J'ai créé une vue en utilisant Interface Builder comme xib. J'ai fait cela car je veux donner à l'utilisateur la possibilité d'incrémenter n'importe quel nombre de ces vues dans le contrôleur de vue. J'ai créé une vue Xib (nommée "TimeRangeView.xib", créé une classe personnalisée (nommée "TimeRangeView.swift") et défini la classe de xib sur cette classe personnalisée, connecté les éléments du storyboard aux IBOutlets de TimeRangeView.swift, définir le le propriétaire du fichier de xib à TimeRangeView.swift, et a ajouté la vue avec sa classe définie sur mon TimeRangeView.swift dans le contrôleur de vue. entrez la description de l'image ici entrez la description de l'image ici

TimeRangeView.swift:

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

Le storyboard UIViewController dans lequel j'utilise ceci a une vue Interface Builder comme sous-vue; la classe de cette vue est assignée comme TimeRangeView.swift. Cette vue est connectée au contrôleur de vue en tant qu'IBOutlet:

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
}

Dans la méthode viewDidLoad () du contrôleur de vue, je souhaite modifier certaines des sous-vues de TimeRangeView (qui, comme nous l'avons vu, sont connectées en tant qu'IBOutlets), mais elles renvoient nul lorsque j'essaye de modifier certaines de leurs propriétés:

@IBOutlet var timeRangeView: TimeRangeView!

timeRangeView ne renvoie pas nil; il est en cours de chargement. Mais lorsque vous essayez d'accéder aux sous-vues de timeRangeView (startDatePicker, endDatePicker, maxTimeLabel), elles renvoient toutes nil.

L'erreur que je reçois est:

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

    }
}


1 commentaires

Si une vue est dans le storyboard, elle n'est pas dans un xib séparé. Votre xib avec les prises n'est jamais chargé.


3 Réponses :


4
votes

Cela se produit parce que le xib n'a jamais été chargé

Donc, ce que vous pouvez faire ici, c'est d'abord ajouter un UIView simple dans le viewController plutôt que TimeRangeView

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
}

Maintenant dans viewDidLoad add xib le xib en utilisant

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

Et ajoutez ceci en tant que sous-vue de timeRangeContainerView

@IBOutlet var timeRangeContainerView: UIView!

Les contraintes sont facultatives


1 commentaires

Solution phénoménale! Je vais modifier mon message pour montrer ce que j'ai changé mon code pour le faire fonctionner. Merci beaucoup!



1
votes

La réponse marquée est correcte et sûrement excellente. J'ai juste pensé à fournir un code plus propre pour cet objectif.

Lien vers github

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

.. ..

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


0 commentaires

0
votes

Une réponse refactorisée ici:

Ajoutez ceci en tant que nouveau fichier à votre projet:

private weak var profilePhotoView: ProfilePhotoView!

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

Ensuite, ajoutez ceci en tant qu'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
    }
}

Dernière étape, utilisation:

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

N'oubliez pas de nommer votre fichier xib comme votre fichier de classe d'affichage personnalisé


0 commentaires