Swift Generic Inheritance

Generic class inheritance in Swift

Your class definition of TrackingCache is wrong. It repeats the generic parameter:

class TrackingCache<AftershipTracking>: BaseCache<AftershipTracking> { }

It should be left out:

class TrackingCache: BaseCache<AftershipTracking> { }

This triggers the underlying swift error Classes derived from generic classes must also be generic. You can work around this issue by specifying a type parameter that is required to be or inherit from AftershipTracking:

class TrackingCache<T: AftershipTracking>: BaseCache<AftershipTracking> { }

Full example:

class BaseCache<T: Equatable>: NSObject {
var items: [T] = []

func appendItems( items: [T]) {
self.items += items
didAppendItems()
}

func didAppendItems() {} // for overriding
}

class AftershipTracking: NSObject {
var identifier: Int
init( identifier: Int) {
self.identifier = identifier
super.init()
}
}

extension AftershipTracking: Equatable { }

func ==( lhs: AftershipTracking, rhs: AftershipTracking) -> Bool {
return lhs.identifier == rhs.identifier
}

class TrackingCache<T: AftershipTracking>: BaseCache<AftershipTracking> {
override func didAppendItems() {
// do something
}
}

let a = TrackingCache<AftershipTracking>()
let b = TrackingCache<AftershipTracking>()

a.appendItems( [AftershipTracking( identifier: 1)])
b.appendItems( [AftershipTracking( identifier: 1)])

let result = a.items == b.items // true

swift - Generic classes with inheritance

In Swift, generics are invariant, e.g. any X<A> will never be assignable to X<B>, regardless of the inheritence relationship between A and B.

Nevertheless, there are some exceptions to this rule, regarding Arrays and Optionals (and mabye some other types):

var array2:[Parent] = [Child]()
// same as:
var array1:Array<Parent> = Array<Child>()

var opt1:Parent? = Child()
// same as:
var opt2:Optional<Parent> = Optional<Child>(Child())

These will compile (since Swift 3) - but these a special cases treated by some hard-coded rules of the the compiler.

Swift generic class, inheritance and covariance

Conceptual Discussion

Your concept for the architecture is flawed and this is leading to your issue.


Simple Generics Example

Here's a very simple example of a generic function, which just returns the value you give it:

func echo <T> (_ value: T) -> T { return value }

Because this function is generic, there is ambiguity about the type that it uses. What is T? Swift is a type-safe language, which means that ultimately there is not allowed to be any ambiguity about type whatsoever. So why is this echo function allowed? The answer is that when I actually use this function somewhere, the ambiguity about the type will be removed. For example:

let myValue = echo(7)      // myValue is now of type Int and has the value 7

In the act of using this generic function I have removed the ambiguity by passing it an Int, and therefore the compiler has no uncertainty about the types involved.


Your Function

func currentPageController <DataSource> (at index: Int) -> BookPageViewController<DataSource>

Your function only uses the generic parameter DataSource in the return type, not in the input - how is the compiler supposed figure out what DataSource is?* I assume this is how you imagined using your function:

let pager = Pager()
let controller = pager.currentPageController(at: 0)

But now, what is the type of controller? What can you expect to be able to do with it? It seems that you're hoping that controller will take on the correct type based on the value that you pass in (0), but this is not how it works. The generic parameter is determined based on the type of the input, not the value of the input. You're hoping that passing in 0 will yield one return type, while 1 will yield a different one - but this is forbidden in Swift. Both 0 and 1 are of type Int, and the type is all that can matter.

As is usually the case with Swift, it is not the language/compiler that is preventing you from doing something. It is that you haven't yet logically formulated what is even is that you want, and the compiler is just informing you of the fact that what you've written so far doesn't make sense.


Solutions

Let's move on to giving you a solution though.


UIViewController Functionality

Presumably there is something that you wanted to use controller for. What is it that you actually need? If you just want to push it onto a navigation controller then you don't need it to be a BookPageViewController. You only need it to be a UIViewController to use that functionality, so your function can become this:

