humancode.us

Exposing Swift implementations to Objective-C

August 9, 2023

In this post I describe a recipe for exposing symbols implemented in Swift to Objective-C through a header file. The symbols supported are:

  • Functions
  • Classes
  • Categories
  • Protocols
  • Enumerations

If you want to go straight to code you can use, check out my github project.

Why expose Swift implementations to Objective-C?

Many large projects contain a mix of Objective-C and Swift code. So, it’s often useful for libraries to expose both Objective-C and Swift interfaces to a common implementation, so the library can be used everywhere in a project.

One customary way to do this is to implement your library in pure Objective-C. As long as your library is packaged as a module (framework), you can easily use the code from both the Objective-C and Swift parts of your project. The downside of doing this is that your interface tends to not feel very “Swifty”. As more and more of your code moves to Swift, you will find yourself wanting a more idiomatic Swift interface that may be impossible to achieve with pure Objective-C headers.

Another step one could take would be to create a Swift overlay to your otherwise pure Objective-C library. You mark your Objective-C symbols NS_REFINED_FOR_SWIFT and provide an alternative API in Swift that provides a Swift interface, but uses your Objective-C implementations under the hood. You can go a long way with this technique, but you may eventually find that optimizing for the Swift user of your library is more valuable than prioritizing the Objective-C user.

That brings us to what this post addresses: how you can write your library in Swift, yet still expose an Objective-C header that maps Objective-C users of your library directly to the Swift implementation, with no manual bridging. I hope that the value of this utility is apparent at this point.

There are limitations to this technique

Because not all Swift concepts can be exposed to Objective-C, you can’t expose everything to Objective-C with this technique.

Only top-level symbols are supported. You can’t expose symbols nested within another symbol, such as enumerations that are scoped to a class.

Only compatible symbols may be exposed to Objective-C. This one may seem obvious, but there are limitations because the symbols you expose have to make sense when used from Objective-C:

  • Classes must inherit from NSObject.
  • Protocols must inherit from NSObjectProtocol.
  • Classes cannot be subclassed from Objective-C (they can be subclassed from Swift).
  • Runtime message-dispatch tricks cannot be used on exposed classes. This means methods like doesNotRecognizeSelector: and friends are never called.
  • Enumerations must inherit from a scalar such as Int.
  • Only compatible method signatures can be exposed to Objective-C. Methods that return enumerations with associated values cannot be exposed, for example.

With that out of the way, let’s get coding.

Follow these steps

Disable automatic header generation

By default, Swift will generate a header that declares symbols you mark as @objc. Because we want to control the generated header ourselves, add the following build setting to disable auto-generation:

SWIFT_INSTALL_OBJC_HEADER = NO

Alternatively, you can set “Install Objective-C Compatibility Header” to NO in the Xcode GUI.

Create public header files

Create a set of public header files that will be packaged in your framework, in which you will declare the Objective-C views to your Swift implementation.

To ensure your framework remains modular, be sure to #import all public headers from your umbrella header. For example, for the framework MyLibrary you would have an umbrella header file MyLibrary.h which imports all other public header files like so:

#import <MyLibrary/MYLIBFoo.h>
#import <MyLibrary/MYLIBBar.h>

and so on.

Declare convenience macros

There are a few clang attributes that mark your symbols as implemented in Swift. The following code is found in the Defines.h file in my github project:

