OgreSwamp
OgreSwamp

Reputation: 4692

Making existing Objective C class conform to swift protocol via extensions

I have 2 versions of the same model in the project (and I can't get rid of the legacy one). It is Customer (legacy code) and struct CustomerModel - modern Swift implementation of the model.

I have a custom UITableViewCell which used to have setup(withCustomer: CustomerModel) method. It worked well for a new model, but now I need to use legacy one to setup same cells.

I decided to define CustomerDisplayable protocol and make both models conform it.

Here is the code:

Customer.h

@interface Customer : NSObject

@property (nonatomic, strong) NSString* name;
@property (nonatomic, strong) NSString* details;

@end

CustomerModel.swift

struct CustomerModel {

    let name: String
    let details: String?

    init(withJSON json: [String: Any]) {
        name = json["name"] as! String
        details = json["details"] as? String
    }
}

CustomerDisplayable.swift

protocol CustomerDisplayable {

    var name: String { get }
    var details: String? { get }
    var reviewCount: Int { get }
    var reviewRating: Double { get }

}

extension Customer: CustomerDisplayable {
    var reviewCount: Int { return 100 }
    var reviewRating: Double { return 4.5 }
}

extension CustomerModel: CustomerDisplayable {
    var reviewCount: Int { return 100 }
    var reviewRating: Double { return 4.5 }
}

I expected that as Customer.h has already properties name & details - it will conform this protocol and extension above will work. But I get a compiling error in my extension: Type 'Customer' does not conform to protocol 'CustomerDisplayable'.

Xcode offers a quick fix - Protocol requires property 'name' with type 'String'; do you want to add a stub. If I agree Xcode add stubs I end up with name and details computable getters but Xcode shows new compile errors:

extension Customer: CustomerDisplayable {
    var details: String? {
        return "test"
    }

    var name: String {
        return "test"
    }

    var reviewCount: Int { return 100 }
    var reviewRating: Double { return 4.5 }
}

Any ideas how to solve this problem? I really want to have this protocol and abstract interface for both model representations. The only solution I came to is to rename properties in CustomerDisplayable

NOTE: Real models are much more complex, but this code is demonstrating the problem.

Upvotes: 3

Views: 1147

Answers (2)

Charles Srstka
Charles Srstka

Reputation: 17050

Hmm, this looks, IMO, like something that probably ought to be considered a bug in the Swift compiler. I'd suggest filing a report at http://bugs.swift.org .

Here's what appears to be going on:

  1. As you've noticed, Swift doesn't seem to notice when an Objective-C selector fulfills a retroactive protocol requirement, which is the part I'd probably file as a bug.

  2. When you explicitly try to add name and details properties to your extension, Swift 3 notices that the extension is on an NSObject subclass and automatically exposes the properties to Objective-C. Objective-C, of course, can't have two methods with the same selector, so you get the error you've been seeing. Swift 4 doesn't automatically expose everything to Objective-C anymore, so you won't get this error there, and in Swift 3 you can work around this by adding the @nonobjc keyword. But then:

  3. Once you do add the property in an extension, it shadows the original Objective-C property, making it hard to get at the correct value to return in the property.

Unfortunately, I can't think of a clean workaround, although I can think of an ugly, hacky one involving the Objective-C runtime. My favorite part is the way we have to use string-based NSSelectorFromString since #selector will choke from the presence of the @nonobjc shadowed property. But it works:

extension Customer: CustomerDisplayable {
    @nonobjc var details: String? {
        return self.perform(NSSelectorFromString("details")).takeUnretainedValue() as? String
    }

    @nonobjc var name: String {
        return self.perform(NSSelectorFromString("name")).takeUnretainedValue() as? String ?? ""
    }
}

Again, I'd recommend filing a bug report so that we don't have to do crap like this in the future.

EDIT: Never mind all this! I'm wrong. Disregard everything I said. The only reason the retroactive protocol didn't work was because you didn't have nullability specifiers in your Objective-C class, so Swift didn't know whether they could be nil or not and thus interpreted types as String!. Should have noticed that, d'oh d'oh d'oh. Anyway, I don't know if you're able to edit the original Objective-C class's definition to add nullability specifiers, but if you can, the original protocol will work fine retroactively with no hacks.

@interface Customer : NSObject

@property (nonatomic, nonnull, strong) NSString* name;
@property (nonatomic, nullable, strong) NSString* details;

- (nonnull instancetype)initWithName:(nonnull NSString *)name details: (nonnull NSString *)details;

@end

Upvotes: 2

BaseZen
BaseZen

Reputation: 8718

NSString in Objective-C is different than the native Swift value type String. Conversion rules I find are too obscure to memorize. In this case it appears the bridging brings in NSString * as: ImplicitlyUnwrappedOptional<String>

So if you don't mind the legacy Customer type dictating some aspects of your Swift code, change types of name and details to String! everywhere and all is well.

Otherwise, you'll have to change your protocol to have new names, say displayName and displayDetails, and reference the underlying properties. Presumably you have the freedom to do this and you achieve what is perhaps an important abstraction layer anyway. Then just dutifully list all the obvious implementations of all four properties in each extension block. Because the conversion from String! to String or String? is automatic, the code looks a bit trivial and bloated, but it also works fine.

Upvotes: 0

Related Questions