Home > database >  Swift init from unknown class which conforms to protocol
Swift init from unknown class which conforms to protocol

Time:12-29

I'm currently working on updating a large project from Objective-C to Swift and I'm stumped on how to mimic some logic. Basically we have a class with a protocol which defines a couple functions to turn any class into a JSON representation of itself.

That protocol looks like this:

#define kJsonSupport @"_jsonCompatible"
#define kJsonClass @"_jsonClass"

@protocol JsonProtocol <NSObject>

- (NSDictionary*)convertToJSON;
- (id)initWithJSON:(NSDictionary* json);

@end

I've adapted that to Swift like this

let JSON_SUPPORT = "_jsonCompatible"
let JSON_CLASS = "_jsonClass"

protocol JsonProtocol
{
    func convertToJSON() -> NSDictionary
    init(json: NSDictionary)
}

One of the functions in the ObjC class runs the convertToJSON function for each object in an NSDictionary which conforms to the protocol, and another does the reverse, creating an instance of the object with the init function. The output dictionary also contains two keys, one denoting that the dictionary in question supports this protocol (kJsonSupport: BOOL), and another containing the NSString representation of the class the object was converted from (kJsonClass: NSString). The reverse function then uses both of these to determine what class the object was converted from to init a new instance from the given dictionary.

All of the classes are anonymous to the function itself. All we know is each class conforms to the protocol, so we can call our custom init function on it.

Here's what it looks like in ObjC:

Class rootClass = NSClassFromString(obj[kJsonClass]);
if([rootClass conformsToProtocol:@protocol(JsonProtocol)])
{
    Class<JsonProtocol> jsonableClass = (Class<JsonProtocol>)rootClass;
    [arr addObject:[[((Class)jsonableClass) alloc] initWithJSON:obj]];
}

However, I'm not sure how to make this behavior in Swift.

Here's my best attempt. I used Swiftify to try and help me get there, but the compiler isn't happy with it either:

let rootClass : AnyClass? = NSClassFromString(obj[JSON_CLASS] as! String)
if let _rootJsonClass = rootClass as? JsonProtocol
{
    weak var jsonClass = _rootJsonClass as? AnyClass & JsonProtocol
    arr.add(jsonClass.init(json: obj))
}

I get several errors on both the weak var line and the arr.add line, such as:

Non-protocol, non-class type 'AnyClass' (aka 'AnyObject.Type') cannot be used within a protocol-constrained type

'init' is a member of the type; use 'type(of: ...)' to initialize a new object of the same dynamic type

Argument type 'NSDictionary' does not conform to expected type 'JsonProtocol'

Extraneous argument label 'json:' in call

Is there any way for me to instantiate from an unknown class which conforms to a protocol using a custom protocol init function?

CodePudding user response:

You will likely want to rethink this code in the future, to follow more Swift-like patterns, but it's not that complicated to convert, and I'm sure you have a lot of existing code that relies on behaving the same way.

The most important thing is that all the objects must be @objc classes. They can't be structs, and they must subclass from NSObject. This is the major reason you'd want to change this to a more Swifty solution based on Codable.

You also need to explicitly name you types. Swift adds the module name to its type names, which tends to break this kind of dynamic system. If you had a type Person, you would want to declare it:

@objc(Person)  // <=== This is the important part
class Person: NSObject {
    required init(json: NSDictionary) { ... }
}

extension Person: JsonProtocol {
    func convertToJSON() -> NSDictionary { ... }
}

This makes sure the name of the class is Person (like it would be in ObjC) and not MyGreatApp.Person (which is what it normally would be in Swift).

With that, in Swift, this code would be written this way:

if let className = obj[JSON_CLASS] as? String,
   let jsonClass = NSClassFromString(className) as? JsonProtocol.Type {
    arr.add(jsonClass.init(json: obj))
}

The key piece you were missing is as? JsonProtocol.Type. That's serving a similar function to conformsToProtocol: plus the cast. The .Type indicates that this is a metatype check on Person.self rather than a normal type check on Person. For more on that see Metatype Type in the Swift Language Reference.

Note that the original ObjC code is a bit dangerous. The -initWithJSON must return an object. It cannot return nil, or this code will crash at the addObject call. That means that implementing JsonProtocol requires that the object construct something even if the JSON it is passed is invalid. Swift will enforce this, but ObjC does not, so you should think carefully about what should happen if the input is corrupted. I would be very tempted to change the init to an failable or throwing initializer if you can make that work with your current code.

I also suggest replacing NSDictionary and NSArray with Dictionary and Array. That should be fairly straightforward without redesigning your code.

  • Related