#define HUMN_IMPLEMENTED_IN_SWIFT(_module) \
__attribute__((external_source_symbol(language="Swift", defined_in=#_module, generated_declaration)))

#define HUMN_CLASS_IMPLEMENTED_IN_SWIFT(_swift_name, _module) \
__attribute__((swift_name(#_swift_name))) \
__attribute__((objc_subclassing_restricted)) \
HUMN_IMPLEMENTED_IN_SWIFT(_module)

#define HUMN_PROTOCOL_IMPLEMENTED_IN_SWIFT(_swift_name, _module) \
__attribute__((swift_name(#_swift_name))) \
HUMN_IMPLEMENTED_IN_SWIFT(_module)

#define HUMN_ENUM_IMPLEMENTED_IN_SWIFT(_type, _name, _swift_name, _module) \
enum _name : _type _name __attribute__((swift_name(#_swift_name))); \
enum __attribute__((swift_name(#_swift_name))) \
__attribute__((enum_extensibility(closed))) \
HUMN_IMPLEMENTED_IN_SWIFT(_module) \
_name: _type

#import this header file at the top of every header file that uses these macros.

Mark up your code

Functions

Declare the Objective-C view of the function in a header file, and mark it with HUMN_IMPLEMENTED_IN_SWIFT:

HUMN_IMPLEMENTED_IN_SWIFT(MyModuleName)
extern NSInteger objcFun(NSInteger a, NSInteger b);

In your Swift implementation file, add a @_cdecl attribute with the function’s Objective-C name.

@_cdecl("objcFun") public func fun(_ a: Int, _ b:Int) -> Int { ... }

Classes

Declare the Objective-C view of the class in a header file, and mark it with HUMN_CLASS_IMPLEMENTED_IN_SWIFT. The first parameter is the Swift name of the class, and the second is the module name.

HUMN_CLASS_IMPLEMENTED_IN_SWIFT(Arithmetic, MyModuleName)
@interface HUMNArithmetic : NSObject

@property (nonatomic) NSInteger valueA;
@property (nonatomic) NSInteger valueB;
@property (nonatomic, readonly) NSInteger sumOfValues;

- (NSInteger)sumOfValuesAndValue:(NSInteger)valueC;

@end

In your Swift implementation file, mark the class with @objc, and mark each method and property you want to expose with @objc, providing the Objective-C name for each symbol, including colons ::

@objc(HUMNArithmetic)
public class Arithmetic: NSObject {
    @objc(valueA) public var a: Int = 0
    @objc(valueB) public var b: Int = 0
    @objc(sumOfValues) public var sum: Int { get { ... } }

    public func sumOfValuesAnd(_ c: Int) -> Int { ... }

    // Example bridging call
    @objc(sumOfValuesAndValue:)
    private func objc_sumOfValuesAnd(_ c: Int) -> Int {
        return sumOfValuesAnd(c)
    }
}

Just for fun, in the example above, the method sumOfValuesAndValue: is implemented by a function specifically created to bridge the Objective-C call. The actual implementation function sumOfValuesAnd() is not visible from Objective-C because it has no @objc attribute. Conversely, the bridging function objc_sumOfValuesAnd() is not visible from Swift because we declared it private—but it is visible from Objective-C thanks to its @objc attribute.

Categories

Declare the Objective-C view of the category in a header file, and mark it with HUMN_IMPLEMENTED_IN_SWIFT:

HUMN_IMPLEMENTED_IN_SWIFT(MyModuleName)
@interface HUMNArithmetic (Multiplication)

@property (nonatomic, readonly) NSInteger productOfValues;

- (NSInteger)productOfValuesAndValue:(NSInteger)valueC;

@end

In your Swift implementation file, declare an @objc extension, marking each exposed function with an @objc() attribute that declares their Objective-C method signature, complete with colons ::

// We assume the Swift class `Arithmetic` is already exposed to Objective-C
// as `HUMNArithemtic` using the `@objc(HUMNArithmetic)` attribute.
@objc extension Arithmetic {
    @objc(productOfValues) public var product: Int { get { ... } }

    @objc(multiplyValuesAndValue:)
    public func multiplyValuesAnd(_ c: Int) -> Int { ... }
}

Protocols

Declare the Objective-C view of the protocol in a header file, and mark it with HUMN_PROTOCOL_IMPLEMENTED_IN_SWIFT. The first parameter is the Swift name of the protocol, and the second is the module name.

HUMN_PROTOCOL_IMPLEMENTED_IN_SWIFT(Subtraction, MyModuleName)
@protocol HUMNSubtraction <NSObject>
- (NSInteger)subtractValue:(NSInteger)valueA fromValue:(NSInteger)valueB;
@end

In your Swift implementation file, declare the protocol marked with the @objc attribute, and mark each function or property that should be visible from Objective-C with @objc. For each attribute, provide the Objective-C name of the symbol, including colons ::

@objc(HUMNSubtraction)
public protocol Subtraction: NSObjectProtocol {
    @objc(subtractValue:fromValue:)
    func subtract(_ a: Int, from b: Int) -> Int
}

Enumerations

To declare an enumeration in Objective-C, you must decide what scalar type you want to hold your enumerated values. Then, define the Objective-C view of the enumeration in a header, and mark it with HUMN_ENUM_IMPLEMENTED_IN_SWIFT in a typedef:

typedef HUMN_ENUM_IMPLEMENTED_IN_SWIFT(NSInteger, HUMNEnumeration, Enumeration, MyModuleName) {
    HUMNEnumerationZero,
    HUMNEnumerationOne,
    HUMNEnumerationTwo,
};

The parameters to HUMN_ENUM_IMPLEMENTED_IN_SWIFT are:

  1. The scalar type of the enumeration (Objective-C NSInteger maps to Swift’s Int)
  2. The Objective-C name of the enumeration
  3. The Swift name of the enumeration
  4. The module name

In your Swift implementation file, declare the enumeration, inheriting from the scalar type and mark it with an @objc attribute.

@objc(HUMNEnumeration) public enum Enumeration: Int {
    case zero
    case one
    case two
}

Conclusion

There you have it: concrete steps you can follow to expose your Swift implementation directly to Objective-C, without bridging. To see these examples in action, check out my github project which comes with all this code and an example program you can run.