func currentPageController (at index: Int) -> UIViewController {
if index == 0 {
return readingPageController()
}
return starsPageController()
}

And you can push the controller that it returns onto a navigation stack.


Custom Functionality (Non-Generic)

If, however, you need to use some functionality which is specific to a BookPageViewController then it depends what it is you want to do. If there is a method on BookPageViewController like this:

func doSomething (input: Int) -> String

which doesn't make use of the generic parameter DataSource then probably you'll want to separate out that function into its own protocol/superclass which isn't generic. For example:

protocol DoesSomething {
func doSomething (input: Int) -> String
}

and then have BookPageViewController conform to it:

extension BookPageViewController: DoesSomething {
func doSomething (input: Int) -> String {
return "put your implementation here"
}
}

Now the return type of your function can be this non-generic protocol:

func currentPageController (at index: Int) -> DoesSomething {
if index == 0 {
return readingPageController()
}
return starsPageController()
}

and you can use it like this:

let pager = Pager()
let controller = pager.currentPageController(at: 0)
let retrievedValue = controller.doSomething(input: 7)

Of course, if the return type is no longer a UIViewController of any sort then you probably want to consider renaming the function and the related variables.


Custom Functionality (Generic)

The other option is that you can't separate out the functionality you need into a non-generic protocol/superclass because this functionality makes use of the generic parameter DataSource. A basic example is:

extension BookPageViewController {
func setDataSource (_ newValue: DataSource) {
self.dataSource = newValue
}
}

So in this case you really do need the return type of your function to be BookPageViewController<DataSource>. What do you do? Well, if what you really want is to use the setDataSource(_:) method defined above then you must have a DataSource object that you plan to pass in as an argument, right? If this is the case then we're making progress. Previously, you only had some Int value which you were passing into your function and the problem was that you couldn't specify your generic return type with that. But if you already have a BookPageDataSource value then it is at least logically possible for you to use this to specialize your
function.

What you say you want, however, is to just use an Int to get the controller at that index, regardless of what the DataSource type is. But if you don't care what the DataSource is of the returned BookPageViewController then how can you expect to set its DataSource to something else using the setDataSource(_:) method?

You see, the problem solves itself. The only reason you would need the return type of your function to be generic is if the subsequent functionality you need to make use of uses that generic type, but if this is the case then the controller you get back can't have just any old DataSource (you just wanted whichever one corresponds to the index you provide) - you need it to have exactly the type of DataSource which you plan to pass in when you use it, otherwise you're giving it the wrong type.

So the ultimate answer to your question is that, in the way that you were conceiving of it, there is no possible use for the function you were trying to construct. What's very cool about the way Swift is architected is that the compiler is actually able to figure out that logical flaw and prevent you from building your code until you've re-conceptualized it.


Footnote:

* It is possible to have a generic function which only uses the generic parameter in the return type and not in the input, but this won't help you here.

swift subclasses used in generics don't get called when inheriting from NSObject

I was able to confirm your results and submitted it as a bug, https://bugs.swift.org/browse/SR-10617. Turns out this is a known issue! I was informed (by good old Hamish) that I was duplicating https://bugs.swift.org/browse/SR-10285.

In my bug submission, I created a clean compact reduction of your example, suitable for sending to Apple:

protocol P {
init()
func doThing()
}

class Wrapper<T:P> {
func go() {
T().doThing()
}
}

class A : NSObject, P {
required override init() {}
func doThing() {
print("A")
}
}

class B : A {
required override init() {}
override func doThing() {
print("B")
}
}

Wrapper<B>().go()

On Xcode 9.2, we get "B". On Xcode 10.2, we get "A". That alone is enough to warrant a bug report.

In my report I listed three ways to work around the issue, all of which confirm that this is a bug (because none of them should make any difference):

  • make the generic parameterized type's constraint be A instead of P

  • or, mark the protocol P as @objc

  • or, don't have A inherit from NSObject


UPDATE: And it turns out (from Apple's own release notes) there's yet another way:

  • mark A's init as @nonobjc


Related Topics



Leave a reply



Submit