nacho4d nacho4d - 7 months ago 16
Swift Question

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()

Answer

"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()
Comments