Help us understand the problem. What is going on with this article?

[Swift5.1] Dynamic Member Lookup(KeyPath)によるProxyパターンの実装(プロパティのみ)

じょ

某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)でいとも簡単に解決して、これはほかでも使えるのではないだろうか、と感じこの記事を書いた次第です。
僕は思いついてないのですが、これを見た人がこれをヒントにすごい使い方を編み出してくれることを期待しています。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした