I'm trying to use a xib file to create a reusable component (multiple times in the same View), and everything was fine until i wanted to update some controls from an @IBInspectable
property. I found that @IBOutlet
's are not set at that moment, so i did a search and found something
http://justabeech.com/2014/07/27/xcode-6-live-rendering-from-nib/
He is saving a the loaded view into proxyView so you could use it in the @IBInspectable
. Unfortunately that code is a bit old and doesn't work as is. But my problem is when i try to load the nib as a class it doesn't work. It only works if i load it as UIView
.
This line fails as this
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? ValidationTextField
It only works when is like this
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? UIView
I think that the problem is, the xib file's owner is marked as ValidationTextField
, but the main view it's UIView
. So when you load the nib it brings that UIView
, that obviously has no custom properties or outlets.
Many examples about loading xib files say that the class must be in the file owner. So i don't know how to get the custom class using that.
import UIKit
@IBDesignable class ValidationTextField: UIView {
@IBOutlet var lblError: UILabel!
@IBOutlet var txtField: XTextField!
@IBOutlet var imgWarning: UIImageView!
private var proxyView: ValidationTextField?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
override func awakeFromNib() {
super.awakeFromNib()
xibSetup()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
xibSetup()
self.proxyView?.prepareForInterfaceBuilder()
}
func xibSetup() {
guard let view = loadNib() else { return }
view.frame = bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
// Saving the view in a variable
self.proxyView = view
}
func loadNib() -> ValidationTextField? {
let bundle = Bundle(for: type(of: self))
return bundle.loadNibNamed("test", owner: nil, options: nil)?[0] as? ValidationTextField
}
@IBInspectable var fontSize: CGFloat = 14.0 {
didSet {
let font = UIFont(name: "System", size: self.fontSize)
self.proxyView!.txtField.font = font
self.proxyView!.lblError.font = font
}
}
}
I don't even know if the rest will work. If what the link says it's true, getting the view after loading the nib will let me access to the outlets.
Running that code fails at this line
guard let view = loadNib() else { return }
I guess that it can't convert the UIView
to the class so it returns nil, and then exits.
My goal is to have a reusable component that can be placed many times in a single controller. And be able to see its design in the storyboard.
Move xibSetup()
to your initializers. awakeFromNib
is called too late and it won't be called if the view is created programatically. There is no need to call it in prepareForInterfaceBuilder
.
In short, this can be generalized to:
open class NibLoadableView: UIView {
public override init(frame: CGRect) {
super.init(frame: frame)
loadNibContentView()
commonInit()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
loadNibContentView()
commonInit()
}
public func commonInit() {
// to be overriden
}
}
public extension UIView {
// @objc makes it possible to override the property
@objc
var nibBundle: Bundle {
return Bundle(for: type(of: self))
}
// @objc makes it possible to override the property
@objc
var nibName: String {
return String(describing: type(of: self))
}
@discardableResult
func loadNibContentView() -> UIView? {
guard
// note that owner = self !!!
let views = nibBundle.loadNibNamed(nibName, owner: self, options: nil),
let contentView = views.first as? UIView
else {
return nil
}
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = true
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.frame = self.bounds
return contentView
}
}
Note that the view that loads the nib must be the owner of the view.
Then your class will become:
@IBDesignable
class ValidationTextField: NibLoadableView {
@IBOutlet var lblError: UILabel!
@IBOutlet var txtField: XTextField!
@IBOutlet var imgWarning: UIImageView!
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
override func commonInit() {
super.commonInit()
updateFont()
}
@IBInspectable var fontSize: CGFloat = 14.0 {
didSet {
updateFont()
}
}
private func updateFont() {
let font = UIFont.systemFont(ofSize: fontSize)
txtField.font = font
lblError.font = font
}
}
I guess that the whole idea about a proxy object comes from misuse of the Nib owner. With a proxy object, the hierarchy would have to be something like this:
ValidationTextField
-> ValidationTextField (root view of the nib)
-> txtField, lblError, imgWarning
which does not make much sense. What we really want is:
ValidationTextField (nib owner)
-> UIView (root view of the nib)
-> txtField, lblError, imgWarning