Reputation: 4692
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
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:
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.
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:
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
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