- Proposal: SE-NNNN
- Authors: Becca Royal-Gordon
- Review Manager: TBD
- Status: Awaiting implementation (but only of the final attribute name)
- Implementation: Preliminary version in main, as
@_objcImplementation
We propose an alternative to @objc classes where Objective-C header @interface declarations are implemented by Swift extensions. The resulting classes will be implemented in Swift, but will be indistinguishable from Objective-C classes, fully supporting ObjC subclassing and runtime trickery.
Swift-evolution thread: first pitch, (this one)
Swift has always had a mechanism that allows Objective-C code to use Swift types: The @objc attribute. When a class is marked with @objc (or, more typically, inherits from an @objc or imported ObjC class), Swift generates sufficient Objective-C metadata to allow it to be used through the Objective-C runtime, and prints a translated ObjC declaration into a generated header file that can be imported into Objective-C code. The same goes for members of the class.
This feature works really well for mixed-language apps and project-internal frameworks, but it's poorly suited to exposing private and especially public APIs to Objective-C. There are three key issues:
-
To avoid circularity while building the Swift half of the module, the generated header cannot be included into other headers in the same module, which can make it difficult to use the Swift-implemented parts of the API in the Objective-C-implemented parts. Worse, some build systems install the headers for all modules and then build binaries for them out of order; generated headers can't really be used across modules in these systems.
-
Objective-C programmers expect API headers to serve as a second source of documentation on the APIs, but generated headers are disorganized, unreadable messes because Swift cannot mechanically produce the formatting that a human engineer would add to a handwritten header.
-
While
@objcclasses can be used from Objective-C, they are not truly Objective-C types. They still contain Swift vtables and other Swift-specific data that the Objective-C compiler and runtime don't fully understand how to work with. This limits their capabilities—for instance, Objective-C code cannot subclass an@objcclass, reliably swizzle its methods, or introspect its instance variables.
Together, these issues make it very hard for frameworks with a lot of Objective-C clients to implement their functionality in Swift. If they have classes that are meant to be subclassed, it's actually impossible to fully port them to Swift, because it would break existing Objective-C subclasses. And yet the trade-offs made by @objc are really good for the things it's designed for, like writing custom views and view controllers in Swift app targets. We don't want to radically change the existing @objc feature.
Swift also quietly supports a hacky pseudo-feature that allows a different model for Objective-C interop: It will not diagnose a selector conflict if a Swift extension redeclares members already imported from Objective-C, so you can declare a method or property in a header and then implement it in a Swift extension. However, this feature has not really been designed to work properly; it has no safety checks to ensure that all declared members have been implemented, and you still need an @implementation for the class metadata itself. Nevertheless, a few projects use this and find it helpful because it avoids the issues with normal interop. Formalizing and improving this pattern seems like a promising direction for Objective-C interop to explore.
We propose adding a new attribute, @implementation, which allows a Swift extension to replace an ObjC @implementation block. You write headers as normal for an Objective-C class, but instead of writing an @implementation in an Objective-C file, you write an @implementation extension in a Swift file. You can even port an existing class’s implementation to Swift one category at a time without breaking backwards compatibility.
Specifically, if you were adding a new class, you would start by writing a normal Objective-C header, as though you were planning to implement the class in an Objective-C .m file:
#import <UIKit/UIKit.h>
NS_HEADER_AUDIT_BEGIN(nullability, sendability)
@interface MYFlippableViewController : UIViewController
@property (strong) UIViewController *frontViewController;
@property (strong) UIViewController *backViewController;
@property (assign,getter=isShowingFront) BOOL showingFront;
- (instancetype)initWithFrontViewController:(UIViewController *)front backViewController:(UIVIewController *)back;
@end
@interface MYFlippableViewController (Animation)
- (void)setShowingFront:(BOOL)isShowingFront animated:(BOOL)animated NS_SWIFT_NAME(setIsShowingFront(_:animated:));
- (void)setFrontViewController:(UIViewController *)front animated:(BOOL)animated;
- (void)setBackViewController:(UIViewController *)back animated:(BOOL)animated;
@end
@interface MYFlippableViewController (Actions)
- (IBAction)flip:(id)sender;
@end
NS_HEADER_AUDIT_END(nullability, sendability)And you would arrange for Swift to import it through an umbrella or bridging header. You would then write an extension for each @interface you wish to implement in Swift. For example, you could implement the main @interface (plus any visible class extensions) in Swift by writing:
@implementation extension MYFlippableViewController {
...
}And the Animation category by writing:
@implementation(Animation) extension MYFlippableViewController {
...
}Note that there is nothing special in the header which indicates that a given @interface is implemented in Swift. The header can use all of the usual Swift annotations—like NS_SWIFT_NAME, NS_NOESCAPE, etc.—but they simply affect how the member is imported. Swift does not even require you to implement every declared @interface in Swift, so you can implement some parts of a class in Objective-C and others in Swift. But if you choose to implement a particular @interface in Swift, each Objective-C member in that @interface must be matched by a Swift declaration in the extension that has the same Swift name; these special members are called "member implementations".
An @implementation extension can contain four kinds of members:
- Open, public, or internal
@objcmembers must be member implementations. Swift will give you an error if they don't match a member from the imported headers, so it will diagnose typos and mismatched Swift names. - Fileprivate or private
@objcmembers are helper methods (think@IBActions or callback selectors). They must not match a member from the imported headers, but they are accessible from Objective-C by performing a selector or declaring them in a place that is not visible to Swift. - Members with a
finalmodifier (or@nonobjcon an initializer) are Swift-only and can use Swift-only types or features. These may be Swift-only implementation details (ifinternalorprivate) or Swift-only APIs (ifpublic). - Members with an
overridemodifier override superclass methods and function normally.
Within an @implementation extension, all non-final and non-@objc members are @objc, and all @objc members are dynamic.
As a special exception to the usual rule, a non-category @implementation extension can declare stored properties. They can be (perhaps implicitly) @objc or they can also be final; in the latter case they are only accessible from Swift. Note that @implementation does not have an equivalent to implicit @synthesize—you must declare a var explicitly for each @property in the header that you want to be backed by a stored property.
The compiler will accept a new attribute, @implementation, on extensions. This attribute can optionally be followed by a parenthesized identifier. If this identifier is present, the extension matches an ObjC category with that name. If it is absent, it matches the main ObjC interface and all ObjC class extensions.
@implementation extension SomeClass {
// Equivalent to `@implementation SomeClass`;
// implements everything in `@interface SomeClass` and
// all `@interface SomeClass ()` extensions.
}
@implementation(SomeCategory) extension SomeClass {
// Equivalent to `@implementation SomeClass (SomeCategory)`;
// implements everything in `@interface SomeClass (SomeCategory)`.
}All non-final, non-@nonobjc members are implicitly @objc, and all @objc members are implicitly dynamic. As a special exception to the usual rule, an extension which implements the main ObjC interface of a class can declare stored properties.
Note that there is currently no way to make an @objc method direct in Swift, so it will be impossible for an @implementation extension to fully implement an @interface that declares a direct method.
An @implementation extension must:
- Extend a non-root class imported from Objective-C which does not use lightweight generics.
- If a category name is present, have imported a category by that name for that class (if no category name is present, the extension matches the main interface).
- Be the only extension on that class with that
@implementationcategory name (or lack of category name). - Implement an
@interfacethat is in the same module, the private clang module, or the bridging header. (If the extension implements a category, this is the module that the category is located in, not the class.) - Not declare conformances. (Conformances should be declared in the header if they are for ObjC protocols, or in an ordinary extension otherwise.)
- Provide a member implementation (see below) for each member of the
@interfaceit implements. - Contain only
@objc,override,final, or (for initializers)@nonobjcmembers. (Note that member implementations are implicitly@objc, as mentioned below, so this effectively means that non-override, non-final, non-@nonobjcmembers must be member implementations.) @nonobjcinitializers must be convenience initializers, not designated or required initializers.
Any non-override open, public, or internal @objc member of an @implementation extension is a “member implementation”; that is, it implements some imported ObjC member of the class it is extending. Member implementations are special because much of the compiler completely ignores them:
- Access control denies access to member implementations in all contexts.
- Module interfaces and generated interfaces do not include member implementations.
- Generated ObjC headers do not include member implementations.
This means that calls in expressions will always ignore the member implementation and use the imported ObjC member instead. In other words, even other Swift code in the project will behave as though the member is implemented in Objective-C.
If a member implementation doesn't have an @objc attribute, one will be synthesized with the appropriate selector.
A member implementation must:
- Have the same Swift name as the member it implements.
- If an explicit
@objc(selector:)attribute is present, have the same selector as the member it implements. - Not have other traits, like an overload signature,
@nonobjc/finalattribute, or direct method bit, which conflict with the member it implements. - Not have
@_spiattributes (they would be pointless since the visibility of the imported ObjC attribute is what will make the member usable or not).
Member implementations are matched by Swift name. If the Swift extension contains several different methods with the same Swift name as an imported ObjC declaration, and one of them has an @objc(selector:) with a matching name, that one will be used; otherwise Swift will diagnose an error.
Member implementations must have an overload signature that closely matches the ObjC declaration’s. However, types that are non-optional in the ObjC declaration may be implicitly unwrapped optionals in the member implementation if this is ABI-compatible; this is because Objective-C does not prevent clients from passing nil or implementations from returning nil when nonnull is used, and member implementations may need to implement backwards compatibility logic for this situation.
When Swift generates metadata for an @implementation extension, it will generate metadata that matches what clang would have generated for a similar @implementation. That is:
@objcmembers will only have Objective-C metadata, not Swift metadata. (finalmembers may require Swift metadata.)- If the extension is for the main class interface, it will generate complete Objective-C class metadata with an ivar for each Objective-C-compatible stored property, and without setting the Swift bit or using any features incompatible with clang subclasses or categories.
Note: I have not yet figured out how we'll handle stored properties with resilient value types. These are tricky because we don't know the size of the instance variable—and thus the size and layout of the instance as a whole—until we have loaded the other library. The existing facility for non-fragile ObjC base classes is probably not sufficient for this use case because it's designed for situations where the library declaring the class knows the size at compile time—it only allows that library's clients to ignore the size. We know that there are bad solutions to this problem, like boxing all resilient value types so that they fit into a fixed-size ivar, but these would sacrifice performance; we don't know yet if there are good solutions which would not require new runtime support, or at least ones which would gracefully degrade to bad solutions in back deployment. (Or is back deployment even necessary for this feature?)
This change is additive and doesn't affect existing code.
All @objc members of an @implementation extension—member implementation or otherwise—have the ABI of an @objc dynamic member, so turning one into the other is not ABI-breaking.
Because @implementation attributes and member implementations are not printed into module interfaces, there is no direct effect on API resilience, but see the note about resilent stored properties in "Objective-C metadata generation" above.
Previous drafts of this proposal used the name @objcImplementation. We shortened that to @implementation because we suspect that C and C++ may eventually use it.
We chose the word "implementation", rather than something like "extern", because the important thing about the attribute is that it marks an implementation of something that was declared elsewhere; names like "extern" usually work in the opposite direction. Also, the name suggests a relationship to the @implementation keyword in Objective-C.
We could have you use a class declaration, not an extension, to implement the main body of a class:
// Header as above
#if canImport(UIKit)
import UIKit
#else
import SwiftUIKitClone // hypothetical pure-Swift UIKit for non-Darwin platforms
#endif
#if OBJC
@implementation
#endif
class MYFlippableViewController: UIViewController {
var frontViewController: UIViewController {
didSet { ... }
}
var backViewController: UIViewController {
didSet { ... }
}
var isShowingFront: Bool {
didSet { ... }
}
init(frontViewController: UIViewController, backViewController: UIViewController) {
...
}
}
#if OBJC
@implementation(Animation)
#endif
extension MYFlippableViewController {
...
}This may allow you to reduce code duplication if you want to write cross-platform classes that only use @implementation on platforms which support Objective-C, and it would also mean we could remove the stored-property exception. However, it is a significantly more complicated model—there is much more we'd need to hide and a lot more scope for the class to have important mismatches with the @interface. And the reduction to code duplication would be limited because pure-Swift extension methods are non-overridable, so all methods you wanted clients to be able to override would have to be listed in the class. This means that in practice, mechanically generating pure-Swift code from the @implementations might be a better approach.
ObjC generics behave in rather janky ways in Swift extensions, so we have banned them in this initial document. If there’s demand for implementing ObjC generic classes in Swift, we may want to extend this feature to support them.
This feature would work extremely well with a feature that allowed Swift to import an implementation-only bridging header alongside the umbrella header when building a public framework. This would not only give the Swift module access to internal ObjC declarations, but also allow it to implement those declarations. However, the two features are fully orthogonal, so I’ll leave that to a different proposal.
This feature would also work very well with some improvements to private ObjC modules:
- The Swift half of a mixed-source framework could implicitly import the private Clang module implementation-only; this would allow you to easily provide implementations for Objective-C-compatible SPI.
- We could perhaps set up some kind of equivalence between
@_spiand private Clang modules so thatfinalSwift members could be made public.
Again, that’s something we can flesh out over time.
Swift can currently call direct ObjC methods, but it can’t declare them. If we invented a spelling for them (@objc final is unfortunately already valid), @implementation extensions would be able to implement interfaces which declare direct methods.
There are pure-C entities, like global functions and NS_TYPED_ENUM constants, which need a separate implementation but are not Objective-C classes. We could extend the @implementation attribute to handle them too:
// Header file
typedef NSString *MYFlippableViewSide NS_TYPED_ENUM;
MYFlippableViewSide const MYFlippableViewSideFront;
MYFlippableViewSide const MYFlippableViewSideBack;
NSString *MYFlippableViewSideToString(MYFlippableViewSide side);// Swift implementation
extension MYFlippableViewSide {
@implementation public static let front = MYFlippableViewSide(rawValue: "front")!
@implementation public static let back = MYFlippableViewSide(rawValue: "back")!
}
@implementation func MYFlippableViewSideToString(_ side: MYFlippableViewSide) -> String { side.rawValue }These features seem easily severable from Objective-C class support, so we feel comfortable deferring them to another proposal.
One could similarly imagine a C++ version of this feature:
// Header file
class CppClass {
int someMethod() { ... }
int swiftMethod();
};@implementation extension CppClass {
func swiftMethod() -> Int32 { ... }
}This would be tricker than ObjC interop because for ObjC interop, Swift already generates machine code thunks directly in the binary, whereas for C++ interop, Swift generates C++ source code thunks in a generated header; an engineer has prototyped this by feeding the generated C++ code directly into an embedded clang instance, but it's not clear if this approach is robust enough for production. However, we believe that there wouldn't be a problem with sharing the @implementation attribute with this feature; Swift could detect whether a given extension is extended an Objective-C class or a C++ class and change its behavior to match.
Doug Gregor gave a ton of input into this design.