nacho4d
nacho4d

Reputation: 45088

Declare array of classes that conform to a protocol

Lets say I have created this protocol and a couple of classes

import UIKit

protocol ControllerConstructorProtocol {
    class func construct() -> UIViewController?
}

class MyConstructor: ControllerConstructorProtocol {
    class func construct() -> UIViewController? {
        return UIViewController()
    }
}

class MyOtherConstructor: ControllerConstructorProtocol {
    class func construct() -> UIViewController? {
        return UITableViewController(style: .Grouped)
    }
}

Now I want to declare an array that contains classes of objects that will conform to such protocol. How can I declare it? Ideally I would like the compiler to check the array is correctly filled (at compile time) rather than checking it myself at (run time) with the as operator.

This is what I have tried without success :(

  1. This leads to a compile error:

    'Any Object does not have a member named 'construct'

    var array = [
        MyConstructor.self,
        MyOtherConstructor.self,
    ]
    
    var controller = array[0].construct() // << ERROR here
    
  2. Writing this is even worse, since the class itself does not conform to the protocol (their instances do)

    Type 'MyConstructor.Type' does not conform to protocol 'ControllerConstructorProtocol'

    var array: Array<ControllerConstructorProtocol> = [
        MyConstructor.self, // << ERROR here
        MyOtherConstructor.self,
    ]
    

EDIT 2016/04/23: In Swift 2.2 (Xcode 7.3) it is possible to write @rintaro's original idea :)

let array: Array<ControllerConstructorProtocol.Type> = [
    MyConstructor.self,
    MyOtherConstructor.self,
]
let viewController = array[0].construct()

Upvotes: 8

Views: 6194

Answers (4)

Dejan Skledar
Dejan Skledar

Reputation: 11435

Try this:

var myConst = MyConstructor()
var myOthConst = MyOtherConstructor()

var array:[AnyObject] = [
    myConst,
    myOthConst
]

for a in array
{
    if a is MyConstructor
    {
        println("a is type of MyConstructor")
        (a as! MyConstructor).myMethod()
    }
    else if a is MyOtherConstructor
    {
        println("a is type of MyOtherConstructor")
        (a as! MyOtherConstructor).myMethod()
    }
}

Another solution, although not that pretty...

Upvotes: 0

feca
feca

Reputation: 1169

If you really want to sotre classes only (that conforms to protocol) then you can do it the following way (in Swift 3):

If you want to create a new instance with the protocol type you have to add init() to protocol declaration:

protocol SomeProtocol: ConformsToOtherProtocolIfNeeded {
    init(...) { ... }
    func someFunction(...) { ... }
}

class Class1: SomeProtocol {
    init(...) { ... }
    func someFunction(...) { ... }
}

class Class2: SomeProtocol {
    init(...) { ... }
    func someFunction(...) { ... }
}

declare array (as above):

var someProtocols: Array<SomeProtocol.Type> = [
    Class1.self,
    Class2.self,
]

and if you want to use someFunction, you have to create an instance, because the elements in the array not an instance. Example:

for sp in someProtocols {
    let instance = sp.init()
    instance.someFunction()
}

If you want to compare the type of class, you also have to create an instance. (So you cannot use item of array directly (sp).)

Examples:

if type(of: instance) == type(of: otherInstance) { ... }
if instance is SomeProtocol { ... }
if instance is Class1 { ... }

Upvotes: 0

rintaro
rintaro

Reputation: 51911

"array of classes that conform to a protocol" can be declared like Array<TheProtocol.Type>.

You can:

var array: Array<ControllerConstructorProtocol.Type> = [
    MyConstructor.self,
    MyOtherConstructor.self,
]

But...,

    array[0].construct()
//  ^ error: accessing members of protocol type value 'ControllerConstructorProtocol.Type' is unimplemented

Calling method on the item is "unimplemented".

As of now, you have to declare the protocol as @objc, and call the method via AnyClass. Moreover, for some reasons, we cannot directly cast array[0] to AnyClass, instead, we have to cast it to Any, then AnyClass.

@objc protocol ControllerConstructorProtocol {
    class func construct() -> UIViewController?
}

var array: Array<ControllerConstructorProtocol.Type> = [
    MyConstructor.self,
    MyOtherConstructor.self,
]

let vc = (array[0] as Any as AnyClass).construct()

Note: Casting problem was fixed in Swift 1.2 / Xcode 6.3. But "unimplemented" is "unimplmented" :(


Just random ideas:

It's depends on your actual use-case, but in this particular case, array of ()-> UIViewController? closures is sufficient:

var array: [() -> UIViewController?] = [
    MyConstructor.construct,
    MyOtherConstructor.construct,
]

let vc = array[0]()

If you have several methods, you might want to use type-erased wrapper of the protocol.

protocol ControllerConstructorProtocol {
    class func construct() -> UIViewController?
    class func whoami() -> String
}

struct ControllerConstructorWrapper {
    private let _construct: () -> UIViewController?
    private let _whoami: () -> String
    init<T: ControllerConstructorProtocol>(_ t:T.Type) {
        _construct = { t.construct() }
        _whoami = { t.whoami() }
    }
    func construct() -> UIViewController? { return _construct() }
    func whoami() -> String { return _whoami() }
}

var array: [ControllerConstructorWrapper] = [
    ControllerConstructorWrapper(MyConstructor),
    ControllerConstructorWrapper(MyOtherConstructor),
]

let who = array[0].whoami()
let vc = array[0].construct()

Upvotes: 11

qwerty_so
qwerty_so

Reputation: 36295

Not sure if I got your question completely, but why not this way:

var array: [ControllerConstructorProtocol] = [MyConstructor(), MyOtherConstructor()]

Upvotes: 0

Related Questions