Can't Create an Array of Types Conforming to a Protocol in Swift

Can't create an Array of types conforming to a Protocol in Swift

Let's say, if we could put an instance of Thing into array foos, what will happen?

protocol Foo {
associatedtype BazType
func bar(x:BazType) -> BazType
}

class Thing: Foo {
func bar(x: Int) -> Int {
return x.successor()
}
}

class AnotherThing: Foo {
func bar(x: String) -> String {
return x
}
}

var foos: [Foo] = [Thing()]

Because AnotherThing conforms to Foo too, so we can put it into foos also.

foos.append(AnotherThing())

Now we grab a foo from foos randomly.

let foo = foos[Int(arc4random_uniform(UInt32(foos.count - 1)))]

and I'm going to call method bar, can you tell me that I should send a string or an integer to bar?

foo.bar("foo") or foo.bar(1)

Swift can't.

So it can only be used as a generic constraint.

What scenario requires a protocol like this?

Example:

class MyClass<T: Foo> {
let fooThing: T?

init(fooThing: T? = nil) {
self.fooThing = fooThing
}

func myMethod() {
let thing = fooThing as? Thing // ok
thing?.bar(1) // fine

let anotherThing = fooThing as? AnotherThing // no problem
anotherThing?.bar("foo") // you can do it

// but you can't downcast it to types which doesn't conform to Foo
let string = fooThing as? String // this is an error
}
}

Swift array of struct types conforming to protocol

I found the problem. My protocol was inheriting from RawOptionSetType. Not sure why that caused an issue, but commenting that inheritance out made it work. Weird.

Can't create array of views conforming to same protocol?

Here is possible approach - the main thing is that your protocol type should not contain generics.

Tested with Xcode 12.1 / iOS 14.1

demo

Note: original style preserved intentionally

protocol viewTest {                 // << here !!
var name: String {get set}
func getView() -> AnyView // << here !!
}

extension viewTest where Self: View {
func getView() -> AnyView {
AnyView(self)
}
}

struct v1: View, viewTest {
var name: String = "v1"
var body: some View {
Text("Name: \(name)")
}
}

struct v2: View, viewTest {
var name: String = "v2"
var body: some View {
Text("2nd view name: \(name)")
}
}

struct testView: View{
var arr: [viewTest] // << here !!
@State private var index: Int = 0

var body: some View {
VStack {
if(index >= 0) {
self.arr[index].getView() // << here !!

Button("Next", action: {
if(index < arr.count-1) {
index += 1
}
})
} else {
Text("no views")
}
}
}
}

struct testView_Previews: PreviewProvider {
static var previews: some View {
testView(arr: [
v1(),
v2()
])
}
}

How would I use an array of generics with a type parameter conforming to a protocol?

To see what's wrong with your scenario, forget about trying to declare the type of this array, and try to actually make such an array out of actual objects:

protocol MyProtocol {}
struct MyStruct<T: MyProtocol> {
let myProp: T
}
struct S1 : MyProtocol {}
struct S2 : MyProtocol {}
let myStruct1 = MyStruct(myProp: S1())
let myStruct2 = MyStruct(myProp: S2())
let array = [myStruct1, myStruct2] // error

The compiler kicks back: "heterogeneous collection literal could only be inferred to '[Any]'". And that sums it up. The types of myStruct1 and myStruct2 have nothing in common, so you cannot make an array of them.

That is why you are not able to declare the array to be of a type that will embrace both of them. There is no such type. The types of myStruct1 and myStruct2, namely MyStruct<S1> and MyStruct<S2>, are unrelated.

I know that they look related, because the word "MyProtocol" in the original declaration appears to provide some sort commonality. But the word "MyProtocol" does not designate a type; it designates a constraint on the actual type, saying that whatever this one type is, it must be an adopter of MyProtocol. S1 and S2 are two different types, and so MyStruct<S1> and MyStruct<S2> are two different types. You can't put them together in an array. The fact that both S1 and S2 happen to adopt MyProtocol is irrelevant.

Part of the difficulty may be that you think that two generic types are somehow related because their parameterized types are related. That is not the case. The classic example is a class and its subclass:

class Cat {}
class Kitten: Cat {}
struct Animal<T: Cat> {}
let cat = Animal<Cat>()
let kitten = Animal<Kitten>()
let array2 = [cat, kitten] // error

We get the same compile error. Again, you might imagine that you can put cat and kitten together in an array because Kitten is a subclass of Cat. But that is not true. Animal<Cat> and Animal<Kitten> are unrelated types.

Array of protocol type

What you need to do is to use type erasure, much like AnyHashable does in the Swift Standard Library.

You can't do:

var a: [Hashable] = [5, "Yo"]
// error: protocol 'Hashable' can only be used as a generic constraint because it has Self or associated type requirements

What you have to do is to use the type-erased type AnyHashable:

