Exposing Swift implementations to Objective-C
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:
- The scalar type of the enumeration (Objective-C
NSInteger
maps to Swift’sInt
) - The Objective-C name of the enumeration
- The Swift name of the enumeration
- 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.