joakim
joakim

Reputation: 4061

Passing Protocol Type as parameter in Swift

In objective-C we can (by importing the language's runtime header file) do the following:

//Pass a service (usually an object) and ANY protocol
- (void)registerService:(id)service forProtocol:(Protocol *)protocol
{
    //Grab the protocol's name (that's why we import runtime.h, it contains the protocol_getname mehod)
    NSString *protocolName = [NSString stringWithUTF8String:protocol_getName(protocol)];

    //If the object we passed does not conform to the protocol, inform and break
    if (![service conformsToProtocol:protocol])
    {
        NSLog(@"Service: %@ does not conform to protocol: %@", service, protocolName);
        return;
    }

    //Else add service in a collection (array, dictionary) for later use
    self.services[protocolName] = service;
}

I use this in obj-C as a "poor man's IOC container", a simple registry used for injecting dependencies.

//The interested party uses this method to obtain the dependency it needs by asking for the object that is registered as responsible for conforming to the Protocol parameter
- (id)serviceForProtocol:(Protocol *)protocol
{
    id result;

    NSString *protocolName = [NSString stringWithUTF8String:protocol_getName(protocol)];

    //Look for the service that conforms to the protocol in the registry dictionary,
    result = self.services[protocolName];

    //if there is no object meeting the criteria, inform/alert
    if (result == nil)
    {
        NSLog(@"No class registered for protocol: %@", protocolName);
    }

    //and return the result
    return result;
}

Trying to replicate this behaviour in Swift, I found out that we don't have access to the language's equivalent "runtime" API like we do in obj-C (yet), and understandably so since swift is a work in progress and giving people this kind of access is undoubtedly risky.

But this also means that we can't use Protocol in the same way anymore i.e. in the sense of ANY protocol.

The first possible workaround that comes to mind is a mix of generics, Any and where, but that feels too much for something that used to be simple.

So, my question is: What are some proposed solutions for passing around Protocol (as in ANY Protocol) in Swift?

EDIT: I had some success using the Metatype Type introduced in Swift which makes sense in terms of language design but also does not (yet) provide the ability to provide a "String" representation of a Metatype that could be used as a key in a dictionary.

This is of course a feature that could be added as the language matures.

Upvotes: 7

Views: 3157

Answers (2)

macabeus
macabeus

Reputation: 4582

I tried for a long time, with many different ways (pointers, Protocol), and the only solution without @objc and pure Swift that I found is with closure. Then, you need use a closure to use a protocol and return a value

protocol Proto { }
protocol Proto2 { }
class Foo: Proto { }
class Bar: Proto, Proto2 { }
class Baz: Proto2 { }
class Qux { }

func printConforms(classList: [AnyClass], protoCond: (AnyClass) -> Any?) {
    for i in classList {
        print(i, terminator: " -> ")
        if protoCond(i) != nil {
            print("is subscriber")
        } else {
            print("NOT IS subscriber")
        }
    }
}

let myClasses: [AnyClass] = [Foo.self, Bar.self, Baz.self, Qux.self]
printConforms(classList: myClasses, protoCond: { $0 as? Proto.Type })

More complete example: https://gist.github.com/brunomacabeusbr/eea343bb9119b96eed3393e41dcda0c9

Edit

Another better solution is using generics, for example:

protocol Proto { }
class Foo: Proto { }
class Bar: Proto { }
class Baz { }

func filter<T>(classes: [AnyClass], byConformanceTo: T.Type) -> [AnyClass] {
    return classes.filter { $0 is T }
}

filter(classes: [Foo.self, Bar.self, Baz.self], byConformanceTo: Proto.Type.self)
// return [Foo.self, Bar.self]

Upvotes: 0

newacct
newacct

Reputation: 122449

What have you tried?

Does something like this not work:

import Foundation

func registerService(service: NSObjectProtocol, forProtocol prot: Protocol) {
  let protocolName = NSStringFromProtocol(prot)

  if (!service.conformsToProtocol(prot)) {
    println("Service: \(service) does not conform to protocol: \(protocolName)")
    return
  }

  //...
}

Upvotes: 1

Related Questions