1

I'm trying to understand how to properly subclass view which is loaded from a xib in Swift.

I've got TitleDetailLabel class which is subclass of UIControl. This class has titleLabel and detailLabel outlets which are UILabels.

class TitleDetailLabel: UIControl {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var detailLabel: UILabel!

    override func awakeAfterUsingCoder(aDecoder: NSCoder) -> AnyObject? {
        return NTHAwakeAfterUsingCoder(aDecoder, nibName: "TitleDetailLabel")
    }

    func setTitle(text: String) {
        self.titleLabel.text = text
    }

    func setDetailText(text: String) {
        self.detailLabel.text = text
    }
}

XIB structure:

  • Placeholders
    • File's Owner: NSObject (not changed)
    • First Responder
  • Title Detail Label - UIView - TitleDetailLabel class
    • Label - UILabel - title label
    • Label - UILabel - detail label

In Storyboard I've got view controller and placeholder - simple UIView object with constraints.

I've created extension to UIView class to simplify swapping placeholder with object I am interested in. It works good with this TitleDetailLabel class. Here is how it looks:

extension UIView {
    public func NTHAwakeAfterUsingCoder(aDecoder: NSCoder, nibName: String) -> AnyObject? {
        if (self.subviews.count == 0) {
            let nib = UINib(nibName: nibName, bundle: nil)
            let loadedView = nib.instantiateWithOwner(nil, options: nil).first as UIView

            /// set view as placeholder is set
            loadedView.frame = self.frame
            loadedView.autoresizingMask = self.autoresizingMask
            loadedView.setTranslatesAutoresizingMaskIntoConstraints(self.translatesAutoresizingMaskIntoConstraints())

            for constraint in self.constraints() as [NSLayoutConstraint] {
                var firstItem = constraint.firstItem as UIView
                if firstItem == self {
                    firstItem = loadedView
                }

                var secondItem = constraint.secondItem as UIView?
                if secondItem != nil {
                    if secondItem! == self {
                        secondItem = loadedView
                    }
                }

                loadedView.addConstraint(NSLayoutConstraint(item: firstItem, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: secondItem, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
            }

            return loadedView
        }

        return self
    }
}

I decided to create BasicTitleDetailLabel subclass of TitleDetailLabel class to keep there some configuration code and other stuff.

class BasicTitleDetailLabel: TitleDetailLabel {

    override func awakeFromNib() {
        super.awakeFromNib()
        self.setup()
    }

    override init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setup()
    }

    private func setup() {
        self.titleLabel.textColor = UIColor.NTHCadetGrayColor()
        self.detailLabel.textColor = UIColor.NTHLinkWaterColor()
    }
}

But application crashes every time after I changed class of this placeholder from TitleDetailLabel to BasicTitleDetailLabel.

App crashes because titleLabel and detailLabel are nil.

How can I properly use this TitleDetailLabel class with xib and how to subclass this correctly? I don't want to create another xib which looks the same like the first one to use subclass.

Thanks in advance.

Tomasz Szulc
  • 4,217
  • 4
  • 43
  • 79
  • Could you post the error returned on crash? – bjtitus Jan 25 '15 at 19:11
  • App crashes because after `super.init(coder: aDecoder)` I called `self.setup()` and in this moment outlets are `nil` (`Unexpectedly found nil...`). `awakeAfterUsingCoder:` return object of type `TitleDetailLabel`. I read something that this is not possible to create object of different class using xibs. – Tomasz Szulc Jan 25 '15 at 20:46
  • Why are you assuming that the outlets are around after init? They will not be around until after the xib has been loaded. Does it work if you just check that the labels aren't nil before trying to access them? To make this more clear, you can set the outlets as optionals and use `if let` before accessing them. – bjtitus Jan 25 '15 at 20:55
  • Hi @TomaszSzulc, Did you find any solution for this. I am facing the same issue. – nikita Nov 25 '19 at 06:54

1 Answers1

1

Make sure you make the set the File's Owner for the .xib file to the .swift file. Also add an outlet for the root view and then load the xib from code. This is how I did it for a similar project:

import UIKit
class ResuableCustomView: UIView {

    @IBOutlet var view: UIView!
    @IBOutlet weak var label: UILabel!
    @IBAction func buttonTap(sender: UIButton) {
        label.text = "Hi"
    }

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

        NSBundle.mainBundle().loadNibNamed("ReusableCustomView", owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }
}

It is a little different than yours but you can add the init:frame method. If you have an awakeFromNib method then don't load the setup method in both awakeFromNib and init:coder.

My full answer for the above code project is here or watch this video.

Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393