How to access an internal Swift class in Objective-C within the same framework?
Asked Answered
S

4

20

Working on a mixed framework. imported inside the Obj-C file but the internal classes are not visible, only the public ones.

The documentation clearly states the internal clasees should be available between Swift and Obj-C:

Importing Swift into Objective-C
To import a set of Swift files in the same framework target as your Objective-C code, you don’t need to import anything into the umbrella header for the framework. Instead, import the Xcode-generated header file for your Swift code into any Objective-C .m file you want to use your Swift code from. Because the generated header for a framework target is part of the framework’s public interface, only declarations marked with the public modifier appear in the generated header for a framework target. You can still use Swift methods and properties that are marked with the internal modifier from within the Objective-C part of your framework, as long they are declared within a class that inherits from an Objective-C class. For more information on access-level modifiers, see Access Control in The Swift Programming Language (Swift 2).

Code Sample (Create a new project with a framework)

// SwiftObject.swift

public class SwiftObject: NSObject {
    public class func doSomething() {}
}

internal class YetAnotherSwiftObject: NSObject {
    internal class func doSomething() {}
}

// SomeObject.m file

@implementation SomeObject

- (void)someMethod {
    [SwiftObject doSomething];
}

- (void)someOtherMethod {
    [YetAnotherSwiftObject doSomething]; // Use of undeclared identifier
}

@end
Stein answered 21/9, 2015 at 16:23 Comment(1)
The current docs developer.apple.com/documentation/swift/… say Methods and properties that are marked with the internal modifier and declared within a class that inherits from an Objective-C class are accessible to the Objective-C runtime. However, they're inaccessible at compile time and don't appear in the generated header for a framework target.Biltong
P
29

As indicated in the docs, declarations marked with internal modifier don't appear in the generated header, so the compiler does not know about them and thus complaints. Of course, you could send messages using performSelector approach, but that's not convenient and bug-prone. We just need to help the compiler know that those declarations are there.

First, we need to use @objc attribute variant that allows you to specify name for your symbol in Objective-C:

// SwiftObject.swift

@objc(SWIFTYetAnotherSwiftObject)
internal class YetAnotherSwiftObject: NSObject {
    internal class func doSomething() {}
}

And then you just need to create @interface declaration with the methods you want to use in your code - so the compiler will be happy, and also apply SWIFT_CLASS macro with the symbol name you've specified earlier - so the linker would pick the actual implementation:

// SomeObject.m file

SWIFT_CLASS("SWIFTYetAnotherSwiftObject")
@interface YetAnotherSwiftObject : NSObject

+ (void)doSomething;

@end


@implementation SomeObject

- (void)someOtherMethod {
    [YetAnotherSwiftObject doSomething]; // Should work now !!!
}

@end
  • I've used the interface declaration in .m file just for clarity, the better option would be to combine such declarations in .h file, and include it.
  • By declaring methods in that interface we're making a promise to compiler, and it won't complain if you'll put there a method that does not exist (or with wrong signature, etc.) Obviously, you'll crash in runtime in that case - so be cautious.
Plank answered 15/10, 2015 at 23:5 Comment(4)
Nice! I did not know about that macro and google doesn't show much. Do you have any reference from the Apple docs?Stein
Nice? This is horribly verbose and messy. Is this really what you need to do? I have run into the same problem, and even apple should have managed to do something better than this. I'm going to give up on this and go back to objective c, this is unreasonable! +1 for posting something that actually works (after hours of searching).Intrigante
According to Apple documentation "You can still use Swift methods and properties that are marked with the internal modifier from within the Objective-C part of your framework, as long they are declared within a class that inherits from an Objective-C class", if the swift class derives from another objc class it should be available in objc code. I have similar problem and I have marked my swift class with @objc and it inherits from NSObject but still not available in objc code.Lenna
I consider the answer somewhat flawed because it requires to use SWIFT_CLASS macro, which is generated as part of {TargetName}-Swift.h header with all other burden of publicly generated headers. You don't actually need this macro if the @objc attribute in Swift code matches the Objective-C class name (@objc(YetAnotherSwiftObject) in this case). But be advised, that Objective-C headers still have naming clash issues, so you better prefix the name of the interface somehowUnderproof
J
5

For me it just worked by checking: "Allow app extension API only". You find it by going to the project setting, select your target and then it is in the General tab under Deployment Info.

Can someone explain to me, why this does solve the problem?

Jemmie answered 19/5, 2016 at 13:38 Comment(3)
I love you ! LOL . That's the only place on earth that found the answer to bypass this stupid limitation. No framework should be required to expose its Swift classes to API consumers just to be able to interop with internal ObjC code!Loose
Be careful of trying this option! It will list the class & methods in auto generated header. Use it only if you don't care about keeping those private.Lillianalillie
But with this checked, there will be a bunch of Cocoa API missing.Unbiased
J
0

While the above solution works (https://mcmap.net/q/618225/-how-to-access-an-internal-swift-class-in-objective-c-within-the-same-framework), it seems overly complicated and unintuitive:

  • Complicated, because it seems to add more things than necessary – I will provide a smoother solution below.
  • Unintuitive, because the objc macro SWIFT_CLASS resolves to SWIFT_RUNTIME_NAME, and the provided value is not actually the runtime name – nor is the objc class name in the header matching the Swift attribute param in @objc. Still, surprisingly, the solution works – but to me it is not clear why.

Here is what we have tested in our own project, and believe to be the better solution (using the example above):

// YetAnotherSwiftObject.swift

@objc(OBJCPREFIXYetAnotherSwiftObject)
internal class YetAnotherSwiftObject: NSObject {
    @objc internal class func doSomething() {}
}
// OBJCPREFIXYetAnotherSwiftObject.h

@interface OBJCPREFIXYetAnotherSwiftObject : NSObject

+ (void)doSomething;

@end

That's it. The interface looks like a regular objc interface. This gives the added benefit that you can include it in other header files (which you cannot do if you use the SWIFT_CLASS macro, as it comes from the autogenerated Swift header file, which in turn you cannot include in an objc header, due to circular dependency).

On the Swift side, the only thing relevant is that you provide the class with the proper objc name. Mind that I only used the name prefix for language consistency – you can even just use YetAnotherSwiftObject everywhere (i.e., in the objc header and in the @objc attribute in Swift – but you need to keep this attribute with explicit naming in any case, and need to keep it consistent with the class name in the header).

This also makes your life easier if you're in the process of converting your objc framework step by step to Swift. You just keep the objc header as before, and now provide the implementation in Swift.

Japeth answered 19/1, 2021 at 15:23 Comment(0)
A
0

Methods and properties that are marked with the internal modifier and declared within a class that inherits from an Objective-C class are accessible to the Objective-C runtime.

so let's make use of that:

class MyInternalClass: NSObject {
    @objc var internalProperty = 42
}
@interface MyPublicClass()
@end

@implementation MyPublicClass

+ (void) printValue {

    Class myInternalClass = NSClassFromString(@"MyPackageNameIfAny.MyInternalClass");
    id myInternalClassInstance = [myInternalClass new];
    int value = [myInternalClassInstance performSelector:@selector(internalProperty)];

    NSLog(@"Value is %d ", value); // "value is 42"
}

@end

Using the SWIFT_CLASS macro and @objc class attribute could easily lead to errors when archiving. This approach is safer.

Arteaga answered 4/10, 2021 at 11:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.