Swift Digest
SE-0436 | Swift Evolution

Objective-C implementations in Swift

Proposal
SE-0436
Authors
Becca Royal-Gordon
Review Manager
Freddy Kellison-Linn
Status
Implemented (Swift 6.1)

01 何が問題だったのか

Swift から Objective-C に型を公開する仕組みとして、従来は @objc 属性を用いる方法がありました。@objc クラスでは、Swift 側の定義から Objective-C のメタデータと、Objective-C から取り込むためのヘッダが自動生成されます。これはアプリや小さなフレームワーク内で言語を混在させる用途にはよく機能しますが、特に公開 API を Objective-C から利用させたいフレームワークにおいては、いくつかの限界があります。

第一に、Swift モジュールのビルド時の循環を避けるため、生成される -Swift.h は同じモジュール内の他の Objective-C ヘッダから取り込めません。モジュールをまたぐビルド順の都合で生成ヘッダを扱えないビルドシステムも存在します。

第二に、Objective-C のヘッダはそのままドキュメントとして読まれることを期待されますが、Swift が機械的に生成するヘッダは、人が手書きするヘッダのような整理された構造を持ちません。

第三に、@objc クラスは Objective-C から「利用」はできますが、内部には Swift 用の vtable などの Swift 固有のデータを持つため、純粋な Objective-C クラスと完全には同等に扱えません。Objective-C 側からサブクラス化したり、メソッドの swizzling を安定して行ったりすることができず、既存の Objective-C サブクラスを持つクラスを Swift に完全に移植することは事実上できませんでした。

一方で Swift には、Objective-C のヘッダで宣言されたメソッドと同名のメンバを Swift の extension で重ねて定義しても、セレクタ衝突を診断しないという非公式な挙動があり、一部のプロジェクトでこれが「ヘッダで宣言し Swift で実装する」パターンとして活用されていました。しかし、この挙動は実装漏れや型不一致を検出できず、クラス本体のメタデータを得るために別途 Objective-C 側の @implementation が必要になるなど、正式な機能としては整備されていませんでした。

02 どのように解決されるのか

新しい属性 @implementation を導入します。@objc などの言語を指定する属性と組み合わせて使い、「他の言語から取り込んだ宣言を Swift 側で実装する」ことを明示します。本Proposalの範囲では、@objc @implementationextension に付けることで、Objective-C の @interface に対する @implementation ブロックを Swift の extension で置き換えられるようにします。

基本的な使い方

まず、Objective-C で通常通りヘッダを書きます。

#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:));

@end

NS_HEADER_AUDIT_END(nullability, sendability)

これを umbrella ヘッダやブリッジングヘッダ経由で Swift に取り込み、@interface ごとに対応する extension@objc @implementation 付きで書きます。

@objc @implementation extension MYFlippableViewController {
    var frontViewController: UIViewController
    var backViewController: UIViewController
    var isShowingFront: Bool

    init(frontViewController: UIViewController,
         backViewController: UIViewController) {
        self.frontViewController = frontViewController
        self.backViewController = backViewController
        self.isShowingFront = true
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError() }
}

カテゴリを実装する場合は、@objc(CategoryName) でカテゴリ名を指定します。

@objc(Animation) @implementation extension MYFlippableViewController {
    func setIsShowingFront(_ isShowingFront: Bool, animated: Bool) {
        // ...
    }
}

ヘッダ側には、当該 @interface が Swift で実装されていることを示す特別な記述は不要です。NS_SWIFT_NAMENS_NOESCAPE などの既存のアノテーションもそのまま使え、Swift への取り込まれ方に反映されます。ひとつのクラスの中で、一部の @interface を Objective-C で、残りを Swift で実装することもでき、既存クラスをカテゴリ単位で段階的に Swift へ移植できます。

メンバ実装のルール

@objc @implementation extension の中で、open / public / package / internal のうちのいずれかで、override でも final でもない @objc メンバは「メンバ実装(member implementation)」として扱われ、ヘッダで宣言された Objective-C メンバに対応します。

  • メンバ実装の Swift 名と Objective-C セレクタは、ヘッダの宣言と一致していなければなりません。@objc(custom:selector:)NS_SWIFT_NAME も尊重されます。
  • シグネチャも一致している必要があります。ただし、Objective-C 側で nonnull な型に対して、Swift 側では implicitly unwrapped optional として受けることが許されます(過去の nil 渡しへの互換処理のためです)。
  • ヘッダに宣言されたメンバはすべてメンバ実装を持たなければならず、逆にメンバ実装はヘッダのどれかに対応していなければなりません。これにより、実装漏れやスペル・シグネチャの食い違いがコンパイル時に検出されます。

