I have a basic protocol like this:
public protocol BasicSwiftClassLayout {
var nibName: String { get }
}
and my class is this:
public class BasicSwiftClass<Layout: BasicSwiftClassLayout> {}
I've defined 2 different structs so that I could initiate this class with 2 different nibs:
public struct NormalLayout: BasicSwiftClassLayout {
public let nibName = "NormalLayoutNibName"
}
and
public struct OtherLayout: BasicSwiftClassLayout {
public let nibName = "OtherLayoutNibName
}
I now have 2 questions based off of this.
I want to use the Layout variable to retrieve the nib name that this class was initiated. So if I were to initiate this class as: let myView = BasicSwiftClass<NormalLayout>
I want to be able to retrieve the nibName for the NormalLayout in the class ("NormalLayoutNibName"). I would think to do something like let myNib = Layout.nibName
but it just tells me Instance member nibName cannot be used on type Layout
So how do I go about retrieving the nibName?
When I didn't have the generic class added into my class and it was just public class BasicSwiftClass
, the classForCoder
was just MyProject.BasicSwiftClass
. Now that I have the generic behavior added in, the classForCoder
is being returned as MyProject.BasicSwiftClass<Layout.NormalLayout>
which is no longer returning the correct bundle when calling let bundle = Bundle(for: classForCoder)
Do I need to override the classForCoder
variable? Or am I doing something else wrong here?
Thank you!
Since your BasicSwiftClass doesn't have a instance of a type that conforms to BasicSwiftClassLayout
, and instead just has knowledge of the type itself, you should make the nibName
requirement static, so it can be called on the type itself, and not an instance, like so:
public protocol BasicSwiftClassLayout {
static var nibName: String { get }
}
public struct NormalLayout: BasicSwiftClassLayout {
public static let nibName = "NormalLayoutNibName"
}
public struct OtherLayout: BasicSwiftClassLayout {
public static let nibName = "OtherLayoutNibName"
}
As for your second question, I don't think the Bundle(for class: AnyClass)
initializer works for generic classes, period. I'm happy to be wrong on this if someone has found a way to do it, but my guess is that because the compiler generates concrete variations of generic types, and only those that are actually used, this leads to a couple of challenges:
The specific specialization of a generic type could be used / generated in any module that imports and has visibility and access to your generic type. For example, if a different module imported your code and defined a public struct DifferentLayout: BasicSwiftClassLayout
type, would the variation BasicSwiftClass<DifferentLayout>
be part of your bundle, which defines all the logic of BasicSwiftClass
, but didn't have that variation when it was compiled, or the other module, which caused the compiler to create that specialization?
You didn't technically defined the specialized type BasicSwiftClass<OtherLayout>
, the compiler did based on determining where it would be used. So again, it can get fuzzy which bundle this really belongs to.
I'm guessing that if there was a way to reference the generic type itself, i.e. an unspecialized BasicSwiftClass<Layout>
type reference, then the Bundle(for:)
would work correctly. But there is no way to reference the generic type like that, because it's more of a template for the compiler to use for generating concrete specializations than an actual type that exists at runtime.
If you make the protocol require a class type for conformance, you can get the bundle for the layout instead of for the BasicSwiftClass. e.g.:
public protocol BasicSwiftClassLayout: class {
static var nibName: String { get }
}
final public class NormalLayout: BasicSwiftClassLayout {
public static let nibName = "NormalLayoutNibName"
}
public class BasicSwiftClass<Layout: BasicSwiftClassLayout> {
func loadFromNib() {
let view = Bundle(for: Layout.self).loadNibNamed(Layout.nibName, owner: self, options: nil)?.first
}
}
In response to your question regarding how to make an array of BasicSwiftClass<Layout>
with different values for Layout
, there are different ways to address this based on how you need to use the element retrieved from the array. Ultimately, Swift requires that collections (including arrays) contain elements that are all the same type or can be addressed as the same common type, so all the different approaches are based on converting, wrapping or casting each BasicSwiftClass<Layout>
instance to some common type that exposes the common functionality you want to call on each instance.
The most simple solution would probably be to create a protocol that BasicSwiftClass<Layout>
conforms to, which exposes the functionality you need. For example, if all you care about is getting a view from each instance to add to a view hierarchy, you could do something like this:
public protocol ViewRepresentable {
var view: UIView { get }
}
extension BasicSwiftClass<Layout>: ViewRepresentable {
public var view: UIView { // load the nib, connect outlets and do other setup, then return the appropriate UIView }
}
let array: [ViewRepresentable] = [BasicSwiftClass<IPadLayout>, BasicSwiftClass<NormalLayout>]
array.forEach { self.containerView.addSubview($0.view) }
If you need something more specialized or that preserves some type information internally, etc. you may need to use a wrapper or a type eraser. You might also try having BasicSwiftClass<Layout>
inherit from a non-generic superclass that has the basic functionality you need and typing the collection to be an array of instances of that superclass type. But if you can express the common requirements in a non-generic protocol, that should keep you on the easiest path.