Swift Digest
SE-0436 | Swift Evolution

SwiftにおけるObjective-C実装

Objective-C implementations in Swift

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

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

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 でボックス化することで回避できます。

03 今後の見通し

本Proposalの範囲を超える発展として、次のような方向性が挙げられています。いずれも将来の構想であり、実現を約束するものではありません。

@objc の機能拡張に合わせた @objc @implementation の拡充

@objc @implementation@objc のコード生成に強く依存しているため、@objc 自体が扱えない機能はそのまま @implementation でも扱えません。たとえば次のような @interface のメンバは、現状では @objc @implementation extension では実装できず、別カテゴリに分けて Objective-C で実装する必要があります。

  • ファクトリ用の convenience initializer(+[NSString stringWithCharacters:length:] のようにクラスメソッドとして実装されるもの)。
  • __attribute__((objc_direct)) メソッドや @property (direct)
  • 標準的でないメモリ管理を持つメンバ(正しくアノテーションされていても対象外)。
  • NSError** パラメータの位置がずれているなど、Objective-C の error convention から外れたメンバ。

加えて、@objc 自体で表現できないトップレベル宣言(フリー関数、グローバル変数、NS_TYPED_ENUM のケース、NS_SWIFT_NAME の import-as-member で型のメンバとして取り込まれる宣言など)も対象外です。これらについても、まず @objc 側を拡張し、その上で @objc @implementation でも扱えるようにする、という順序で対応する方針が示されています。

プレーン C 宣言の @implementation

非 Darwin プラットフォームを含む C クライアント向けにも、同様の仕組みがあれば有用だとされています。@_cdecl のような仕組みを安定化・拡張してヘッダ生成に対応させ、@implementation と組み合わせて使えるようにする方向性です。コンパイラには、グローバル関数向けの @_cdecl @implementation の実験的サポートが既に存在しますが、本Proposalの範囲外として別の experimental feature flag の下に置かれています。

C++ 宣言の @implementation

C++ クラスのメソッドを Swift 側で実装する、@_expose(Cxx) @implementation extension CppClass { ... } のような形も構想されています。Objective-C 連携ではコンパイラが直接バイナリ中にサンクを生成しますが、C++ 連携では生成ヘッダ中に C++ ソースとしてサンクを出力するため、その C++ コードをコンパイラ内部でビルドするか、ファイルに書き出してビルドシステムにビルド・リンクさせるか、といった検討事項があります。@implementation 自体は言語固有の属性と組み合わせて使う設計のため、このような将来的な拡張とも共有できると考えられています。

lightweight generics を使うクラスへの対応

Objective-C の lightweight generics を使うクラスは、ジェネリックパラメータが型消去されるため、Swift の extension で扱う上でさまざまな制約が生じます。利用しているクラスがごく少数であることから、本Proposalではこの組み合わせを禁止していますが、需要が大きければ後から解禁する余地があるとされています。

@objc @implementation(unchecked)

+instanceMethodForSelector: のような動的な仕組みでメソッドを実行時に生成したい場合に向けて、@objc @implementation の網羅性チェック(ヘッダで宣言されたメンバはすべて実装し、逆も成立しなければならないという要求)を緩めるオプションが考えられます。純粋な追加機能のため、要望次第で導入を検討できるとされています。

実装専用のブリッジングヘッダや private Clang モジュールとの連携

公開フレームワークのビルド時に、umbrella ヘッダと並行して実装専用のブリッジングヘッダを取り込めるようにする機能や、private Clang モジュールに関する改善(mixed-source フレームワークの Swift 半分から internal で private Clang モジュールを暗黙インポートする、@_spi と private Clang モジュールを対応付けて final な Swift メンバを公開できるようにする、など)と組み合わせると、@objc @implementation の使い勝手がさらに高まると考えられています。これらは本Proposalとは独立した別の改善として扱う方向です。