2014年6月6日金曜日

Swift の enum型を for-in でイテレーションする方法

例えばJavaのEnum型などはそのまま以下のようにイテレーションすることが可能なのですが、

なぜかSwiftのenum型はそのままではイテレーションすることができません。対策としてGeneratorという仕組みが標準ライブラリに用意されてますので、それを使ってenumをイテレーションできるようにします。

具体的には、Generatorを継承したクラスを作成して next() -> Element? を実装してください。ElementはAnyObjectのtypealiasなので実際には好きな型を返していただければOKです。あとはSequenceOf<T>型でGeneratorをラップしてあげればOKです。next()メソッドがnilを返すまでSequenceOf<T>はイテレーションを続けてくれます。

以下にサンプルコードを示します。

Generator内部でyieldが使えれば便利なんですが、おそらくyield構文は無さそうです。Enum型を一覧したい場合以外にもGeneratorは便利に使えますのでぜひお試しください。

Swift を使ってみてがっかりした点まとめ



数日間iOS8/Xcode6/Swiftな環境で色々試してみて、Swiftを使っていて思ったよりがっかりした点が多かったのでちょっとまとめてみようと思います。

動的な処理がSwiftだけでは一切できない

[NSObject performSelector:]の類と、NSInvocationがSwiftからは一切呼び出せません。使おうとすると怒られます。objc/runtime.hは試していませんが、同様に直接Swift経由では呼び出せず間にObjective-Cをかます必要があるのではないかと思われます。

@optionalなprotocolが限定的にしか使用できない

具体的には@objc属性を付けないと使えません。しかしながらこのような後方互換性のためだけに存在する属性をいつまでもAppleがサポートするかは疑問が残るというのと、もう一つ以下の様な問題があります。

@objc属性のついたSwiftの型はただのObjective-Cクラスになる


こういう問題があるのであまり使いたいとは思えません。ちなみになのですがCocoaのクラスはほぼすべて@objc属性が付いているため、それを継承して使うことになるアプリでは事実上Swiftの本来の能力を出せないのではないかと思っていますが、実際のところはわかっていません・・・

メモリ管理が相変わらず必要

Swiftのメモリ管理はGCではなくARCでありただの参照カウント方式にすぎないため、Swiftでも循環参照が発生しないようにプログラマが明示的に参照の種類を指定しなければなりません。その上Objective-Cでも存在したstrong, weakに加えunownedという新しい種類のメモリ管理が追加されています。これはweakは参照が消滅するとnilにするという挙動であるためOptional型を使わなければならないのに対し、unownedは参照が消滅してもnilにならない代わりに通常の型がそのまま使えるというもののようです。

closureでselfをキャプチャするときの循環参照対策が相変わらず必要

いちばんがっかりしたのがこれです。Swiftはdelegateよりもclosureを使ったcallbackのほうが言語構造上向いているためclosureを大量に使うことになると思うのですが、このときselfがclosureを強参照し、closureがselfをキャプチャするようなコードを書いてしまうと、循環参照になるためメモリが開放されなくなるという問題がObjective-Cから引き続き発生します。対策としてclosure capture listと呼ばれる新たな構文が追加されています。closureの先頭、引数宣言の前に[unowned self]のような構文を追加することで、selfをunownedとしてキャプチャすることができます。

以下に使用前・使用後の例を示します。

Objective-CのweakSelfよりはマシに思えますが、とはいえこの辺りはコンパイラが自動的に対応してほしいところです(´・_・`)

2014年5月26日月曜日

Android で 画面の回転や状態の復元まで考えた Fragment の使い方のガイドライン(自分用メモ)

Fragment を使った画面を作る際に、どのように作ればうまい具合に画面の回転や状態の復元を扱えるかという自分用のメモです。

最初にまとめ

  • 基本方針として可能な限りすべての管理を当該ActivityのFragmentManagerに任せると楽
  • ActivityのフィールドとしてFragmentを保持するのはバッドノウハウな気がする
  • onCreateとonDestroyが呼び出されたからといってインスタンスが生成破棄されているとは限らない、これらはFragmentManagerのタイミング次第
最終的に実装したコードは以下のような感じになりました。

今回の発端

ActionBarのタブに2つのFragmentを格納し、片方はListView, もう片方はGoogle MapsのMapViewを突っ込むようなUIを作っていたのですが、Androidは素人なもので普通に作っていると画面回転とタブの切替時にうまいこと状態を復元するのがなかなか手こずってしまいました。というわけで良いプラクティスを考えてみることにしました。

Activity と Fragmentのライフサイクルを復習

Activityは画面回転時に一度破棄されてしまいます。このような場合はActivityのFragmentManagerが現在管理しているFragment(バックスタックに入っているものが含まれるかどうかは未検証)については、Activity破壊時にFragmentManager経由で自動的にonSaveInstanceが呼び出され、Activity復旧時に自動的にonCreateとonCreateView経由で復元が試みられます。

これとは別に、Activityは破棄されないがFragmentは破棄されるケース、例えばActionBarのタブを切り替えたりNavigationDrawerを選択するなどして同一のActivity上で別の画面Fragmentに遷移する場合もあります。

上記いずれの場合も、可能な限りテキストビューの入力内容やリストのスクロール位置、地図のカメラ位置などを保持することをユーザーから期待されるため、状態の復元が必要になります。画面を回したりタブを切り替えたらスクロール位置が先頭に戻ったらユーザーはイライラするでしょう。

状態の保存はBundleとonSaveInstanceStateを使い、復元はonCreateとonCreateViewを使うのが楽です。

解決策

画面回転などActivity自体の破棄と再生成が自動的に行われるケースであればFragmentManager管理下にあるFragmentについて自動的に再生成が試みられるため大して難しくはないと思います。タブを切り替えたりするケースについては、以下のいずれかが良さそうな気がしています。
  • 解決策1: すべてのFragmentをFragmentManagerにattachされた状態のままにし、タブが切り替えられたら見せないFragmentはhideする
  • 解決策2: 必要に応じてFragmentManagerにattach/detachを行い、そのかわり自分でBundleを作りonSaveInstanceStateを呼び出す
1のメリットはタブ切り替え時に復元がそもそも発生しないため管理が簡単です。確実に動作しますし、再生成も必要ないためパフォーマンスも良いです。デメリットはFragmentおよびFragmentが抱えるView構造をすべて保持し続けるためメモリを大量に消費します。

今回採用した解決策2のメリットはタブ切り替え時にFragmentのView構造をすべて捨てるためメモリが効率的です。MapViewはどうしてもメモリを大量に使うためいくらhide状態とはいえあまり他のタブの後ろにおいておきたくはなかったのでこうしました。デメリットはやはり複雑になります。今回はFragmentのインスタンスフィールドとして一時的にBundleを保持していますが、これは正直なぜFragmentがタブから外れてDetachされてDestroyされてるのにメモリ上に残ってるのかわかりづらい変な挙動になるので、Activity側かまたは何らかのマネージャクラスに任せてしまうべきではないかと思います。・・・ってそれがFragmentManagerなんですけど。もっとうまいやり方で出来そうな気がするんですが・・・