5

I've seen lots of questions regarding implementing Obj-C protocols in Swift, but not so much the other way around, and I haven't seen this specifically.

I am using a mixed Obj-C / Swift codebase. I have a Swift protocol defined as follows:

NamedItem.swift

@objc protocol NamedItem {
    var name: String { get }
}

I have an existing Objective-C class that currently has its own name property:

MyObjcClass.h

@interface MyObjcClass : NSObject
@property (nonatomic, strong, readonly) NSString* name;
@end

I have a couple other classes that have a name property, so obviously I'd like to associate them all with a protocol instead of typecasting to a bunch of different types. Now, if I try to switch my Obj-C class from having its own property to implementing the Swift protocol:

MyObjcClass.h

@protocol MyObjcProtocol
@property (nonatomic, strong, readonly) NSString* place;
@end

@interface MyObjcClass : NSObject
@end

MyObjcClass.m

@interface MyObjcClass () <NamedItem>
@end

@implementation MyObjcClass
@synthesize name = _name;
@synthesize place = _place;
@end

This works great, in my other Objective-C classes, but if I try to access the name property from a Swift class:

SomeSwiftClass.swift

let myObj = MyObjcClass()
myObj.name // error
myObj.place // no problem

I get the following error:

Value of type 'MyObjcClass' has no member 'name'

If I don't remove the existing @property declaration from MyObjcClass.h and omit the @synthesize statement everything builds correctly. Which seems weird and wrong - If you adopt a Objc-C protocol from an Obj-C class you don't have to re-define the property, just the @synthesize statement is sufficient.

I've tried defining the Swift protocol about every way I could think of and have been able to find suggestions about, but I haven't been able to get around this.

So, what is the correct way to implement a Swift protocol property (maybe specifically a read-only property?) in an Objective-C class such that another Swift class can access it? Do I really have to re-declare the property in the Obj-C header? I know I could always give it up and just define the protocol in Objective-C, but... Swift is the future! (or something like that...)

jscs
  • 63,694
  • 13
  • 151
  • 195
Jordan
  • 4,133
  • 1
  • 27
  • 43

2 Answers2

3
// MyObjcClass.m

@interface MyObjcClass () <NamedItem>
@end

This declaration isn't visible to Swift. Move the protocol conformance clause <NamedItem> to the class's header file.

// MyObjCClass.h

// Required for visibility of the protocol
#import "MyProject-Swift.h"

@interface MyObjcClass : NSObject <NamedItem>
@end

As you commented, the Swift compiler still says that myObj does not have a property name. This is quite strange. Note that

let myObj = MyObjcClass() as NamedItem
myObj.name

compiles fine.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • Nope. I've tried that and unfortunately that does not change anything. All that gets you is a clang warning "Cannot find protocol definition fro 'MySwiftProtocol'", and if you #import the "-Swift.h" (which you shouldn't do in a header), the warning goes away but the original error remains. – Jordan Jan 23 '19 at 19:50
  • Import the MyProject-Swift.h header into the class header. – jscs Jan 23 '19 at 19:51
  • _"which you shouldn't do in a header"_ There's no reason not to do this. – jscs Jan 23 '19 at 19:57
  • Very odd behavior. – jscs Jan 23 '19 at 20:00
  • The reason not to import a Module-Swift.h in an Obj-C header is to avoid cyclical references. Instead, you should forward-declare the protocol using `@protocol MySwiftProtocol`. That, however, doesn't change anything either. See "Include Swift Classes in Objective-C Headers Using Forward Declarations" here https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_swift_into_objective-c – Jordan Jan 23 '19 at 20:14
  • You can't use a forward declaration when you're declaring protocol conformance. – jscs Jan 23 '19 at 20:18
  • 2
    OK, found a solution. Your `MyObjcClass() as NamedItem` was the key clue. So, firstly, you technically CAN forward declare protocol for conformance (it's a warning not an error), but I certainly don't like that. I found this post (https://cjwirth.com/tech/circular-references-swift-objc). The author offers a clever (not perfect) solution near the bottom that I went with. – Jordan Jan 23 '19 at 20:33
  • 1
    _"still feels like a Swift bug"_ Agreed. – jscs Jan 23 '19 at 20:37
  • 1
    You should feel free to post your full solution as its own answer, BTW. – jscs Jan 23 '19 at 20:38
2

Ok, managed to get a working solution with some suggestions from JoshCaswell and this blog post.

Classes now look as follows:

NamedItem.swift

@objc protocol NamedObject {
    var name: String { get }
}

MyObjcClass.h

@protocol NamedItem;

@interface MyObjcClass : NSObject
- (id<NamedItem>)asNamedItem;
@end

MyObjcClass.m

@interface MyObjcClass () <NamedItem>
@synthesize name = _name;

- (id<NamedItem>)asNamedItem
{
    return self;
}
@end

Usage now looks like:

SomeSwiftClass.swift

let myObj = MyObjcClass()
myObj.asNamedItem.name

Not as squeaky clean as I'd like, but it's better than a bunch of warnings or admitting defeat and re-writing the protocol in Objective-C. (I dunno, maybe it's not... but it's what I went with).

Hope that helps someone else.

Jordan
  • 4,133
  • 1
  • 27
  • 43
  • Interesting, thanks for posting what you came up with! (Don't hesitate to also mark it as the accepted answer, if you like.) – jscs Jan 23 '19 at 20:56