Swift Digest
SE-0058 | Swift Evolution

Allow Swift types to provide custom Objective-C representations

Proposal
SE-0058
Authors
Russ Bishop, Doug Gregor
Review Manager
Joe Groff
Status
Rejected

01 何が問題だったのか

Swift と Objective-C が混在するコードベースでは、ライブラリの API を Objective-C からも呼び出せるようにしておく必要があります。ところが、ジェネリクス、関連値を持つ enumstruct、関連型付きプロトコルといった Swift 独自の機能は Objective-C 側に表現できず、その結果、Swift で書いた新しい API であっても Objective-C で表現できる範囲に機能を制限せざるを得ない という問題がありました。

フレームワークやライブラリの作者には、実質的に次のような不本意な選択肢しか残されていませんでした。

  1. Swift の型と Objective-C の型の間を変換する大量のグルーコードを書く
  2. Swift で書きつつ、API 上は @objc の型だけを使い「Objective-C 風」のまま妥協する
  3. いっそフレームワーク自体を Objective-C で書く

1 はスケジュールの都合で現実的でないことが多く、結果として 2 や 3 が選ばれがちです。そしていったん Objective-C 風の API に寄せてしまうと、将来 Objective-C を切り捨てたあとにも「Swift で書かれているのに Objective-C の直訳のような」コードが残り、Swift らしい設計に戻す余地を失ってしまいます。

現状、自動橋渡しは標準ライブラリ専用だった

String / Array / Dictionary / Set などの標準ライブラリ型は、Objective-C とやり取りする際に対応する Foundation クラス(NSString / NSArray など)へ自動的に橋渡しされます。この仕組みは内部的には非公開プロトコル _ObjectiveCBridgeable で実装されていました。

しかし、_ObjectiveCBridgeable は標準ライブラリ専用の「マジック」であり、ユーザーが自作の Swift 型に適合させることはできませんでした。そのため、Swift の豊かな型システムを活かしたユーザー定義型を、自動的に別の @objc 型として Objective-C に公開する手段が存在しませんでした。

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

この提案は Rejected(却下) となりました。ユーザー定義型を自作の @objc クラスへ自動橋渡しする公開 API は、Swift には導入されていません。標準ライブラリ型と Foundation の間の橋渡しは現在も内部実装として存続していますが、外部からそのメカニズムに乗ることはできません。

提案されていた内容(却下されたもの)

非公開だった _ObjectiveCBridgeable を、次のような公開プロトコル ObjectiveCBridgeable として公開する案でした。

public protocol ObjectiveCBridgeable {
    /// Self を Objective-C 側で表現する @objc クラス(またはその基底クラス)
    associatedtype ObjectiveCType : AnyObject

    static var isBridgedToObjectiveC: Bool { get }

    func bridgeToObjectiveC() -> ObjectiveCType

    init?(bridgedFromObjectiveC: ObjectiveCType)

    init(unconditionallyBridgedFromObjectiveC: ObjectiveCType?)
}

ある Swift 型がこのプロトコルに適合していれば、その型を引数・戻り値に使う @objc メソッドをコンパイラが検出し、ObjectiveCType に変換する 橋渡しの thunk を自動生成する、という動作を想定していました。StringArray のような標準ライブラリ型も、「マジック」ではなく通常のプロトコル適合としてこの機構に相乗りする予定でした。

仕組みの要点は次のようなものでした。

  • @objc クラスのメソッドが @nonobjc な型(=Objective-C に直接公開できない Swift 型)を引数や戻り値に使っていても、その型が ObjectiveCBridgeable に適合していればコンパイラが ObjectiveCType に置き換えた thunk を生成し、Objective-C に公開する。
  • Objective-C 側の型には、対応する Swift 型を示す clang 属性(SWIFT_BRIDGED("SwiftTypeName"))を付けて宣言する。Swift 側で ObjectiveCType を定義した場合は、コンパイラが生成ヘッダに自動で付加する。
  • Swift 型と ObjectiveCType同じモジュール で定義する必要がある。
  • ひとつの ObjectiveCType(あるいはその基底クラス)に対して橋渡しできる Swift 型は 1 つまで。複数の Swift 型が同じ Objective-C 型に橋渡ししようとした場合はコンパイルエラー。
  • Int / NSInteger のようにビルトインで特別扱いされている型は、コレクションの外側では従来どおりの直接変換を維持し、ObjectiveCBridgeable 経由にはしない。

たとえば、関連値を持つ enum@objc クラスに変換して Objective-C に公開するとこう書けるようになる、というのが提案されていた使い方です。

enum Fizzer {
    case Case1(String)
    case Case2(Int, Int)
}

extension Fizzer: ObjectiveCBridgeable {
    func bridgeToObjectiveC() -> ObjCFizzer {
        let bridge = ObjCFizzer()
        switch self {
        case let .Case1(x):
            bridge._case1 = x
        case let .Case2(x, y):
            bridge._case2 = (x, y)
        }
        return bridge
    }

    init?(bridgedFromObjectiveC source: ObjCFizzer) {
        if let stringValue = source._case1 {
            self = Fizzer.Case1(stringValue)
        } else if let tupleValue = source._case2 {
            self = Fizzer.Case2(tupleValue.0, tupleValue.1)
        } else {
            return nil
        }
    }
}

class ObjCFizzer: NSObject {
    private var _case1: String?
    private var _case2: (Int, Int)?
    // Objective-C 側から _case1 / _case2 を読み書きするための API
}

却下の理由

2016 年 4 月 12 日、Swift core team はこの提案を Swift 3 から defer(先送り) し、後にまとめて却下としました。ライブラリ作者が自分の型を Foundation と同じ仕組みで橋渡しできる機能自体には価値があると認めつつも、次の点で公開 API として確定させるには時期尚早と判断されています。

  • _ObjectiveCBridgeable は、名前が示すとおり Objective-C のオブジェクト型と Swift の値型との橋渡し という特定の用途に合わせて設計されていて、公開プロトコルとしての一般性が十分か確信が持てない。
  • 将来的には C++ の値型や、COM / GObject / JVM / CLR といった別の言語・オブジェクトシステムとの橋渡しも考えられる。それらが既存機構の一般化として表現できるのか、それとも用途ごとに個別のプロトコルが必要になるのかを判断するための実装経験が不足している。

その結果、公開 API としてコミットするにはまだ早い、ということで却下になりました。現時点の Swift でも、Swift 型を独自の @objc クラスへ自動橋渡しする公式な手段はなく、Objective-C に公開したい API は引き続き手動で @objc 互換の型に変換する必要があります。