update.iOSのプログラムを書いていると度々遭遇する憂鬱な場面。その一つが異なるオブジェクト間でのデータ共有です。あるボタンをタップしたらモーダルビューを表示する処理を考えます。モーダルビューには表示元が保持しているあるデータを使った処理が存在し、モーダルでユーザが処理したデータをまた表示元で更新して使いたいとします。iOSの教科書ではこういった場面の時に@prorpertyとdelegateを使った親子間のデータ共有パターンをよく目にします。以下の様なコードです。referenceなどという馴染みのない概念を覚えさせられるわけですが、このコードは高度に複雑化したプロダクションレベルのコードでは物足りない方法です。1つめの理由は、senderからrecieverに送るデータが任意個あったり、送るデータの型がバラバラだったりする場合です。そもそも送るデータの数が決まっていたとしても、文字列のプロパティ10個をわざわざrecieverに実装する意味も薄いと思います。プロパティはオブジェクトが保持すべき(外部からのreadを前提に作られた)データであるわけですから、一方向のデータ窓口として使うには勝手が悪いわけです。2つ目は、データ共有の双方向性がないということです。上記の例でも、オブジェクト間のオーナーシップが対等でない(Strong-Weak-Relationship)という理由で、ModalからParentへデータを返す処理がdelegateで行われていますが、わざわざ文字列一つを受け取るためだけにプロトコルを宣言してメソッドを実装する手間を考えると憂鬱な気分になります。そもそも2つのオブジェクトがまったく別の場所に存在していて、オブジェクト間の参照もない場合などにデータを受け渡すことができなくなるという点もネックです。比較的規模が大きいアプリケーションではシングルトンオブジェクトやMasterViewControllerなどが乱立し、もういま誰がどこでなにやってんのかわかんないみたいな状態に陥りやすい(私の職場だけ?)。3つめは、任意個のオブジェクト間でデータ共有ができないという点です。言ってこれが一番やっかいです。そもそも、データの共有は2つのオブジェクト間だけで行われるというよりは、Shareという名前からして2つ以上のオブジェクト間でなされるものです。先の例でも、ParentとModalの間で受け渡されたデータは実はSomeやOtherも興味があるかもしれません。stringByAppendingString:@"_suffix"]];}こんなこといちいちやってられるか!と叫びたくなったプログラマーは私だけではないはずです。無茶な要求を実装で実現するために致し方ないような気もしてきます。オブジェクト設計はプロダクションレベルのコードではなかなか修正ができず、とはいえ仕様や機能はどんどん変更されていきますから、オブジェクトは増える一方で、オブジェクト間の関係性はデータ共有という目的のためだけに蜘蛛の巣のように増えていきます。疎結合の原則はどこへ行ってしまったのでしょう?疎結合の重要性オブジェクト間に明確な親子関係ーーそれも一人っ子が望ましいーーがある場合は、親は子の面倒を思い切りみて甘やかしていいのです。子供のために専用のプロパティを作ってあげたり、デリゲートを実装してあげたりといった感じで。それ以外の関係ーーRelationというよりはもはやGraphのようなものが不可避になった場合、オブジェクト間のデータ共有を各個に任せるのは危険だと言わざるを得ません。それはもう終わりの始まりです。各個でデータをやりとりすると、ある人には送ったけどある人には送らなかった、といった通信の非対称性が発生するからです。ある人があるデータが必要になった時に「すいません、次から私にも送ってもらえますか」と頼むのは効率がよくありません。いう場合は、DropboxでもGoogleDocsでもホワイトボードでも何でもいいのですが、ひとつの大きな共有データスペースを作り、そこにデータを書き込んでおき、必要な人がそれを必要なときに読むのが一般的です。LindaLindaという並列分散処理のためのプログラミング言語(?)があります。これは、マルチプロセス間で協調的に分散処理をするための仕様で、既存の言語に手を加えないことが特徴です。詳しい説明は以下のブログが参考になります。http://shokai.org/blog/archives/7515今回は、この考えを拡張してiOS/Macアプリケーションの中でのオブジェクト間のデータ通信/共有する方法を実装してみました。Lindaではプロセス間の通信をネームスペースベースの共有メモリ空間にデータを読み書きしてとりおこないます。各プロセスは、あるネームスペース(例:data/of/something)に対して自由にread/write(正確には名称は違うのだけど)/takeなどのデータ通信を行うことができます。readとtakeの違いはreadがただ読み込むのに対し、takeは取り出してデータを無くしてしまう点です。Lindaにはこの他にデータが現れるまでブロックしてread/takeするという仕様もあるのですが、今回は割愛しました。見たほうが早い例えばこんなUIがあったとします。この簡単そうなアプリ、実はiOS上ではかなりスマートな実装ができません。Viewの構成が以下の様な感じになっているからです。これを見てウッと胃が痛くなった人は少なくないはずです。?見ての通り、UIの操作をハンドリングするAboveViewControllerと結果を反映するBelowViewControllerに直接の関係がないからです。その上、AboveとBelowは何らかの理由でNavigationControllerにembededされたうえにContainerViewに収納されています。そのためAbove->Belowをたどるにはこんなコードを書く必要があります。rootViewController]しかも、いま適当に書いたから本当にこれでたどり着けるのかはわかりません。してこんな構成にしたと問い詰めたくなりますが、実際の開発はこんなものなのです。使うそこで先のLindaの共有データ空間の発想を使います。考え方を図にすると以下のようになります。AboveでのUIの変更は、IBActionなどのハンドラで処理し、その値を"App"というネームスペースの共有空間に書き込みます。Belowは"App"のネームスペースをKVO監視しており、対象のkeyのvalueに変更があった時にそれをLabelに反映させます。ついでにContainerViewControllerでログ出力します。これは特に意味は無いのですが、これが可能であるということには大きな意味があるのです。BelowはViewの構成上明確な親子関係が成り立っています。親子関係とは親が子を強参照し、子が親を弱参照しているということ。AboveとBelowははとこ同士ですが、実際はほとんど無関係です。Spaceを使うことによって、それら3つがあたかも同レイヤーに存在するかのようにして通信が可能になり、かつ、KVOでリアクティブなUI構築が可能になります。オブジェクト間に関係を手動で作る必要はなくなるし、Below以外にもAboveのUI操作を反映するViewが追加された時も、そちらの実装だけであとは手を加える必要がありません。逆に、Above以外からデータを操作したいときも、"App"にwriteをすればいいだけです。いうのが、理想なのですが...?これを利用して、SharedSpaceの実装を行います。実装SharedSpaceの条件として、以下が上げられます。Storeとなっているスペースに対しては、addObserver:forKeyPath:options:context:メソッドでKVOが可能であるスペースに対してread/write/takeを行ったオブジェクトは、自動的にスペースに対する所有権(強参照)を持つスペースは、すべてのオーナーが消えた場合に自動的に消える1~3は、普通に簡単です。どこかにstaticなNSMutableDictionaryを一つ用意して、その中にNSMutableDictionaryを入れ子にすれば解決するからです。そこには一つの問題があります。それは、NSDictionaryはコンテンツに対して強参照を持つため、staticなNSMutableDictionaryの中身は、明示的にremoveObjectForKey:を呼ばない限りプロセスの起動中はずっとメモリに残り続けてしまうということです。これでは、一度作ったスペースは、それが誰からも読み書きされないデータになったとしても、残り続けてしまう。NSMapTable、NSHashTableを使う4,5は、それを防ぐための条件です。スペースに対して読み書きが必要なオブジェクトは、そのスペースのデータを使う必要があるので、オーナーであるといえます。スペースは、原則としてオーナー間でのデータ共有に使うからだ。NSMapTableは、NSMutableDictionaryと同じようなインターフェイスを持ちながら、コンテンツに対して弱参照を持つという特性を持ります。これを利用して、以下のようなリレーションを作ります。*name;@end使用方法は以下のような感じ。trueしかし、このままだとこのスペース"App"の所有者はselfのみのままです。あるスペースに対して所有権を主張した場合は、以下のようにします。trueこれで4,5が達成できました。addOwnerの処理は以下のようになっています。}}当然ながらownerになるオブジェクトには、スペースに対する強参照をもつ専用のプロパティはありません。そこで、objc_setAssociateObjectを使って、強引に自分を所有させてしまいます。することで、自身はNSMapTableに弱参照されながら、複数のownerを持つことができるようになります。カテゴリ化するしかし、このような使用方法は少々面倒なので、カテゴリを作りました。valueKey:(NSString*)valueKey;@endこれを使うと、こんなことができます。自分がオーナーになっている自分で空間を作ったわけでもないのに、名前を指定して書き込んだらもうスペースが出来ていて、自分がオーナーになっています。KVOする上記で上げた@shokai氏のLinda実装で便利だと感じているのが、watchingです。Lindaの共有タプルスペースには、常にデータが流れ続けていますが、それらは読み込み可能なストリームとして存在しているため、監視ができます。データと書きこむプロセスと、データを監視するプロセスがまったく無関係でも、データをやりとりでる。CocoaにはKVO機構が組み込まれており、(少々面倒だが)おおむね便利に使うことができます。今回実装したSharedSpaceは、インターフェイスこそストリームのようだが、実際はストリームではありません。スペースの実体は、単なるNSMutableDictionaryです。NSDictinoaryは、各keyに対してKVOが可能であり、中身のオブジェクトがNSKeyValueObservingに準拠していれば、"key.to.obj"のようなkeyPathと呼ばれるドット記法でアクセスができます。-removeObserver:forKeyPath:メソッドは以下のようになっています。現在スペースに存在するkeyと未来に追加されるすべてのkeyの変化についてKVOで監視できます。すべての値をKVOするのはパフォーマンス的にどうかと思うのですが、場合によっては必要なこともあるかもしれません。ちなみに、addObserverしたオブザーバは(おそらく)dictionaryが強参照するので-deallocなどできちんとremoveObserverするようにしてください。DEMOもう忘れてるかもしれませんが、これを使って先のUI操作を解決すると、以下のようになります。詳しい実装はhttps://github.com/keroxp/KXSharedSpaceにあるので、興味があれば観てみてください。最後にこのライブラリ、結構ちゃんとテストは書いたんですが、どうもNSMapTable周りの挙動がまだ分かりきってません。http://qiita.com/keroxp/items/328f98be28a8260bc058でも書きましたが、弱参照をもったコンテナがどのように内部を扱っているのかよく分からないからです。テストでは問題ないのですが、実際の使用場面で原因不明のバグ(メモリスワップっぽい)が起こっているので、もし使う場合はあまり信用しないというか、自己責任でお願いします。cocoapodsで使う場合は以下のように記述してください。upLoginユーザーは見つかりませんでした。