Better Translation of Objective-C APIs Into Swift
01 何が問題だったのか
Swiftは、Clang Importerを通じてC言語やObjective-CのAPIをそのままSwiftから呼び出せるようになっています。しかし、Objective-CのCoding Guidelines for Cocoaに従って設計されたAPIをそのままSwiftに持ち込むと、SwiftのAPI Design Guidelinesとは合わず、冗長で「Swiftらしくない」書き心地になっていました。
たとえば、文字列から空白を取り除くコードはObjective-C由来のAPIをそのまま使うと次のようになります。
let content = listItemView.text.stringByTrimmingCharactersInSet(
NSCharacterSet.whitespaceAndNewlineCharacterSet())
Swiftは強い型付けと型推論、ジェネリクス、オーバーロードを備えているため、引数や戻り値の型からわかる情報をメソッド名で繰り返す必要がありません。本来であれば、次のように書けた方がSwiftの文脈には合います。
let content = listItemView.text.trimming(.whitespaceAndNewlines)
型情報の繰り返しによる冗長さ
Objective-CのCoding Guidelinesは「各引数を名前で説明する」ことを求めており、引数の型名がメソッド名の一部として繰り返されるのが一般的です。stringByTrimmingCharactersInSet: や addLineToPoint: のように、型名(String、CharacterSet、Point など)がそのままメソッド名に含まれます。
一方、Swiftのomit needless wordsというガイドラインでは、コンパイラが型としてすでに保証している情報を名前で繰り返すことは避けるべきとされています。そのため、同じAPIがObjective-Cでは自然で、Swiftでは冗長に見える、というねじれが発生していました。
デフォルト引数や引数ラベルが活用されない
Objective-Cにはデフォルト引数がないため、オプションセットや補助的な辞書、トレイリングクロージャなども常に明示的に渡す必要があります。Swiftに取り込んだあとも、機械的な変換のままではこれらを省略できず、呼び出し側のノイズが増えていました。
また、Swiftのガイドラインでは「第一引数がデフォルト値を持つならラベルを付ける」ことが推奨されていますが、Objective-Cから取り込んだメソッドはメソッド名に第一引数の意味を埋め込む慣習のため、第一引数にラベルがない状態で取り込まれ、デフォルト引数を使った呼び出しの意味が曖昧になっていました。
Boolean プロパティの命名ずれ
SwiftのBoolean assertionsガイドラインでは、真偽値のプロパティは「レシーバに対する主張」として読めるよう isEmpty のように is を前置するのが推奨です。ところがObjective-CのCoding Guidelinesはプロパティ名に is を付けることを禁じており、ゲッター側にだけ isEmpty と書き、プロパティ自体は empty と宣言するスタイルが広く使われていました。Swiftからはそのまま empty として見えてしまい、Swiftの読み味に合わなくなっていました。
値の名前の大文字小文字
Swiftでは、型以外の宣言は小文字で始めるのが原則です。しかしObjective-Cの列挙定数やプロパティは URLHandler のように大文字から始まる、あるいはすべて大文字のプレフィックスを含む名前が多く、そのままSwiftに持ち込むとガイドラインとずれていました。
比較可能な型と Comparable の不整合
NSDate や NSNumber、NSIndexPath、NSString などObjective-Cのクラスの多くは compare(_:) -> NSComparisonResult メソッドを実装し、順序比較が可能であることを表現しています。ところがSwift側では Comparable への適合として扱われないため、someDate < today のような自然な比較が書けず、someDate.compare(today) == .OrderedAscending と書くか、自分で Comparable への適合を追加する必要がありました。
02 どのように解決されるのか
Clang ImporterがObjective-Cの名前をSwiftに変換する際のルールを拡張し、Swift API Design Guidelinesに近い形に自動で整えます。変換は複数のステップの組み合わせで、それぞれに例外規則(絶対に空の名前にしない、Swiftキーワードになる名前には変換しない、など)が設けられています。以下では主なルールを順に紹介します。
なお、個別のAPIで自動変換の結果が不自然になる場合は、Objective-Cヘッダで swift_name 属性を使って明示的にSwift側の名前を指定できるようになります。swift_name の適用範囲はこの提案で拡張され、enumケースやファクトリメソッドだけでなく、CやObjective-Cの任意のエンティティに付けられます。
冗長な型名を削る
Objective-Cのセレクタ片(stringByTrimmingCharactersInSet: の stringByTrimmingCharactersInSet のような単位)には、引数や戻り値の型名を表す単語がしばしば含まれます。これらを、型情報から推測できる範囲で削ります。
たとえば、UIBezierPath の一部APIは次のように取り込まれていました。
class UIBezierPath : NSObject, NSCopying, NSCoding {
convenience init(ovalInRect: CGRect)
func moveToPoint(_: CGPoint)
func addLineToPoint(_: CGPoint)
func addCurveToPoint(_: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)
func appendPath(_: UIBezierPath)
func bezierPathByReversingPath() -> UIBezierPath
func applyTransform(_: CGAffineTransform)
var empty: Bool { get }
func containsPoint(_: CGPoint) -> Bool
func copyWithZone(_: NSZone) -> AnyObject
}
これが新しいルールでは次のように変換されます。
class UIBezierPath : NSObject, NSCopying, NSCoding {
convenience init(ovalIn rect: CGRect)
func move(to point: CGPoint)
func addLine(to point: CGPoint)
func addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)
func append(_ bezierPath: UIBezierPath)
func reversing() -> UIBezierPath
func apply(_ transform: CGAffineTransform)
var isEmpty: Bool { get }
func contains(_ point: CGPoint) -> Bool
func copy(with zone: NSZone = nil) -> AnyObject
}
型名の照合は単語境界(文字列の先頭末尾と、各大文字の直前)に合わせて行うため、NSURL のような大文字略語も自然に認識されます。また、NSString と String のようにインポート時に別名になる型、Index/Int、Indexes/IndexSet、複数形と要素型の配列なども、対応ペアとして扱われます。末尾の Type や _t サフィックス、2D のような数字+D の形もスキップして本体を照合します。
一方で、次のようなケースでは削りを行いません。
- セレクタ片を空にしてしまう場合
- 第一引数(メソッドのベース名)がSwiftキーワード(
defaultなど)になってしまう場合 - 残る名前が
get/set/with/for/usingのような意味の薄い語になってしまう場合 - 名詞句の一部だけを削ってしまい、引数の意味を誤解させる場合(例:
setTextColor(_:)からColorを落としてsetText(_:)にするのは不可) - クラスの既存プロパティと被るメソッドのベース名(例:
gestureRecognizersプロパティがあるときにaddGestureRecognizer(_:)をadd(_:)にするのは不可)
また、レシーバ型と結果型が同じで「変換版を返す」ような型保存メソッドは、セレクタ片の先頭から型名を削ります。そのあと残った By + 動名詞(例: ByApplyingTransform)の先頭 By も落とします。これによって foregroundColor.colorWithAlphaComponent(0.5) が foregroundColor.withAlphaComponent(0.5) に、rawInput.stringByApplyingTransform(...) が rawInput.applyingTransform(...) になります。
デフォルト引数を補う
シングルパラメータ以外のメソッドについて、Objective-CのシグネチャからSwift側のデフォルト引数を推測します。主なケースは次のとおりです。
- ヌラブルなトレイリングクロージャ → デフォルト値
nil - ヌラブルな
NSZone引数 → デフォルト値nil(Swiftではゾーンは事実上未使用) - 型名に
Optionsを含むオプションセット → デフォルト値[] - 名前に
options/attributes/infoを含むNSDictionary引数 → デフォルト値[:]
この結果、呼び出し側は不要な nil や [] を省略できます。
// Before:
rootViewController.presentViewController(alert, animated: true, completion: nil)
UIView.animateWithDuration(
0.2, delay: 0.0, options: [], animations: { self.logo.alpha = 0.0 }) {
_ in self.logo.hidden = true
}
// After:
rootViewController.present(alert, animated: true)
UIView.animateWithDuration(
0.2, delay: 0.0, animations: { self.logo.alpha = 0.0 }) {
_ in self.logo.hidden = true
}
第一引数ラベルを付ける
最初のセレクタ片に前置詞が含まれているときは、最後の前置詞以降を切り出して第一引数の必須ラベルにします。これにより、第一引数にデフォルト値がある場合でも呼び出し側で意図が明確になります。
// Before:
extension NSArray {
func enumerateObjectsWith(_: NSEnumerationOptions = [],
using: (AnyObject, UnsafeMutablePointer<ObjCBool>) -> Void)
}
array.enumerateObjectsWith(.Reverse) { ... }
array.enumerateObjectsWith() { ... } // With what?
// After:
extension NSArray {
func enumerateObjects(options _: NSEnumerationOptions = [],
using: (AnyObject, UnsafeMutablePointer<ObjCBool>) -> Void)
}
array.enumerateObjects(options: .Reverse) { ... }
array.enumerateObjects() { ... }
Boolean プロパティはゲッター名を使う
Objective-CでゲッターだけCocoa流に isEmpty と指定されているBooleanプロパティは、Swift側では isEmpty がそのままプロパティ名になります。
// Objective-C:
// @property (readonly, getter=isEmpty) BOOL empty;
// Swift:
extension NSBezierPath {
var isEmpty: Bool { get }
}
if path.isEmpty { ... }
値の名前を小文字化する
enumケースやオプションセットのメンバ、関数・プロパティの名前は、プレフィックスを取り除いたうえで先頭を小文字化します。URLHandler は urlHandler に、.Reverse のような列挙定数も .reverse スタイルに揃えられます。
compare(_:) を持つ型を Comparable に適合させる
compare(_:) -> NSComparisonResult を実装するObjective-Cのクラスは、Clang Importerが自動的に Comparable に適合させます。NSDate、NSNumber、NSIndexPath、NSString などがこの恩恵を受け、比較演算子で自然に書けるようになります。
// Before:
if someDate.compare(today) == .OrderedAscending { ... }
// After:
if someDate < today { ... }
既存コードへの影響と移行
このルール変更はObjective-C由来のAPIを使っているSwiftコードに広範な影響を与える、大規模なソース互換性破壊を伴う変更です。Swift 3でガイドラインに沿った名前へ一斉に切り替わるのに合わせて、Swift 2.2では -swift3-migration フラグで旧名から新名へのFix-Itを提供する暫定マイグレータが用意され、Swift 3本体にもFix-Itと旧名の二次ルックアップを組み合わせたエラーメッセージが組み込まれました。結果として、ユーザーは移行支援を受けながらSwift 3の新しい名前体系へ移行できます。