じょ
某Twitter上で「CGRectはCGFloatが面倒くさい」という旨の話が流れてきたので、いろいろ考えていたらピコーンときたので。
Proxyパターン
GoFのあれです。今回はインターフェースを変えてしまうパターンの奴です。
Dynamic Member Lookup(KeyPath)
Dynamic Member LookupはSwift4.2で実装されましたが、この時はKeyをStringでとるようになっていたため、Typoをしてもコンパイル時には検出できないという致命的な欠陥がありました。
しかし、Swift5.1でKeyPathをKeyにとる方式が実装されこれが克服されました。
やり方
目的はCGFloat型のプロパティをFloat型で扱えるようにすることです。
CGPoint
CGRectにはCGPoint型のプロパティもありますので、まずこちらを実装しちゃいます。
@dynamicMemberLookup
struct Point {
var cgPoint: CGPoint
subscript(dynamicMember keyPath: WritableKeyPath<CGPoint, CGFloat>) -> Float {
get { Float(cgPoint[keyPath: keyPath]) }
set { cgPoint[keyPath: keyPath] = CGFloat(newValue) }
}
}
以上です。
CGPointにはプロパティが2つしかないので直接コンピューテッドプロパティとしてもいいのですが、Dynamic Member Lookup(KeyPath)を使った型変換Proxyが簡単にできるという例です。
ポイントはKeyPath(ここではWritableKeyPath)の型パラメータを明示してあげるというところです。
こうすることでこのsubscriptはCGPoint型のプロパティのうちCGFloatを返すKeyPath以外を受け付けなくなります。
これでCGPoint型であればFloatへのキャスト(あるいはFloatをCGFloatへ)する必要があったものが、そのままFloatとして利用可能になります。
var point = Point(cgPoint: .zero)
print(point.x)
// prints 0.0
point.x = Float(10) // 例のためFloatを明示
print(point.cgPoint.x)
// prints 10.0
CGSize
CGPointと同じです!
@dynamicMemberLookup
struct Size {
var cgSize: CGSize
subscript(dynamicMember keyPath: WritableKeyPath<CGSize, CGFloat>) -> Float {
get { Float(cgSize[keyPath: keyPath]) }
set { cgSize[keyPath: keyPath] = CGFloat(newValue) }
}
}
CGRect
最後にCGRectです。
これも同じといえば同じです。
@dynamicMemberLookup
struct Rect {
var cgRect: CGRect
subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGFloat>) -> Float {
get { Float(cgRect[keyPath: keyPath]) }
set { cgRect[keyPath: keyPath] = CGFloat(newValue) }
}
subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGPoint>) -> Point {
get { Point(cgPoint: cgRect[keyPath: keyPath]) }
set { cgRect[keyPath: keyPath] = newValue.cgPoint }
}
subscript(dynamicMember keyPath: WritableKeyPath<CGRect, CGSize>) -> Size {
get { Size(cgSize: cgRect[keyPath: keyPath]) }
set { cgRect[keyPath: keyPath] = newValue.cgSize }
}
}
こちらはCGPointをPoint、CGSizeをSizeとして扱えるようにするインターフェイスの書き換えを追加しています。
多段のドットアクセスも可能です。
var rect = Rect(cgRect: CGRect(x: 0, y: 0, width: 100, height: 23))
rect.size.width = Float(120) // 例のためFloatを明示
print(rect.cgRect)
// prints (0.0, 0.0, 120.0, 23.0)
問題点
変なエラー
readonlyなプロパティに代入を行おうとすると、ちょっと意味不明なエラーが出てしまいます。
var rect = Rect(cgRect: CGRect(x: 0, y: 0, width: 100, height: 23))
rect.height = 30 // ambiguous reference to member 'height'
これは「mutableなキーパスheightを探したけどよく分からない」的な意味合いだと思うのだが、突然これを出されると「heightプロパティ以外の何?? ほかにheightってあったっけ??」となること請け合いです。
メソッドどこ行った?
メソッドは頑張って書くしかないです。
まとめ
「CGRectのCGFloatをFloatにしたRectを作る」と見たときは一瞬簡単だと思ったんですけど、プロパティの数が無駄に多いことが簡単さレベルを大幅に下げてることに気づいた。
これがDynamic Member Lookup(KeyPath)でいとも簡単に解決して、これはほかでも使えるのではないだろうか、と感じこの記事を書いた次第です。
僕は思いついてないのですが、これを見た人がこれをヒントにすごい使い方を編み出してくれることを期待しています。