Get the type name of derived type in a static method of base class or extension Swift
Asked Answered
E

1

2

TL:DR Paste this into Swift playground:

import UIKit

public protocol NibLoadable {
    static var nibName: String { get }
}

extension NibLoadable where Self: UIView {
    public static var nibName: String {
        return String(describing: self)
    }

    func printName(){
        print(Self.nibName)
    }
}

public class Shoes: UIView, NibLoadable {


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

    override init(frame: CGRect) {
        super.init(frame: frame)
        printName()
    }
}

public class Cake: Shoes {

}

let cake = Cake() // this line prints 'Shoes'

How do i make it print Cake, not Shoes?

Full explanation:

I have found the method to get xib files loaded into storyboards like so: https://mcmap.net/q/150479/-load-view-from-an-external-xib-file-in-storyboard

This works amazingly, however i have to put boilerplate NibLoadable code into each view like so:

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

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }

this is obviously pretty annoying when you have some 20-30 views in this manner.

So i am trying to setup a base class that would take care of this, and then have everything inherit it.

Unfortunately and no matter what i tried, the String(describing: Self.self) always returns the name of my base class, NOT the name of the derived class.

I get this exception in the console logs when i open my Storyboard in XCode:

[MT] IBSceneUpdate: [scene update, delegate=] Error Domain=IBMessageChannelErrorDomain Code=3 "Failed to communicate with helper tool" UserInfo={NSLocalizedFailureReason=The agent raised a "NSInternalInconsistencyException" exception: Could not load NIB in bundle: 'NSBundle (loaded)' with name 'BaseNibLoadable.Type', IBMessageSendChannelException=Could not load NIB in bundle: 'NSBundle (loaded)' with name 'BaseNibLoadable.Type', NSLocalizedDescription=Failed to communicate with helper tool}

Example:

@IBDesignable
class BaseNibLoadable: UIView, NibLoadable {
    // MARK: - NibLoadable
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }
}
@IBDesignable
class SomeHeader: BaseNibLoadable {

    @IBOutlet fileprivate weak var headerLabel: UILabel!
    @IBInspectable var header: String? {
        didSet {
            self.headerLabel.text = header
        }
    }
}

Obviously my main bundle (same bundle for both) includes the SomeHeader.xib file.

So what i am looking for is to somehow get the name of the actual type being instantiated in the nibName static method.

UPDATE As requested, here is the original answer i linked at the top of the question (rather, just the code from it as it is quite long otherwise... please follow the link):

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    public static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
        view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
}

Update 2

I do know that the subclass is definitely in the stack at that point because if i log the Thread.callStackSymbols the actual concrete view class name is there.

Export answered 1/4, 2019 at 23:33 Comment(7)
@matt Ok, let me rehash the answer i link to again. What i am trying to do is build views with xibs and then add those views to OTHER storyboards and have IB render the UI, rather than blank views. Which means that this specific code does not run when the app runs, it is ONLY run by interface builder trying to render my views.Export
So you would add a SomeHeader.xib, SomeHeader.swift and OtherStoryboard.storyboard to your app. Then you would add a UIView somewhere to the storyboard. Then you would change the class name of that view to SomeHeader - and voila, you see your header inside IB.Export
This works totally fine if i put boilerplate code into Someheader file (e.g. remove BaseNibLoadable). Now i am just trying to get rid of boilerplate.Export
Okay, so this is for like an IBDesignable view?Bausch
@Bausch Correct. let me update the code in the question.Export
@Bausch i added a simplified Swift playground example, maybe you know how to solve that? it has a lot less to do with Interface builder...Export
Excellent distillation!Bausch
B
1

How do i make it print Cake, not Shoes?

Change

print(Self.nibName)

to

print(type(of:self).nibName)
Bausch answered 2/4, 2019 at 1:31 Comment(2)
Actually, my crappy answer only worked with debug symbols, when i uploaded to testflight, the app was crashing. Your solution works everywhere, and it is actually correct. Thank you so much! (too bad Stack doesn't have reddit's 'give gold')Export
Thanks but I couldn't have done it without your distillation which was also a lot of work. We solved it together!Bausch

© 2022 - 2024 — McMap. All rights reserved.