Swift Digest
SE-0033 | Swift Evolution

Import Objective-C Constants as Swift Types

Proposal
SE-0033
Authors
Jeff Kelley
Review Manager
John McCall
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Objective-C の多くの API では、特定の意味を持つ値の集まりを グローバルな定数 として提供してきました。典型的なのは NSString 型の識別子で、たとえば HealthKit の HKTypeIdentifiers.h は次のような定数群を公開しています。

HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyMassIndex;
HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyFatPercentage;
HK_EXTERN NSString * const HKQuantityTypeIdentifierHeight;
HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyMass;
HK_EXTERN NSString * const HKQuantityTypeIdentifierLeanBodyMass;

これらは Swift からは単なる String 型の定数として import されます。

public let HKQuantityTypeIdentifierBodyMassIndex: String
public let HKQuantityTypeIdentifierBodyFatPercentage: String
// ...

これらの定数を消費する API も NSString * を受け取る形になっており、Swift 側では String を受け取るメソッドに見えてしまいます。

+ (nullable HKQuantityType *)quantityTypeForIdentifier:(NSString *)identifier;

型情報からは「特別な値」であることが読み取れない

このような API はシグネチャ上ただの String を要求しているだけなので、「任意の文字列を渡してよい」ように見えます。実際には別ファイルで定義された特定の定数のみが有効であることが多く、それを知らないと誤った文字列を渡しても型検査をすり抜けてしまいます。ドキュメントを読まなければ使えない API になってしまっており、特に初学者にとって使いにくい状況でした。

コード補完の恩恵を受けられない

Swift の強みであるコード補完も、受け取り側が単なる String では「どの定数を渡せばよいのか」を示せません。利用者は自力で定数名を思い出したり、ヘッダを grep したりする必要がありました。

一部は列挙、一部は拡張可能という性格の違いがある

こうした定数群の中には、フレームワーク側が定義する値だけを使うもの(HealthKit の識別子など)と、利用者側が新しい値を追加してよいもの(NSError のドメインなど)の両方があります。前者は列挙型(enum)として表現したい一方、後者は外部モジュールからも値を足せる必要があるため、enum では表現できません。これらを適切に区別して Swift に橋渡しする仕組みが必要でした。

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

Objective-C のヘッダ側で typedef と新しい属性 swift_wrapper を組み合わせることで、定数群を Swift の enum もしくは struct として import できるようにします。属性には enumstruct の2種類があり、API の性格に合わせて使い分けます。

swift_wrapper(enum): 拡張不可な定数群を列挙型として import する

利用者が値を追加する余地がない定数群(HealthKit の識別子など)には swift_wrapper(enum) を指定します。

typedef NSString * HKQuantityTypeIdentifier __attribute__((swift_wrapper(enum)));

HK_EXTERN HKQuantityTypeIdentifier const HKQuantityTypeIdentifierBodyMassIndex;
HK_EXTERN HKQuantityTypeIdentifier const HKQuantityTypeIdentifierBodyFatPercentage;
HK_EXTERN HKQuantityTypeIdentifier const HKQuantityTypeIdentifierHeight;
HK_EXTERN HKQuantityTypeIdentifier const HKQuantityTypeIdentifierBodyMass;
HK_EXTERN HKQuantityTypeIdentifier const HKQuantityTypeIdentifierLeanBodyMass;

Swift 側ではこれが String を raw value に持つ enum として見えます。

enum HKQuantityTypeIdentifier: String {
    case BodyMassIndex
    case BodyFatPercentage
    case Height
    case BodyMass
    case LeanBodyMass
}

あわせて Objective-C のメソッドシグネチャも NSString * ではなく新しい typedef を使うように書き換えます。

+ (nullable HKQuantityType *)quantityTypeForIdentifier:(HKQuantityTypeIdentifier)identifier;

こうすると Swift からはドット省略記法でケースを直接渡せるようになり、rawValue を介して String を取り出す必要がなくなります。

let quantityType = HKQuantityType.quantityTypeForIdentifier(.BodyMassIndex)

enum として import された型は、他のモジュールや利用者側のコードから新しいケースを追加することはできません。

swift_wrapper(struct): 拡張可能な定数群を構造体として import する

NSError のドメインのように、フレームワーク外からも新しい値を追加したい定数群には swift_wrapper(struct) を指定します。

typedef NSString * NSErrorDomain __attribute__((swift_wrapper(struct)));

FOUNDATION_EXPORT NSErrorDomain const NSCocoaErrorDomain;
FOUNDATION_EXPORT NSErrorDomain const NSPOSIXErrorDomain;
FOUNDATION_EXPORT NSErrorDomain const NSOSStatusErrorDomain;
FOUNDATION_EXPORT NSErrorDomain const NSMachErrorDomain;

Swift 側では RawRepresentable に適合した struct として import され、各定数はその型の static プロパティになります。

struct NSErrorDomain: RawRepresentable {
    typealias RawValue = String

    init(rawValue: RawValue)
    var rawValue: RawValue { get }

    static var Cocoa: NSErrorDomain { get }
    static var POSIX: NSErrorDomain { get }
    static var OSStatus: NSErrorDomain { get }
    static var Mach: NSErrorDomain { get }
}

別モジュールが同じ typedef に対して新しい定数を宣言すると、それは Swift からは extension による static プロパティの追加として見えます。

extern NSString * const NSURLErrorDomain;
extension NSErrorDomain {
    static var URL: NSErrorDomain { get }
}

これにより、列挙型のような type-safe な使い勝手を保ちながら、必要に応じて利用者側で値を増やすことも可能になります。

定数名のネーミング

定数を enum のケース名や struct の static プロパティ名に変換する際は、型名と各定数名の共通接頭辞・接尾辞を取り除き、名前は大文字始まりになります。たとえば HKQuantityTypeIdentifierBodyMassIndex は型名 HKQuantityTypeIdentifier を削って BodyMassIndex になります。結果として、既存の Swift API における NSSortOptions.ConcurrentNSSearchPathDirectory.ApplicationDirectory のような名前付けと揃った見た目になります。

既存コードへの影響

属性を付けていない既存の Objective-C コードはこれまで通り import されるため、影響はありません。属性を新しく付与した API を利用していた Swift コードは、定数を直接渡す書き方へ更新する必要があります。ただし元の Objective-C 定数名がそのまま Swift 側の名前の素になっているため、機械的に追従しやすい変更となっています。