var a: [AnyHashable] = [AnyHashable(5), AnyHashable("Yo")]
a[0].hashValue // => shows 5 in a playground

So your solution would be to first split the protocol in smaller parts and promote Equatable to Hashable (to reuse AnyHashable)

protocol Conditionable {
var condition: Condition? { get set }
}

protocol Executable {
func execute() -> SKAction
}

protocol Commandable: Hashable, Executable, Conditionable {}

Then create an AnyCommandable struct, like this:

struct AnyCommandable: Commandable, Equatable {
var exeBase: Executable
var condBase: Conditionable
var eqBase: AnyHashable

init<T: Commandable>(_ commandable: T) where T : Equatable {
self.condBase = commandable
self.exeBase = commandable
self.eqBase = AnyHashable(commandable)
}

var condition: Condition? {
get {
return condBase.condition
}
set {
condBase.condition = condition
}
}

var hashValue: Int {
return eqBase.hashValue
}

func execute() -> SKAction {
return exeBase.execute()
}

public static func ==(lhs: AnyCommandable, rhs: AnyCommandable) -> Bool {
return lhs.eqBase == rhs.eqBase
}
}

And then you can use it like this:

var a = FunctionCommand()
a.commands = [AnyCommandable(MoveCommand()), AnyCommandable(FunctionCommand())]

And you can easily access properties of commands, because AnyCommandable implements Commandable

a.commands[0].condition

You need to remember to now add Hashable and Equatable to all your commands.
I used those implementations for testing:

struct MoveCommand: Commandable {

var movingVector: CGVector!

var condition: Condition?
func execute() -> SKAction {
return SKAction()
}

var hashValue: Int {
return Int(movingVector.dx) * Int(movingVector.dy)
}

public static func ==(lhs: MoveCommand, rhs: MoveCommand) -> Bool {
return lhs.movingVector == rhs.movingVector
}
}

struct FunctionCommand: Commandable {
var commands = [AnyCommandable]()

var condition: Condition?

func execute() -> SKAction {
return SKAction.group(commands.map { $0.execute() })
}

var hashValue: Int {
return commands.count
}

public static func ==(lhs: FunctionCommand, rhs: FunctionCommand) -> Bool {
return lhs.commands == rhs.commands
}
}

Array of enum types conforming to a protocol with an associated type

I want to display a menu listing all the possible options.

So you want Strings, not the cases themselves. That's absolutely doable. First, start by saying what you really want the type to do in the form of a protocol:

protocol CaseNamed {
static var caseNames: [String]
}

If you had that, you could build what you want:

var enums: [CaseNamed.Type] = [A.self, B.self]
enums.flatMap { $0.caseNames }

(I call this "wish driven development." I wish I had a type that could....)

Now you just need to conform types to CaseNamed by implementing caseNames. Luckily that's easy if the type also happens to conform to CaseIterable:

extension CaseNamed where Self: CaseIterable {
static var caseNames: [String] {
self.allCases.map { "\($0)" }
}
}

But you can have CaseNamed types that don't conform to CaseIterable. CaseIterable isn't a requirement. It's just nice if you have it. Here's the full code:

protocol CaseNamed {
static var caseNames: [String] { get }
}

enum A: String, CaseIterable, CaseNamed {
case a1, a2, a3
}

enum B: String, CaseIterable, CaseNamed {
case b1, b2, b3
}

extension CaseNamed where Self: CaseIterable {
static var caseNames: [String] {
self.allCases.map { "\($0)" }
}
}

var enums: [CaseNamed.Type] = [A.self, B.self]

enums.flatMap { $0.caseNames }

Now of course you might also want this CaseNamed protocol to do other things, so you can add those other things. But you need to think in terms of the calling code and what it fundamentally needs to do its job.

How to define an array of objects conforming to a protocol?

The problem is about using the generics counterpart for protocols, type aliases.
It sounds weird, but if you define a type alias, you cannot use the protocol as a type, which means you cannot declare a variable of that protocol type, a function parameter, etc. And you cannot use it as the generic object of an array.

As the error say, the only usage you can make of it is as a generic constraint (like in class Test<T:ProtocolWithAlias>).

To prove that, just remove the typealias from your protocol (note, this is just to prove, it's not a solution):

protocol MyProtocol {
var abc: Int { get }
}

and modify the rest of your sample code accordingly:

class XYZ: MyProtocol {
var abc: Int { return 32 }
}

var list = [MyProtocol]()

You'll notice that it works.

You are probably more interested in how to solve this problem. I can't think of any elegant solution, just the following 2:

  • remove the typealias from the protocol and replace T with AnyObject (ugly solution!!)
  • turn the protocol into a class (but that's not a solution that works in all cases)

but as you may argue, I don't like any of them. The only suggestion I can provide is to rethink of your design and figure out if you can use a different way (i.e. not using typealiased protocol) to achieve the same result.



Related Topics



Leave a reply



Submit