メンバ実装は、式の解決上はコンパイラから「存在しないもの」として扱われます。アクセス制御やモジュールインターフェース、生成ヘッダのいずれからも除外されるため、同じモジュール内の他の Swift コードから見ても、そのメンバはあくまで Objective-C で実装されたメンバとして振る舞います。

メンバ実装以外に置けるもの

@objc @implementation extension の中には、メンバ実装に加えて次のようなメンバも置けます。

  1. fileprivate または privatefinal でないメンバ。@IBAction やコールバック用セレクタなどのヘルパメソッドに相当し、ヘッダの宣言とは対応しません。Objective-C 側からはセレクタを通じて利用できます。
  2. override 修飾子を持つメンバ。スーパークラスのメンバをオーバーライドし、通常どおりに機能します。
  3. final を付けたメンバ(イニシャライザでは @nonobjc)。Swift 専用のメンバとして扱われ、Swift 固有の型や機能を使えます。internal / private なら Swift 側の実装詳細、public / package なら Swift 専用 API になります。@objc @implementation extension 内では、final を付けたメンバは既定で @nonobjc になります。

クラス本体の @interface を実装する(カテゴリではない)@objc @implementation extension には、通常の extension では許されない stored property、designated / required な initdeinit を宣言できます。@implementation には Objective-C の暗黙の @synthesize に相当する仕組みはなく、ヘッダの @property を stored property で実装したい場合は、対応する var を明示的に宣言する必要があります。

対象クラスとカテゴリ名

@objc @implementation extension が対象にできるクラスは、Objective-C から取り込んだ、ルート以外のクラスで、lightweight generics を使っていないものに限られます。また、@objc(CategoryName) による明示的なカテゴリ名の指定は、@implementation と組み合わせない通常の extension にも拡張されます。クラスとカテゴリ名の組み合わせごとに extension はひとつしか存在してはならず、コンパイラがこれを強制します。

生成される Objective-C メタデータ

@objc @implementation extension に対して Swift が生成するメタデータは、clang が対応する @implementation から生成するものと同等になります。@objc メンバは Swift 側のメタデータを持たず、Objective-C のメタデータのみを持ちます(final メンバは Swift 側のメタデータを必要とする場合があります)。クラス本体の @interface を実装する場合には、stored property ごとに ivar を持つ完全な Objective-C クラスメタデータが生成され、Swift ビットの設定や、clang サブクラス・カテゴリと非互換な機能は使われません。結果として、Objective-C 側から見ればこのクラスは通常の Objective-C クラスと区別がつかず、Objective-C からのサブクラス化や swizzling が問題なく行えます。

導入時の制約

@implementation extension のうちカテゴリを実装するものは、Swift 5.0 以降のランタイムにバックデプロイ可能で、クラス本体を実装するものも多くはバックデプロイ可能です。ただし、stored property の ivar レイアウトをコンパイル時に確定できないクラスでは、新しいランタイムサポートが必要となり、古いプラットフォームへはバックデプロイできません。具体的には、library evolution が有効な他モジュールから取り込んだ非 frozen な enum / struct を stored property に含むクラスが該当します(この性質は推移的です)。実務上は、該当する値をクラスや existential でボックス化することで回避できます。

Future Directions

今後の方向性としては次のようなものが挙げられています。いずれも本Proposalの範囲外で、将来的に検討される可能性があるにとどまります。

  • @objc 自体が対応していない機能(factory convenience initializer、objc_direct、特殊なメモリ管理、変則的な error convention、トップレベル宣言など)について、@objc の拡張に合わせて @implementation でも扱えるようにする方向性。
  • @_cdecl @implementation のように、プレーン C 宣言を Swift から実装する仕組み(実験的に存在します)や、C++ 宣言を Swift から実装する仕組み。
  • lightweight generics を使う Objective-C クラスへの対応や、網羅性チェックを外す @objc @implementation(unchecked) の導入。
  • 実装専用のブリッジングヘッダや、private Clang モジュールとの連携強化。