skip to Main Content

I have a Framework with mixed Objective-C and Swift classes and i’d like to communicate between them keeping the Swift interface internal (without making Swift classes and methods public). Is there a way to achieve that?

P.S. Please pay attention that since it’s a Framework project, it’s not an option to use the bridging header solution, because it’s not supported for frameworks.

3

Answers


  1. A solution, although hacky, is that you could mark the private swift methods with an Objective-C decorator @objc, and then call them from Objective-C using performSelector.

    Login or Signup to reply.
  2. If you don’t want to make your Objective-C headers public to use in Swift, you have to create a module.map file at the root of your project and add all your internal/private headers:

    module FrameWorkProjectName_Internal {
        // import your private headers here
        export *
    }
    

    Then, in your SWIFT_INCLUDE_PATHS build settings add $(SRCROOT)/FrameWorkProjectName so that this module can be found by Swift classes.

    To use the Objective-C code of the said module you have to import FrameWorkProjectName_Internal in your Swift file.

    To use internal Swift classes in your Objective-C code, first you mark the classes/methods with @objc/@objcMembers. Since you don’t want to make your Swift interface public you can either use performSelector to send messages OR you can manually create a header containing all your Swift interface definitions. For example, define type symbol name or selector name in your swift file with @objc attribute:

    @objc(FrameWorkProjectName_SwiftType)
    class SwiftType: NSObject {
        func call() { /* Do something */ }
    }
    

    and then define the swift type interface in your header file:

    SWIFT_CLASS("FrameWorkProjectName_SwiftType")
    @interface SwiftType : NSObject
    
    - (void)call;
    
    @end
    

    When defining type symbol names make sure that it doesn’t conflict with other names in your or any other external module. To avoid this try to include your framework name in symbol name.

    Be advised, that SWIFT_CLASS macro is generated as part of {TargetName}-Swift.h header.

    Some Clarifications:

    These are some clarification points answering some comments:

    1. The header {FrameWorkProjectName}.h is not a bridging header, it’s the umbrella header of a framework, where only public headers can be exposed, which contradicts the requirements of the question.

    Yes for the framework it is called umbrella header, but you can achieve the same effect of bridging header in app targets, the only difference being the headers have to be public to be included here. And the side effect being the declarations in these headers would be exposed to consumers of your library.

    While this point doesn’t address your question specifically, I put it as a workaround if you think the actual action is quite cumbersome and requires a lot of manual work for your time constraints.

    For framework projects, a bridging header is just not supported, so you have to rely on swift-generated headers which only include public interfaces

    Swift generated header is used to expose your Swift interfaces to Objective-C. They basically achieve the opposite of what bridging headers do. So in short,

    Bridging header => Expose Objective-C declarations to Swift in app target.
    Umbrella header => Expose Objective-C declarations to Swift in framework target and targets consuming the framework.
    Swift generated header => Expose Swift interfaces to Objective-C (for frameworks declarations has to be public)

    For more detail, you can go through official docs for importing objective-c into swift and importing swift into objective-c.

    1. A few times you suggest to make Swift/Objective-C interface public, which is not really a solution to the question, as it looks for a way to avoid that.

    These also I put as workarounds if you think the actual action is quite cumbersome and requires a lot of manual work for your time constraints.

    1. You cannot use SWIFT_CLASS macro without importing the -Swift.h generated header. It’s redundant when it comes to use of internal classes

    You are correct, unfortunately, I missed that. Thanks for adding this point.

    Login or Signup to reply.
  3. There is actually not that much magic around Swift and Objective-C interoperability. First, let’s take a look at what the official documentation says:

    When you’re building an app target, you can import your Swift code
    into any Objective-C .m file within that same target using this syntax
    and substituting the appropriate name:

    #import "ProductModuleName-Swift.h"
    

    By default, the generated header contains interfaces for Swift
    declarations marked with the public or open modifier. If your app
    target has an Objective-C bridging header, the generated header also
    includes interfaces marked with the internal modifier.

    The automagic part merely generates Objective-C interfaces for us. Yes, the generated part for internal interfaces happens only if we use the bridging header and Xcode doesn’t support bridging headers for framework targets. However the documentation explicitly says that internal interfaces are still accessible to Objective-C runtime (emphasis mine):

    Because the generated header is part of the framework’s public
    interface, only declarations marked with the public or open modifier
    appear in the generated header for a framework target. 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.

    It means, that while generated headers are public/open only, nothing prevents you from writing the same interfaces manually and use them for internal classes.

    Consider the following swift class:

    @objc
    class InternalClass: NSObject {
        
        @objc
        var mValue = 64
        
        @objc
        func call() {
            print("Hello from swift")
        }   
    }
    

    All parts of it are exposed to the Objective-C runtime somehow, it’s just unknown how exactly. Since nobody is going to generate this guidance we can just describe it ourselves. Here is a header file TDWInternalClass.h which has this description:

    @interface TDWInternalClass : NSObject
    
    @property(assign, nonatomic) NSInteger mValue;
    
    - (void)call;
    
    @end
    

    The problem here is that when Objective-C classes out of Swift interfaces are generated, their names are mangled, so TDWInternalClass interface would have hard time finding the InternalClass implementation. However it’s possible to change the runtime name for the Swift declaration by providing argument to @objc attribute:

    @objc(TDWInternalClass)
    class InternalClass: NSObject
    

    As we deal with a Framework target, don’t forget to add this manually implemented header to the Header section of the Build Phases (I use the Project section to avoid exposing this header to the client code):
    enter image description here

    And we are done. The internal Swift class implementation is now accessible to project’s own Objective-C classes through the TDWInternalClass interface, and at the same time this InternalClass is not accessible by the client code of the framework:

    #import "TDWInternalClass.h"
    #import "TDWClass.h"
    
    @implementation TDWClass
    
    - (void)printData {
        TDWInternalClass *instance = [TDWInternalClass new];
        [instance call]; // prints "Hello from swift"
        NSLog(@"%ld", instance.mValue); // prints "64"
        
    }
    
    @end
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search