Playground QuickLook API Revamp
01 何が問題だったのか
Xcode や Swift Playgrounds では、プレイグラウンド上で値がどのように表示されるかを型ごとにカスタマイズできる仕組みがありました。Swift 4.0 までは、PlaygroundQuickLook という列挙型と CustomPlaygroundQuickLookable プロトコルが標準ライブラリに置かれていて、これらを使って「この型はこう表示してほしい」と指定するようになっていました。
public enum PlaygroundQuickLook {
case text(String)
case int(Int64)
case uInt(UInt64)
case float(Float32)
case double(Float64)
case image(Any)
case sound(Any)
case color(Any)
case bezierPath(Any)
case attributedString(Any)
case rectangle(Float64, Float64, Float64, Float64)
case point(Float64, Float64)
case size(Float64, Float64)
case bool(Bool)
case range(Int64, Int64)
case view(Any)
case sprite(Any)
case url(String)
case _raw([UInt8], String)
}
public protocol CustomPlaygroundQuickLookable {
var customPlaygroundQuickLook: PlaygroundQuickLook { get }
}
しかしこの設計にはいくつもの問題がありました。
まず、PlaygroundQuickLook のケース名は Swift の命名規則に合っていません(uInt など)。また、NSImage や UIColor のような高位フレームワークの型を直接受け取ると標準ライブラリから上位フレームワークへの依存が逆転してしまうため、多くのケースが Any で型付けされており、型安全性が損なわれていました。
次に、この列挙型は PlaygroundLogger 側でサポートされていない sound のようなケースを含んでいる一方で、新しい種類の表示を追加したくても標準ライブラリと PlaygroundLogger の両方を同時に更新しなければならず、IDE(Xcode など)の表示能力を拡張するのに標準ライブラリの変更が必要になるという窮屈な関係(revlock)に陥っていました。
さらに、Swift 5 で標準ライブラリの ABI が安定化されることが決まっていたため、「不完全な API を抱えたまま ABI に固定してしまう」のを避けるラストチャンスという事情もありました。ABI に入ってしまうと、後から列挙型のケースやプロトコル要件を変更することが事実上できなくなります。
加えて、CustomPlaygroundQuickLookable は不透明な「プレビュー表現」を返すことしかできず、たとえば「この構造体の中身を配列として構造的に表示してほしい」といった用途には CustomReflectable プロトコルを別途実装する必要があり、手間が増えていました。
02 どのように解決されるのか
標準ライブラリ側の PlaygroundQuickLook と CustomPlaygroundQuickLookable は Swift 4.1 で非推奨(deprecated)とし、Swift 5 で標準ライブラリから完全に削除します。代わりに、PlaygroundSupport フレームワーク側に、よりシンプルで柔軟な CustomPlaygroundDisplayConvertible プロトコルを新設します。
/// A type that supplies a custom description for playground logging.
public protocol CustomPlaygroundDisplayConvertible {
/// Returns the custom playground description for this instance.
///
/// If this type has value semantics, the instance returned should be
/// unaffected by subsequent mutations if possible.
var playgroundDescription: Any { get }
}
ポイントは、プレイグラウンド上での「代わりに表示してほしい値」を Any 型として返すだけ、という点です。固定の列挙型に詰め直す必要はなく、String でも Int でも NSImage でも、任意の値を返せます。
使い方
これまで CustomPlaygroundQuickLookable に適合していた型は、CustomPlaygroundDisplayConvertible に置き換えます。
// 従来
extension MyStruct: CustomPlaygroundQuickLookable {
var customPlaygroundQuickLook: PlaygroundQuickLook {
return .text("A description of this MyStruct instance")
}
}
// これから
extension MyStruct: CustomPlaygroundDisplayConvertible {
var playgroundDescription: Any {
return "A description of this MyStruct instance"
}
}
Any を返すため、構造的に表示してほしい場合は配列や辞書をそのまま返すだけで済みます。これまで CustomReflectable を実装しなければ得られなかった「中身を展開した表示」も、同じプロトコルから自然に扱えます。
extension MyStruct: CustomPlaygroundDisplayConvertible {
var playgroundDescription: Any {
return [1, 2, 3]
}
}
表示の連鎖
playgroundDescription が返した値自体が CustomPlaygroundDisplayConvertible に適合している場合、PlaygroundLogger はそちらの実装を再度たどっていきます。たとえば次のコードでは、MyOtherStruct のプレイグラウンド表示は "MyStruct description for playgrounds" になります。
extension MyStruct: CustomPlaygroundDisplayConvertible {
var playgroundDescription: Any {
return "MyStruct description for playgrounds"
}
}
extension MyOtherStruct: CustomPlaygroundDisplayConvertible {
var playgroundDescription: Any {
return MyStruct()
}
}
ただし無限再帰を防ぐため、ロガー側は連鎖の深さに妥当な上限を設けてよいことになっています。
置き場所が標準ライブラリではない理由
新しいプロトコルは、標準ライブラリではなく PlaygroundSupport フレームワークに置かれます。プレイグラウンド表示のカスタマイズはプレイグラウンド環境でしか意味を持たないため、標準ライブラリのサーフェイスを広げる必要がないというのが理由です。PlaygroundSupport は ABI 安定性を保証しない(プレイグラウンドは常にソースから再コンパイルされる)ので、後から要件を調整する余地も残ります。
移行と互換性
Swift 4.1 は移行期間として位置づけられ、この期間は PlaygroundLogger が旧来の CustomPlaygroundQuickLookable も引き続き認識します。ただし、同じ型で新旧両方のプロトコルに適合している場合は、CustomPlaygroundDisplayConvertible の実装が優先されます。
Swift 5 で標準ライブラリから PlaygroundQuickLook と CustomPlaygroundQuickLookable は削除されますが、プレイグラウンド環境では互換性用の小さな shim ライブラリが自動的にインポートされ、旧 API を使った既存のプレイグラウンドはそのまま動きます。一方、通常のプロジェクトやパッケージで旧 API を参照していたコードは、Swift 5 コンパイラに切り替えた時点で意図的にソース非互換となります(互換性モードでコンパイルしても同様)。移行は手作業になりますが、CustomPlaygroundQuickLookable の利用箇所は元々かなり少ないため、多くの場合は機械的な書き換えで済みます。