さて本日は、ひと通り見てきた個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」の中で紹介されていた UIKit にまつわるチップス集的な技術ブログと思われる「同じような処理だけどこっちの方がいいよってやつ」を眺めてみようと思います。内容的に Swift 言語仕様の本質とは離れてくるかもしれないですけれど、興味が湧いたのと、隙あらば Swift 言語の話と絡めて紹介できたりしたら楽しいと思って、読み進めていきますね。よろしくお願いします。
⋯ と、またしても思ったのですけれど、プロパティーラッパーでそれを内包するインスタンスにアクセスする方法の話題をゆめみ社内の Slack で見かけて、極めて面白そうすぎまして。先にこちらを眺めてみようかどうか迷い始めました。
————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #285
00:00 開始 00:20 今回は @ViewLoading を自作する話 01:40 @ViewLoading と EnclosingSelf 02:58 subscript を実装すると ViewLoading を自作できる 05:12 実装の様子を眺めてみる 11:42 自作 ViewLoading のための要所 13:30 ViewLoading の独自実装は将来の機能 17:48 実際に ViewLoading を自作してみる 23:00 EnclosingSelf は参照型にする 24:58 どこが間違っているんだろう? 28:47 実際に動くコードを拝借してみる 30:59 実装実現ならず、とりあえず動作の解説をしておく 32:02 無念のクロージング —————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #285
はい、では始めていきます。今日は少し面白そうな話題を見つけましたので、それについて話していきます。先日、この勉強会でお話しした「プロパティラッパー」の話題に関連していますが、今回は別の視点から話を進めてみようと思います。
長らく気になっていた田中亮香さんのブログ記事「Swiftコードの書き換える提案」についてです。その中で紹介されていた「ビューローディング」というプロパティラッパーについての話題です。このビューローディングのプロパティラッパーがもう少し工夫できないかという話題がありました。
まず、ビューローディングプロパティラッパーについて見てみましょう。以前、例えば独自のクラスを作成し、これがUIViewControllerに所属していないときにimport UIKitを使い、UIViewControllerが持っているビューローディングをバーで採用するとビルドエラーが出る、という話をしました。このエラーは、エンクロージングセルフがUIViewControllerに関連付けられていないために発生するものです。
このエラーを解決するために、スタティックサブスクリプトを自分で実装する必要があることがわかりました。このインターフェイスによって、プロパティラッパーで囲まれた値へのアクセスをするためのキーパスやストレージが得られます。
具体例として、マジクローディングプロパティラッパー(Magic Loading Property Wrapper)というのが紹介されていました。このプロパティラッパーは、iOS 16.4やmacOS 13.3より前のバージョンでも機能するように自分で作ったものです。
細かい話として、まずソースコードを見てみましょう。NSViewControllerに関連するファイルプライベートな便利関数loadViewIfNeededを作成し、ビューがロードされていなければビュープロパティにアクセスして自動でビューのロードを発火させる、といった内容です。具体的には以下のようなコードです:
fileprivate func loadViewIfNeeded() {
if view.superview == nil {
_ = view
}
}
このように最適化がされていない旨の心配もありますが、プロパティラッパーの仕組みを活用することで安全に実装できると考えられます。今回はここまでの内容になります。他に質問や興味があるテーマがあれば、ぜひ教えてください。 とにかく、このビューモジュールに関しては、NSViewControllerをエクステンションしていますが、これが正しいのか少し疑問です。ただ、条件分岐を使っているため、問題ないのかもしれません。iOSの場合、プラットフォームビューコントローラーはUIViewControllerになります。このコードは少しアンバランスに感じます。
NSViewControllerをimport AppKitして、NSViewControllerを別の名前に代入しているのは訳が分かりにくいですね。実際の意図としては、プラットフォームビューモジュールを入れ替えたいわけではなく、ビューモジュールに入れて、その後エクステンションでNSViewControllerを拡張し、loadViewIfNeededを呼び出しています。このコードはファイルプライベートでアクセスされる特性を活かし、ビューが未ロードならビューにアクセスすることでロードを行う仕組みです。
if view.loaded判定に関しても不要ではないかと思います。ビューがロードされているため、パフォーマンスには影響しなさそうですし、このままでも良さそうです。
条件分岐から分かるように、プラットフォームビューコントローラーはiOSのみで存在するものなので、#endifで終わっています。これに関連して、プロパティラッパーやエイリアスなども定義されていますが、これもiOSのケースに限定されているようです。
テストコードについても確認します。たとえば、MagicLoadingTestやPlatformButtonTestなどがありますが、これはあくまで確認コードですね。PlatformViewControllerやUIViewControllerを使う部分についても、汎用性を持たせる必要が無さそうに思います。
iOS 16.4とmacOSを考慮したコードですが、これはクラスとしてPlatform ViewControllerを使用するためのものです。両方の動きをテストしたいのかもしれませんが、Apple Wraps VCという名前のところで不安になります。パッケージの動作やmacOSでの動作確認が必要です。条件分岐の外にエクステンションを出さないといけないのではないかという不安があります。エクステンション先がNSViewControllerでなく、Platform ViewControllerにするべきではないかと感じます。
プロパティラッパーに関しても、MagicReloadingを作成し、値を保存する場所を用意します。イニシャライザはラップドバリューで受け取りつつ、静的サブスクリプトを使って、エラーメッセージに出てきたサブスクリプトのエンクロージングセルフとラップドバリューを活用することになるでしょう。 とりあえず、これを受け取るサブスクリプトを搭載してあげると、wrappedValueにアクセスするとき、ちょっと特別なプロパティラッパーのプロパティにアクセスしたときに、wrappedValueのインターフェースではなく、このstatic subscriptのインターフェースを読んでくれます。そのときに親の参照を自動で取得してくれるという機能があります。これを使ってマジックローディングを実装できるという話なのですが、こういった実装の根拠として、プロポーザルが了解されています。この中でプロパティラッパーの話が出ているので、それを使って実装したいという背景です。
興味深いのは、この機能が将来の展望として提案されているところです。具体的には「Referencing the Enclosing Self」というところに出てきます。将来的には、この機能が使えるようになるでしょう。ただ、現時点ではまだ実用化するには早いかもしれませんが、計画されているということは、将来的には確実に使えるようになりそうです。
従来では、親の参照が必要な場合、明示的に親を渡さなければならないのが普通でしたが、この新機能ではstatic subscriptを使用します。ラベルはまだ正式には決まっていないかもしれませんが、instanceSelfやwrappedStorageなどのネーミングが検討されています。この機能が実装されると、プロパティラッパーに対する参照アクセスや書き込みアクセスが簡単になります。この二つのアクセスをサブスクリプトとして実装することで、ビューのロード処理を行い、その上でアクセスできるようになるわけです。
この機能は将来的なものですが、既にAppleが計画を立てています。したがって、大きな問題が発生しない限り、将来的に使える機能として期待して良さそうです。現時点でも一部で使えるため、個人のプロジェクトや実験的な用途で使ってみるのは良いかもしれません。しかし、ライブラリやプロダクトの中で踏み込んで使うには慎重さが必要です。将来的な互換性に懸念があるためです。
このプロポーザル258は将来的に採用される予定ですが、現在は実験的に試す程度にしておくことをお勧めします。それでも、実際に使ってみたいと思います。
インターフェース実装はやや複雑なので、サンプルコードとしてstatic subscriptを以下のように実装してプロパティラッパーを作成してみます。
まず、プロパティラッパーの定義です。どんな型でも対応できるようにしておきます。
@propertyWrapper
struct MyWrapper<T> {
private var value: T
var wrappedValue: T {
get { value }
set { value = newValue }
}
init(wrappedValue: T) {
self.value = wrappedValue
}
}
例えば、String型に特化したプロパティラッパーを作りたい場合、下記のようにします。
@MyWrapper var myProperty: String = "初期値"
これにより、プロパティラッパーMyWrapperを使って任意の型のプロパティをラップすることができます。この技術を使いこなすことで、より柔軟で再利用性の高いコードを書くことができます。 "Red" サブクラスがちょっと変ですね。"myValue"としていて、value = myValueとしていますが、あまり良い名前ではありません。しかし、こうやって実装すると、print(value)とか出来ます。これはmyValueの型です。中のvalueの名前が良くなかったですね。
次に、Stringの初期化をこのようにすると、たとえばABCが取れると思ったのですが、名前を変えたので、こうやって取れます。これをwrappedValueの代わりにstatic subscriptでやってみますね。これはenclosing selfジェネリクスになっています。ちょっとジェネリクスをやめてみますかね。こうすると、enclosing instanceになります。ここでポイントとして、アンダースコアの名前を後で紹介します。ちゃんと終わってから紹介しますね。
enclosing selfのラベルはまあいいでしょう。enclosingがmyValueですね。これをmyValue型にして、wrappedValueのキーパスとしてリファレンスします。wrappedKeyPathとして、myValue型のvalueです。今回はストリングですね。このように局所的にピンポイントでマッチするものを作っていますので、あまり気にしなくていいです。
普段はストレージとしてmyValue型のここで何をやっていたかというと、viewLoadingのバリューですね。長い名前かもしれませんが、エラーメッセージを出すときにこうなっていたんでしょう。storageはプロパティラッパーへのキーパスです。ここがselfです。自分自身、myWrapperでもいいですが、ここだとジェネリクスで書きたくなった理由です。
このプロパティラッパーの扱うバリューが戻るので、今回はストリングで条件付きになっています。この部分は状況に応じて作成し、その後セッターを搭載します。この場合、ストリングを返せばいいので、XYZとして試してみます。Zの場合は何もしないことにしてみます。これで実装が完了しました。動くかどうか試してみましょう。
ABCがどうなるかというと、initが失敗しましたね。実際のところ、先程紹介したプロパティラッパー、ウェブで紹介されたやつですね。リンクが切れてしまいましたが、このviewLoadingを再度見てみましょう。非常にわかりやすいです。
重要な制限として、enclosingSelfがプラスでなければならないという制限があるようです。まあいいでしょう。試しにプラスに変えてみます。インターフェースが間違っているのかな。クラスにして、それだけです。インシャライザーがなくなってしまいましたね。
String
Stringでself
StringがString
これで問題ありません。あと、インターフェースが間違っている可能性がありますね。これが残ってしまったからではないでしょうか。将来的には取り払えると思いますが、今は必ず搭載しなければならないのです。データレイアウトが記載されていました。呼ばれないはずなのですが、正しく書けていれば。
さて、この部分が呼ばれてしまうのは自分が間違っているのでしょう。enclosingInstance、先ほどはselfでなかったでしょうか?やはりこのコードを見るほうが良いですね。この辺りはちゃんと実装されればもっと間違いに気づきやすいと思います。
重要なのはここですね。
enclosingInstance
になっていますね。ジェネリクスがどうでも良いと思いますが、
wrap2KeypathでtのwrappedValueで
次の部分です。ここが間違っていましたね。selfと書いてしまいました。ここはself.toMyValueです。MyValue型にジェネリクスのwrappedStorageを使って、enclosingInstanceとなっているはずです。ジェネリクスではないとダメなのかもしれません。書いてみましょう。事前に調べた時、自力で書いたら動かなかったのですよね。 エラーが発生した場合でも、ラップドバリューを使えば問題なく動作するはずです。例えば、Optional 型として使うと、nil を許容することができます。また、アンラップする必要がある場合でも、通常の方法で対処できるでしょう。
ジェネリクス (generics) を使って型を柔軟にするというアイディアもあります。例えば、T を型パラメータとして使うことで、さまざまな型に対応することが可能です。ここで、value プロパティを定義し、それを返すという方法を取ることができます。
struct MyStruct<T> {
var value: T
}
また、イニシャライザーで value を設定し、ラップドバリューに格納することも可能です。以下のようなコードが考えられます。
struct MyStruct<T> {
var value: T
init(value: T) {
self.value = value
}
}
エンクロージング・インスタンス (enclosing instance) にアクセスする場合でも、ラップドプロパティを使う方法が有効です。これにより、コンテキストを管理しやすくなります。
実際にコードを動かすためには、Playground でのビルドと実行が必要です。例えば、次のようにプロパティラッパーを使用した場合、
@propertyWrapper
struct Wrapper<T> {
var wrappedValue: T
}
この場合、wrappedValue を T 型として定義し、インスタンス化して値を設定することができます。
プロパティラッパーの応用として、サブスクリプトやファクトリメソッドを活用する方法もあります。これを用いることで、より複雑な機能を簡略化できます。
今回は制約時間内にすべてを動かすことができませんでしたが、プロパティラッパーの動作確認には特に iOS 環境で試すことをお勧めします。最後に、将来的な機能がどのように拡張できるか注目しておくと、有益な情報を得られるかもしれません。
以上で今回の勉強会を終わりにしたいと思います。お疲れ様でした。ありがとうございました。