Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What is going on with this article?
@masakihori

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

More than 1 year has passed since last update.

じょ

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

4
Help us understand the problem. What is going on with this article?
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
masakihori
非えんじにあ / 素人が雰囲気で書いてます

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
4
Help us understand the problem. What is going on with this article?