タグ: Swift

iOSアプリ開発基礎ハンズオン

この資料はラビットハウス社内で開催される、iOSアプリ開発未経験者向けのハンズオン会向けに作成されたものです。このハンズオンを一通り行うことにより、iOS開発において以下のことができるようになります。

  • 基本的なUIKitコンポーネントの利用
    • UILabel: テキストの表示
    • UIButton: ボタンの表示
    • UIBarButtonItem: バーボタンの表示
    • UITextField: テキストフィールドの表示
    • UITableView: テーブルの表示
    • UITableViewCell: テーブルセルのカスタマイズ
    • StoryboardとAuto Layoutの利用: UIをデザインベースで構成する仕組み
    • プッシュ遷移とモーダル遷移: iOSの大きな2つの画面遷移パターンへの理解

お品書き

0. はじめに

 iOSアプリ開発自体は技術的にはそれほど難しくないため、他の分野のエンジニアに開発に関わってもらえるようにハンズオン会を開催しました。また、場合によっては、簡単なプログラミングができるアプリディレクターやデザイナーにも理解出来るようになるべく平易な内容とするように心がけました。 if, else などのキーワードや変数、関数などについてざっくりと理解していて、簡単な関数を実装できれば、この資料を見ながらきっと誰でもアプリ開発ができると思います。

 2時間でTODOアプリを作るところをゴールとして進めていきますが、プログラミングに慣れている方であれば1時間で終わってしまう内容かもしれません。しかしながら、iOSアプリ開発のフローは一通り体験でき、Googleで検索をしながらアプリ開発を進めていけるレベルには達するのではないかと思います。また、このハンズオンで実装を進めていくアプリのソースコードは https://github.com/53ningen/iOSHandsOn に置いてありますので、困ったときは参照してみると良いかもしれません。

それでは、iOSアプリ開発の世界に飛び込んでいきましょう。

1. 環境構築

このハンズオンを進める上で必要な Xcode 8.2 の実行環境を構築します

1.1 Xcode 8.2 の導入

 Xcode 8.2 を含めた過去のバージョンの Xcode は以下の URL からダウンロードすることができます。ダウンロード、およびインストールにはかなりの時間がかかりますので、時間に余裕のあるときにやることを強くお勧めします。

https://developer.apple.com/download/more/

1.2 .gitignore

自分がよくつかってる .gitignore は以下のような感じです

2. プロジェクトの作成、 Hello, world とその周辺

 ここでは実際に開発を進めるためのプロジェクトを作成し、簡単な文字を表示させるところまでを行います。

2.1 プロジェクトの作成

Xcode を起動し、 Create a new Xcode project を選択してください。

xcode

続いて Single View Application を選択してください。

ChooseATemplateForYourNewProject

 ProductName として、とりあえず今回のハンズオンでは iOSHandsOn を指定しておきましょう。また、Organization Name は適当に、Organization Id についてはサービスのドメイン名や、github の id を指定することが多いかもしれません。

 このあたりの指定については実際にプロダクトを AppStore へリリースするなどの際に重要になってきますが、今回のハンズオンアプリではとりあえずの値を指定しておけば良いでしょう。また、後から変更することも可能ですので、必要であればそのときに直せば良いと思います。

ChooseOptionsForYourNewProject

 以上をすすめると最後にプロジェクトを保存するパスを指定するダイアログが出てきますので、お好きな場所に保存すると良いでしょう。これでひとまず iOS アプリ開発のためのひな形を作成することができました。この状態で Command + R (アプリ実行へのショートカットキー) を押すと、iOSシミュレーターが立ち上がり、真っ白な画面が表示されると思います。

2.2 Hello, world

続いて簡単なサンプルとして画面に文字を表示させてみましょう。目標とするイメージは下図のようなものです。

 早速 Main.storyboard を選択し、右下にあるパネルからラベル(UILabel)を選択して上図のような画面を作ってみてください。ラベルに表示される文字はラベルをダブルクリックすると編集できるようになります。作業が終わったら再びシミュレーターで実行してみてください。きっとストーリーボードに入力した文字どおりの表示が出ているかと思います。しかしながら、このままでは画面回転をするときっとレイアウトが崩れてしまうでしょう。試しに Command + → キーを押して画面を回転させてみてください。

 画面が回転した場合でも上下中央位置に表示させたい場合にはConstraints(制約)を指定してあげる必要があります。ラベルを選択したあと、ストーリーボード編集画面右下のボタンを押し、下図のような設定にチェックを入れ Add 2 Constraints ボタンを押してください。それぞれ Horizontally in Container は水平方向中央揃え、Vertically in Container は上下方向中央揃えの意味となります。最後に右下5つアイコンがならんでいる部分の一番左、更新マークのボタンを押すと、追加された制約がストーリーボードに反映されます。

 この状態で再びシミュレーターを立ち上げ画面を回転させてみてください。きっと画面がどのような方向を向いていてもラベルは画面上下中央位置に表示されたのではないでしょうか。もしストーリーボードを使わずコードでUIを構成する際も同じような形で Constrain を指示してあげないと、画面回転時や異なる縦横比のデバイスでの表示がおかしくなることがありますので、デバッグ時に画面を回転させながらデザインが崩れないかをチェックしてみてください。

まとめ
* 開発中はこまめに画面回転を試し、制約のつけ忘れなどミスがないかを確認しながらすすめる

2.3 ボタンを押したら文字がかわる機能をつける

 画面にただ単に Hello, world と表示されているだけではつまらないので、ボタンを設置して、押したときの時刻が表示される機能を搭載してみましょう。まずはストーリーボードに対して下図のようにボタンを追加してください。

 続いてXcodeの右上にある ボタン(真ん中)を押してみてください。ストーリーボードとソースコードの2画面表示になったかと思います。この状態でラベルを選択したのち control キーを押しながらソースコードの適当な位置にドラッグすると下図のようなダイアログが出てくるでしょう。

 Name に label と入力し Enter を押すと @IBOutlet weak var label: UILabel! というコードが追加されると思います。これはストーリーボード上にあるラベルをコードから操作できるようにリンクしてあげたと思っていただければ大体良いと思います。ボタンに関しても同様に操作を行うと @IBOutlet weak var button: UIButton! というコードが追加されるでしょう。

 続いて、ボタンを押したときのイベント処理を書いてみましょう。 ViewController.swift を開いて、次のようなメソッドを追加してください。

 最後にボタンのタップイベント発生時にこのメソッドを呼び出すように設定するコードを追加します。

 これでボタンが .touchUpInside、つまりタップして指を離した瞬間に、先ほど定義したメソッドを呼び出すという紐付けができました。早速シミュレーターで動かして試してみてください。おそらく、ボタンを押すと現在時刻が表示されるようになったのではないでしょうか?

 ついでに、同じ仕組みを異なる方法で実装しておきましょう。一旦ストーリーボードのボタンを control キーを押しながらクリックして、New Referencing Outlet に紐付いているものを削除し、もう一度 control キーを押しながらソースコードへドラッグします。その際に下記のように ConnectionAction に変更します。この場合 Name の欄にはボタンの名前ではなく action の名前を指定することになるので buttonOnTouchUpInside など適当な名前を指定してあげてください。すると今度はメソッド定義が生成されたのではないでしょうか。この手法ではこうして生成されたメソッド内に、ボタンが押されたときの挙動を定義していくことになります。

さきほどと同じ内容を実現しようとすると最終的にコードは次のようになるかと思います。

 実装が終わったらシミュレーターで実行してちゃんと動作するかを確認してください。これで iOS で UI を構成する最も基本的なUI部品である UILabelUIButton の使い方の雰囲気は理解できたのではないでしょうか。

まとめ
* ボタンのイベント処理には @IBOutlet を使う方法と UIButton.addTarget を使う方法がある

2.4 トラブルシューティング

 この先ますます複雑なことをやるとしばしば謎のエラーがでて、うまくシミュレーターで実行できない場合がでてくるかと思います。そんな際は command + shift + alt + K キーを押して、ビルドした成果物をディレクトリごと消すということを試してみてください(mvn clean的なものになります)。また Xcode がおかしな挙動を見せてくることがしばしばありますが、たいてい再起動すれば治るかと思います。

 何か問題が生じた場合は無理に頑張ろうとせず、まず上記の2点を試してみるということを忘れずに、寛容な心で開発を進める必要があります。

3. 2種類の画面遷移

 1画面だけで構成されるアプリはほとんどなく、プロダクトとして出すものは、ほとんどの場合画面遷移が発生します。iOSの画面遷移には主に次の2つのパターンが存在します。

  1. モーダル遷移(下からビューが飛び出してくる、閉じるボタンでビューを閉じる)
  2. プッシュ遷移(右方向にビューがスタックする、スワイプで戻れる)

 iOSヒューマンインターフェースガイドラインにこの2つの遷移の使い分けの思想について書かれているので、Apple 信者の方々は是非そちらをごらんください。そうでない場合には普段のiOSアプリの利用の仕方に照らし合わせて適切な方を選んでいくと良いでしょう。例えばコンテンツの構造として 「漫画アプリ」 ⊃ 「漫画作品」 ⊃ 「漫画エピソード」 という図式が成り立つのであれば、きっと漫画アプリトップから作品への遷移と作品からエピソードへの遷移はプッシュ遷移が適しているはずです。また、この間の任意の場所でログインさせるというアクションをユーザーにさせたい場合はモーダル遷移でログイン画面をユーザーに提示し、ログインが完了したらその画面を閉じてあげると良いでしょう。

 余談になりますが、近年はデバイス自体が大型化してきており、モーダル遷移時の「閉じる」ボタンの位置が左上にあると手の大きさ的に届きにくい位置になるため、モーダルで出すビューの画面デザインをする際にこのあたりのことを気をつけると良いと思います(※ 個人の感想です)。前話が長くなってきたのでこのあたりにして、早速それぞれの遷移を実装してみましょう。

3.1 プッシュ遷移の実装

 プッシュ遷移をするためには現在の ViewControllerUINavigationController に属している必要があります(正確にいうと UINavigationControllerchildViewControllers の要素である必要ということ)。こんなこと言葉で言われてもわからないと思うので、早速作業をしながら理解していきましょう。

 ここではボタンを押すとプッシュ遷移をする仕組みを実装していきたいので、その準備段階として Main.storyboard でいままで「時刻を表示」となっていた部分を「遷移する」というテキストに変えてみましょう。続いて、ViewControllerを選択し、右カラムのメニュー Identity -> Storyboard ID の部分に Main というテキストを入力しましょう。

 そして「上部メニュー:Editor」→「Embed in」→「Navigation Controller」を選択してください。すると Navigation Controller とかかれたよく分からない画面が出現したと思います。と、同時にさきほどまでのビューの上部にも謎の灰色のスペースが生じてしまいました。とりあえず起動して動作確認してみましょう。

 上図のような状態になっているのではないでしょうか。さて、ここからプッシュ遷移を実装をしていきます。新しい画面を作るのは一旦後回しにして、まずは同じ画面に遷移するコードを書き足してみましょう。そのためにボタンのタップイベント処理のメソッド内を次のように書き換えます。

 この状態でシミュレーターで実行すると、ボタンを押したときにきちんとプッシュ遷移できるようになっているのではないでしょうか? またスワイプや左上の < ボタンで前の画面に戻れるようになっていると思います。

まとめ
* プッシュ遷移には navigationController?.pushViewController を使う

3.1.1 (プログラマ向けトピック) UINavigationController のふるまい

 UINavigationController の振る舞いについてちょっとだけ見ておきましょう。冒頭で UINavigationController は UIViewController をスタックしていくことが役割だと言いました。その様子を実際に見るためにページ遷移が終わった瞬間(viewDidAppear)に navigationControllerchildViewControllers をログ出力するコードを ViewController に仕込みます。

この状態でシミュレーター実行をし、遷移をしたり戻りながら、ログ出力を観察してみましょう。

きちんとスタック構造になっていることがわかるかと思います。

3.1.2 (プログラマ向けトピック) Storyboard と 型

 静的型付け言語になれたプログラマであれば、ViewController を取得するときのコードについて文字列で指定しているあたりに恐怖心を覚える方もいるのではないでしょうか? 実際この部分の指定を間違えると、静的チェックが走らず実行時にアプリがクラッシュします。

 この部分に対する対応策は各自で取らざるを得ないのが現状で、例えば次のようなやり方をしている人が多いです。

  1. 動作確認を徹底することによる対応(筋肉系)
  2. ViewController を生成するファクトリを作り、そのファクトリのすべてのメソッドについてテストを書き、確実にインスタンスが得られることを保証する
  3. ViewControllerStoryboard ID(または Storyboard のファイル名) の命名規則を定める、ViewController の型を受け取り、そのインスタンスを生成するファクトリメソッドを用意し、アプリ内ではそれを通してインスタンスを得る

 3. については具体的には、MainViewController に対する Storyboard の名前は必ず MainViewController にするという規則を設け、次のように ViewController の型名を取得して上記のようなことを実現するという手法になります。命名をミスれば安全でもなんでもない手法ですが、StoryboardやViewControllerの名前を変更することはまれであるため、手軽な方法ではないかと考えています。

3.1.3 (プログラマ向けトピック) Storyboard ID を使わず Storyboard に紐づくViewController を取得する

以下のようなコードで可能です。口頭で説明いたします。

3.1.4 プッシュ遷移画面から戻るボタンの実装

 プッシュ遷移をしたあとにスワイプで前の画面に戻れますが、ある条件のときに強制的に前の画面に戻したいことがあるかと思います。ここでは、ボタンを押すと前の画面に戻る機能を実装してみましょう。まずはおなじみ Main.storyboard を次のような状態に改変しましょう。またトラブル防止のため、各UIButton を control + クリックし Outlet を一旦クリアにしましょう。

 続いて、ストーリーボード上のボタンとコードを結びつける作業をもう一度やりましょう。その作業が終わったら、まず「プッシュ遷移する」ボタンについて以前やったとおりに addTarget を利用してプッシュ遷移の実装を行ってください。これがおわるとコードは以下のような状態になっているかと思います。

 つづいてポップボタンについてですが、こちらについては最初の画面では戻り先が存在しないのでボタンを無効化しておくのが親切だと思います。そのためにビューが読み込みされた時点(viewDidLoad)で navigationController!.childViewControllers.count (スタックされているViewControllerの数) が 1 より大きかったら有効にしてあげる処理を記述します。

 また、プッシュ遷移から戻る処理は navigationController?.popViewController(animated: true) という命令を呼び出してあげれば実現できるため、この処理とボタンのタップイベントを addTarget で紐付けましょう。

 シミュレーターで実行すれば期待したとおりの動作になっているのではないでしょうか。

まとめ
* プッシュ遷移からもどるときには navigationController?.popViewController を使う

3.2 モーダル遷移

 同様のノリでモーダル遷移についても実装していきましょう。モーダル遷移については UINavigationController うんぬんは一切関係なく、どのビューからもこの遷移が使えます。いつもどおり Main.storyboard に「モーダル遷移する」ボタンを、これまでの流れと同じ手順で一つ増やし、右上にモーダル画面を閉じるボタンを設置しましょう。イメージは下図のとおりです。

 例によって最初の画面では戻り先の画面が存在しないので、右上の × ボタンを無効化したくなります。モーダル遷移からの戻り先があるかどうかの判定には presentingViewController が使えます。これの中身が空っぽだった場合、戻り先がないということになります。諸々の実装は以下の通りになります。

まとめ
* モーダル遷移には present を使う
* モーダル遷移後の画面から元の画面に戻るには dismiss を使う
* モーダル遷移の戻り先の VC は presentingViewController に格納されている

4 TODO管理アプリの作成

 UITableView(テーブルビュー)はiOSアプリでしばしば見かけるUIコンポーネントのひとつです。たとえば標準のメールアプリや設定画面などに使われています。さて、ここからは簡単なTODOリスト管理アプリを作っていきましょう。TODOリストの表示にはテーブルビューが適していると思いますので、さっそく実装方法を見ていきましょう。

4.1 新しい画面の作成

 Storyboard に新たな ViewController を追加するほうほうもありますが、ここでは新しいストーリーボードと新しいViewControllerファイルを追加しましょう。TODOMainViewController.storyboardTODOMainViewController.swift を作成して以下のように画面を構成して、遷移できる状態まで持って行ってください。

 TODOMainViewController.swift のソースコードは以下のような状態になっていると良いかと思います。

これでTODO機能を実装する下地が整いました。

4.2 テーブルビューを組み込んでみる

 テーブルビューを組み込んでみましょう。TODOMainViewController.storyboardTableViewを追加して適切な constrain を設定しましょう。左・右・下方向への制約は constrain to margins のチェックボックスを外す必要がある点に注意してください。またViewController自体について Under Top Bars のチェックボックスをオフにしてください。TableViewのStyleは Grouped を選択します。

 つづいて TableView をコードに紐付ける作業を行ったあと、以下のようにコードを記述してください。tableView.dataSource はテーブルのデータを供給するための設定でここには、UITableViewDataSource プロトコルを満たす全てのインスタンスを指定することができます。また、tableView.delegate はテーブルのふるまいを指定するための設定でここには UITableViewDelegate プロトコルを満たす全てのインスタンスを指定することができます。

 
 ここで罠になるのが tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) です。ここに 0 を指定するとなぜかテーブルビュー上部に大きな余白ができます。なので、雑に対応したければ、0.1などの小さな値を指定するとよいでしょう。厳密にやりたければここに任意の数字を指定して、tableView自体を上方向にその任意の数字分ずらしてあげれば良いと思います(ホントか)。

4.3 テーブルビューのセルをスワイプできるアレ

テーブルビューのセルをスワイプできるアレを実現したいときには以下のようなコードを extension TODOMainViewController: UITableViewDelegate に追加します。

4.4 テーブルビューセルの表示を作る

 画面を構成する部品を作るには Storyboard ではなく、 xib というファイルを作ったほうが良いことが多いです(※あまりこだわりはないし、宗教的な問題だと思うので思想の強い方はブコメとかで暴れていただければ)。さっそくタスク名を表示するだけのセルを作ってみましょう。iOSHandsOn 直下に TODOMainViewTableCell.xibTODOMainViewTableCell.swift というファイルを作り、xibにViewを紐付けてください。このあとの作業はストーリーボードでやったときと一緒なので、同じノリでやって下図のような状態にしてみてください(だんだん説明が雑になってきた)。

4.5 「タスクを追加するテキストフィールド」を上部に適当に作る

もうだいたい絵を見れば実装する方法わかってきましたよね? TODOMainViewController.storyboard を以下の状態に変更して、コードベースとの紐付けもよしなにやってください(雑)。

4.6 TODOデータの入力・保存・削除の仕組みをひと通り実装する

 TODOデータを保存する仕組みとしてとりあえず、文字列のリスト([String], Array<String>) を使いましょう。注意して欲しいのは、この実装ではTODO管理機能のモーダルを dismiss するとすべてのデータが水の泡になるという点です。ちゃんとデータを保持するためのほうほうは後ほどご紹介しますが、基本的にはここで完成させたものに少し手をいれて、iOSデバイス内のファイルに保存するという形になりますので大筋の流れは同じです。というわけで、まず TODOMainViewController に以下のコードを追加しましょう。

 また、表示させるセルの数はタスクの数と同じにしたいので tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) を変更する必要があります。セルにはタスク名を表示させたいので tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) にも手を入れます

4.6.1 タスクの削除

 削除については対象のセルのインデックスと変数 tasks のインデックスが一致しているので、変数からタスクを削除してテーブルの再描画命令を呼び出してあげればよいので簡単です。削除はセルをスワイプすると出てくるボタンで実現するので、tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) に手を入れます。

シミュレータで実行すると、もうタスク削除機能がうまく動作していることがわかるのではないでしょうか?

4.6.2 タスクの追加

 テキストフィールドに文字を入力して、エンターを押した瞬間にタスクが追加されるという仕様を実装に落とし込みましょう。テキストフィールドの振る舞いは UITextFieldDelegate プロトコルを実装したクラスのインスタンスを指定することができます。

また普通のアプリだとキーボードで文字入力中にテキストフィールド以外の適当な部分をタップすると、キーボードを閉じて入力を中断することができます。その振る舞いを実装するには次のようなコードを追記すればよいと思います。

 まあこのままじゃテーブルビュー領域をタップした時にキーボードを閉じないんですけどね…。

4.6.3 ローカルへのデータ保存

あとで書く

4.7 テーブルビューのよくある実装

 テーブルビューを実装するときによく使われるのが「引っ張って更新」と「セルの逐次読み込み」なので、それぞれどのように実装されているのかを軽くおさらいしておきます。

4.7.1 引っ張って更新の実装

説明あとで書く

さりげなく非同期処理の導入をしてしまった

4.7.2 ページネーションの実装

あとで書く

5. CocoaPods の導入

 iOS開発にはしばしば依存ライブラリ管理ツールとして CocoaPods が利用されています。これは node における npm や Java における maven などのようなものです。CocoaPods は Ruby で作られていて、Mac には標準で Ruby の実行環境が入っていますが CocoaPods の最新バージョンは古い Mac に標準で入っている Ruby では動作しない可能性があります。

 そこでまず Ruby のバージョン管理ツール rbenv を導入しましょう。続いて Ruby のライブラリ管理ツールである bundler を導入し、それを利用して CocoaPods を導入します。よく分からないと思った方は、とりあえずおまじないだと思って以下のコマンドを実行していってみてください。余談ですがチーム開発においては、手元の環境で CocoaPods が正常に動くとしても新しいメンバーが開発に加わりやすいように rbenv を導入しておくと何かとスムーズで良いと思います。1

5.1 rbenv を使った Ruby 2.3.3 の導入

 rbenv とは、複数のRubyバージョンを切り替えることができるツールになります。マシンによっては OS X 標準で入っている Ruby のバージョンが古く、iOSアプリ開発によく使われる Ruby のライブラリ(gem)がうまく利用できない場合があるため、rbenv を利用して特定環境かにおけるつまづきどころをなくす意図があります。

5.2 bundler と CocoaPods の導入

プロジェクトルートに Gemfile を以下のように追加して rbenv exec bundle install を実行してください

続いて rbenv exec bundle exec pod initPodfile が生成されるので、いい感じに依存を指定して、 rbenv exec bundle exec pod install すれば良いと思います。

6. 追加課題

  • データ構造の定義(リスト/遅延リスト/スタック/キュー)
    • ユニットテストを書く
  • 電卓アプリの作成
    • その1: ボタンベースのiOS標準の電卓を目標に進める
    • その2: テキストフィールドベースでユーザーに数式を入力させ、計算結果を表示するアプリ
  • 本格的な TODO アプリを作成してください
    • タスクをどういった形で持てば拡張性を維持できる?
    • タスクがマイルストンやタグといったものに紐づくときにはどうすればよい?
    • データやロジックと UI がなるべくひっつかないようにするためにはどうしたらよい?
  • 株価/天気アプリの作成

時間が余ったら

  • URLSession を使った HTTP 通信
  • DispatchQueue を使った非同期処理
  • RxSwift を用いた非同期処理

などを試してみてください。


  1. rbenv を利用した開発フローに載せてくださった @koki_cheese さん、ありがとうございます 

UnicodeScalar とは?

docs: https://developer.apple.com/reference/swift/unicodescalar

The UnicodeScalar type, representing a single Unicode scalar value, is the element type of a string’s unicodeScalars collection.

UnicodeScalar は Unicodeスカラ値を表す値

UnicodeScalar から String への変換

Swift3について色々まとめていくスレ

String を Data に変換する

色々実験したスクショ

スクリーンショット 2016-08-12 18.50.49

Swift1系脳で出来ないと思い込んでいたことができるようになっていた件

再帰的構造を持つ enum

Swift 1 系では、再帰的構造を持つ enum を定義することはできませんでした。たとえば、

というコードを書くと、Swiftの処理系は受け付けてくれません。しかしながら、Swift2はとても素晴らしい indirect という修飾子を導入することにより、再帰的構造を持つ enum を定義できるようになりました。

また、挙動的に面白いのが、再帰的構造を持つが即座に評価されないようになっている場合、たとえば以下のような遅延ストリームについては、indirect 修飾子がなくても動くのです。これは Swift 1 系のときに挙動を試してないので、どうだったかわかりませんが、Xcode ダウンロードするのめちゃめちゃ時間掛かるし重いのでだれか試して欲しい。

にゃ〜ん…って感じですね?

関数内に定義した関数の再帰呼び出し

まあコードみればわかると思うけど、以下のようなコードが Swift 1 系では動かなかった(記憶がある…)。間違っていたらごめんなさい。でも関数内に定義した関数なんて、再帰呼び出しにくらいしか使わなくないですか?と半ギレした記憶があるので多分ほんとに動かなかったと思う。Swift2でやっと動くようになりました。

Swift 最高!!!!!!(大嘘)

Cocoaにおける同期

ひとつのアプリケーションに複数のスレッドがあると、同じリソースに対する複数スレッドから変更が意図せず干渉する場合がある。基本的かつ有効な方針は、共有リソースを減らし、スレッド間のやり取りを最小化することだ。

不変オブジェクトはスレッドセーフであるので、スレッド間で安全に受け渡しすることができる。また、そもそもあるオブジェクトが単一のスレッドからしか利用されない場合は、当たり前ではあるがなにも問題はない。Foudationの基本的なクラスはだいたいスレッドセーフになっているが、そうでないものもあるので注意が必要。

同期ツール

しかし、完全に干渉のない設計が常にできるわけではないのでその場合には同期ツールを使う。同期ツールには以下のようなものがある。

  • アトミック操作: 単純なデータ型を操作するだけの同期、スレッドをブロックしない点が特徴
  • メモリバリア: 確実に正しい順序でメモリ操作を実行させる
  • ロック: クリティカルセクションの保護ができる
  • 条件変数: 特定の条件に該当する場合にスレッド同士シグナルを送り合う

パフォーマンス

  • mutex: 0.2 μsec
  • compare and swap: 0.05 μsec

デッドロックおよびライブロックへの配慮

単一のスレッドで複数のロックを同時に取得しようとする場合は、常にデッドロックが発生する可能性がある。できるだけそういう処理を避ける。

アトミック操作

ハードウェア命令とメモリバリアにより、特定の操作を必ず完了してから、その操作の影響を受けるメモリへのアクセスが再開されるような仕組みになっている。

対応している演算は以下のとおり

  • Add: 加算
  • Increment: +1
  • Decrement: -1
  • 論理OR: OR
  • 論理AND: AND
  • 論理XOR: XOR
  • compare and swap:
    • 古い値と変数を比較して等しい場合に、新しい値を代入する
    • 比較と代入をアトミックに行う
  • test and set:
    • 変数ないのビットをテストし、このビットを1にして、元のビットの値をブール値として返す
  • test and clear:
    • 指定された変数内のビットをテストし、このビットを0にして、元のビットの値をブール値として返す

ロック

いろいろなロック

  • ミューテックス: リソースを囲む保護バリアとして機能する相互排他的なロック。一度に1つのスレッドだけにアクセスを許可するセマフォの一種。
  • 再帰ロック: ミューテックスロックの亜種。ロックを取得した単一のスレッドで、そのロックを解放する前に複数回ロックを取得できる。同じ回数アンロックをかけるとロックを解除できる。
  • 読み取り/書き込みブロック: 共有排他ロックのこと。規模の大きな操作で利用することがおおい。データの読み取りを頻繁に行いながら、部分修正するような場合にパフォーマンスが良い。POSIXスレッドを用いる。
  • 分散ロック: プロセスレベルの相互に排他的なアクセスを実装できる
  • スピンロック: 条件が真になるまでロックの条件を繰り返しポーリングする。粗相されるロック待機時間が短いマルチプロセッサシステムで用いられる。カーネルプログラミングにより実装する。

POSIXミューテックスロック

どのアプリケーションからも利用できる。

NSLock

tryLock() -> Bool とか lockBeforeDate など便利なやつもいる。

NSRecrsiveLock

同一スレッドによる複数回ロック取得が可能なロック。ロック数をカウントしているので、対応するアンロックがすべて走ってようやくロックが解除される。使い方は NSLock と同じ。再帰呼び出しが発生する場合はこれを使うべし。ただ再帰呼び出しが発生しないように実装できるならばその限りでない。

NSConditionLock

NSConditionLock は特定の値を使用してロックおよびロック解除できるミューテックスロックを定義する。

条件変数の利用

条件変数は、必要な順序に合わせて操作を進めるために使用出来るロック。ある条件で待機しているスレッドはその条件のシグナルが別スレッドから明示的に送られるまでブロックされたままになる。

NSConditionの利用

Appendix

参考文献

Apple公式のこれとかこれとかこれを読んだ。

用語の定義

ドキュメントの中では用語の定義が以下のようになされている。

  • スレッド: コードを実行する、他とは切り離されたパスのこと
  • プロセス: 動作中の実行形式コードのことで、複数のスレッドから成ることもある
  • タスク: 実行するべき処理を表す、抽象的な概念

スレッドの代替テクノロジ

  • オペレーションオブジェクト: NSOperation のインスタンス, 通常は OperationQueue に突っ込んで使う
  • Grand Central Dispatch(GCD)
  • 他、アイドル時間通知, 非同期関数, タイマー, プロセス

RxSwiftライブラリの作り方 〜Observer/Observable編〜

 RxSwiftライブラリの作り方をご紹介します。一つの記事ですべてを説明するのは非常に厳しいので、まず ObserverObservable といった基本的なコンポーネントとその周辺について、ひとつずつ作っていく流れで説明します。

注意事項

  • 以下の内容を理解しなくても RxSwift は十分使えるライブラリです
  • まだ Rx 系のライブラリを使ったことがない方は、まずライブラリを使ってみてください
    • Qiitaの記事を読むのもよいですが、公式のドキュメントExampleが充実しているのでそちらを読みながら、まずはコードを書いてみることを強くお勧めします。意外に簡単に使いどころが理解できるようになると思います。
  • 記事の内容的には Rx 系ライブラリの利用経験がなくても分かるように書いたつもりです
  • 以下の実装は RxSwift のものであり、他言語の Rx ライブラリとは実装が異なる場合があります

Observerパターンの復習

以下の問題について考えていきます

  • 【問題】 A が更新されたことを、B に通知したい
  • 【解決策】 A が B のインスタンスを保持し、B に 変更を伝える

解決策を単純に実装すると、次のような構造になると思います

 シンプルですが、問題をしっかりと解決できています。続いて「通知先が増えそうである」という条件が加わったを場合を考えてみましょう。このままでは、次のような問題が発生しそうです。

  • 通知先が増減するたびに A の内部を変更しなければならない
  • 通知先のI/Fの変更により A の内部を変更しなければならない

 この2点はともに、通知元が通知先のオブジェクトの詳細を知っていることによって生じている問題です。通知元はどうあがいても、通知先を保持しなければなりませんが、その詳細を知ったまま保持する必要はないはずです。

 したがって通知元は、必要のない情報をそぎ落とした状態で通知先のオブジェクトを保持すれば、問題が解決しそうです。つまり、通知先に共通のインターフェース(Swiftのプロトコル)を切ればよいということになります。

 また、通知元のオブジェクトの種類を増やしたいとすれば、こちらもインターフェースを切っておくと使いまわしが効いて便利です。ObserverObservable からの通知を受け取り始めるための attach というメソッドと、通知の受け取りを解除するための detach というメソッドがあれば十分でしょう。

 この形式を pull 型 Observer パターンと呼びます。Observer が通知を受けたあとに、Observable から値を引っ張ってこなければならないために pull という名前が付いています。以下のように Observable が更新時に Observer に対して値を投げるような実装も可能です。こちらは push 型とよばれています。

 この UML を Swift の実装へ単純に落とし込むことはできません。Swiftの protocol は、generic type parameter を持つことができないからです。代わりに関連型 (〜2.2: typealias, 2.3+: associatedtype) で表現する必要があります。

 ここまでくれば Observerパターンの基礎についてはなんとなく理解できるようになっているのではないかと思います。次節では RxSwift ではどのように push 型の Observer パターンに用いられる基本的なコンポーネントを構成しているかを見ていきます。

Observer, Observable を作る(Rxライブラリの下ごしらえ)

 ここからは実際に RxSwift でどのように Observer と Observable が定義されているかを見ていきましょう。基本的には push 型の Observer パターンをそのまま実装していけば良いだけです。ただしRxでは値を単純に通知するのではなく、成功(Next)、失敗(Error)、完了(Completed)という文脈をつけた イベント を通知します。また、ObserverObservable に登録した際に Disposable というI/Fを持ったオブジェクトを返し、そのオブジェクトに購読解除の機構を持たせている点も特徴的です。

したがって実現したい構造は下図のようなものになります。
 

ObserverType, ObservableType の実装

 前述したとおり、protocol には generic type parameter を用いることができないので、まず関連型を用いて ObserverTypeObservableType を以下のように定義します。

AnyObserver, Observable の実装

 さて、こうして定義した protocol を generic type parameter を用いたクラスに落とし込みます。Swiftの言語機能が不足しているので、ここは醜い表現になっていますが、 本来であればインスタンス化できないように abstract class にするような部分 だと思います。RxSwift 内では、苦し紛れですが @noreturn アノテーションを用いて抽象メソッドを表現しています。

 ここで登場するパターンは、なぜか皆さん大好きな type erasure ですが、これをやらなきゃいけないのは決して褒められたことではないと思います。個人的には普通にJavaより劣ってるでしょって感想です(←炎上しそう)。Swift の protocol が generic type parameter を持てない理由については、この記事によくまとまっているようですが、この点について自分は理解できてはいません。Swiftの言語仕様に阻まれ、随分遠回りにはなりましたが、無事 Observable<Element>Observer<Element> を定義することができました。

Observableの具象クラスを作ろう

Bag, SubscriptionDisposable の実装

 先ほど定義した Observable<Element> の実装クラスを作成してみましょう。実装が必要なメソッドは subscribe です。ここでは受け取った observer を保持する必要があります。
 
 observerO: ObserverType where O.E == E という制約のもと渡ってきますが、ObserverType のコレクションを作ることは残念ながらできません。なぜなら ObserverType は abstract type member を持っているからです。ここでは、型制約を利用して AnyObserver<E> のコレクションに突っ込んであげればよいでしょう。例が稚拙で申し訳ないのですが、Observable<String> であるような StringObservable を定義するとして、書き出しは以下のようになると思います(実際のライブラリにはこのようなクラスは存在しません)。

 subscribe に渡った observer は、observers に登録され、何かイベントがあったら通知されるようになりました。イベント購読の機能の実装はこれでとりあえずOKとしましょう(いろいろ細かい問題はありますがとりあえず置いておく)。
 
 続いてイベント購読解除の仕組みを作る必要があります。これは単に observers から購読を解除したい observer を削除してあげれば良いだけです。現状は配列に突っ込んでいますが、辞書的なものに突っ込んでキーを指定して削除できた方が、取り回しが良いでしょう。
 
 そこで、要素を追加すると同時に、 id を発行するコレクション Bag を作ります。差し当たっての問題は、どのように id を生成するのかということです。ここでは Swift の unsafeAddressOf メソッドが暗躍します。クラスのインスタンスはメモリを確保するため、インスタンスそのものがある種の id の役割を持っています。そして、メモリの番地を取得するのが unsafeAddressOf メソッドになります。

 こうしてコレクションのキーとなる BagKey の実装が終わりました。続いてコレクションの本体となる Bag について考えましょう。RxSwift の Bag の実装は要素数が少ないときに最適化されるような仕組みがのっていますが、本質だけ抜き出すとただの Key-Value store です。

 準備が整いました。もう一度やることを確認しておきましょう。Observable#subscribe では以下のことを行いたいのでした。

  1. 受け取った observerBag に突っ込む(通知先を保持する)
  2. 受け取った observerBag から一意探せるようなキーと、Observable 自身の弱参照を持ち、dispose で購読解除ができるような Disposable オブジェクトを返す

 2. のようなことができる Disposable の具象クラスとして SubscriptionDisposable というものを実装していきましょう。構造はとても単純で、以下のようなものになります。

 observer を所有している owner にキーを指定して購読解除ができなければならないので、 owner 自身は SynchronizedUnsubscribeType プロトコルに適合している必要があります。そういった部分も含めて StringObservable の実装を修正すると以下のようになります。

 これがマルチスレッド下で正しく動くかというと、またそれは別の話なのですが、とりあえず、シングルスレッド下で Observer パターンを実現させるための Rx の基本的なコンポーネントは出揃いました。利用側のコードを以下に示します。

 ここまできたらもう BehavoirSubjectVariable の実装を読むことができるようになっていると思います。マルチスレッド対応のためのロック処理なども完結にまとまっているので勉強になるコードです。次節ではその実装をみていきます。

BehaviorSubject, Variable の実装

BehaviorSubject

 前節では、String値を状態として持つ Observable の具象クラスを実装しましたが、これを一般化して任意の型の値を状態として持つような Observable があれば便利そうです。また、こうしたオブジェクトは通知元になりうると同時に、通知先になることもできそうです。この性質を SubjectType プロトコルにまとめます。普段は通知元として振る舞いますが、必要な時に通知先のインターフェースへの変換メソッドを呼び出せれば十分なので以下のように asObserver() を持っていれば大丈夫そうです。

 また、BehavoirSubject をスレッドセーフに実装するために NSRecursiveLock が使われています。これは lock している場合に同一スレッド以外からのアクセスを、unlock されるまで待たせることができるものです。使い方は、以下のように直感的なものです。

 スコープから抜けるときに必ず unlock が実行されるという点を強調するためなのか、RxSwift では defer を使った次のような書き方がちらほら見受けられます(とはいえ統一されているわけではない)。

 これらを踏まえた上で BehaviorSubject の実装をしていきましょう。基本的には前節で実装した StringObservable に対して、SubjectType, Disposable の実装と排他制御を追加しただけの構造になっています。

 クライアント側のコードは前節とほぼ同じ形となります。

Variable

 BehavoirSubjecton.Error.Completed を渡すことにより閉じることができてしまいますが、ただの値を Observable にしたいだけならそんな機能は要らないはずです。そこで、そういったインターフェースを隠蔽し、もっと変数ライクに扱えるようにしたのが Variable です。基本的に実装は、ただの変数ラッパー + asObservable のために保持しているBehavoirSubject ですが、不要なAPIの隠蔽と、変数として扱うのに便利な computed property を生やす役割を担っていると言ってよいと思います。

 こうして取り回しのきく Variable を作ることができました。RxSwift には他に PublishSubjectReplaySubject というような Subject (通知元にも通知先にもなりうるオブジェクト)が存在しますので、また機会があれば別の記事でご紹介したいと思います。また、ここまで記事の内容を理解しながら読み進めている方であれば、ソースコードを読みさえすれば何をしているか大体わかるのではないでしょうか。

まとめ

 歴史的経緯はともあれ、Rx の基本的なインターフェースである Observable, Observer については以下のように説明できると思います(自分は歴史的経緯は知らないので実際の流れは違うかもしれません)

  • push 型 Observer パターンが基本的な出発点
  • 値に next, error, completed という文脈をつけたものが push の対象物になっているのが特徴的
  • 購読解除の仕組みを Disposable に分離しているのが特徴的
  • 以上を踏まえると Observable, Observer といったインターフェースを自然に導き出すことができる

 また、Observable の実装クラスのうちのひとつである BehaviorSubjectVariable などについては次のようなことが言えます。

  • ある型のインスタンスを観測可能な状態(Observable)に簡単にリフトアップさせることのできる役割を持つ
  • 同時に、観測者側(Observer)にも変換できる状態にする役割を持つ
  • 中身は、単純に push 型 Observer パターンの Observable がやらなければならないことをスレッドセーフに実装しているだけ

 最後にもう一度書いておきますが、この記事の内容がわからなくても、RxSwift は使えますので、利用を迷っている方は、巷に飛び交う記事に惑わされず、是非公式の Example や Playground を真似して使ってみてください。

Appendix

ライセンス表記

RxSwiftはMITライセンスで公開されています。記事内のコードはライセンスに基づき、そのまま掲載している箇所や改変して掲載している箇所があります。

The MIT License Copyright © 2015 Krunoslav Zaher All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

class縛り

という感じで class で縛れる。structでは縛れない。

タグ付けできない休日

これを Swift で書こうとして無理だと悟って休日が終わりました。はぁ…。
はやく protocol 宣言に型変数をかけるようになってほしい。

Self = A を表現する言語機能が Swift には足りていないのが原因かなと思います。その部分を妥協するとしたら普通に Phantom Type 的な解決法として以下のような具合になるのかなぁと思います。

もちろん hogeId + 456 なんてものはできないですね…。残念。
しかも以下のようなことはできない。

ため息しかでない。

Swift2で静的なDIを実現する謎のソースコード

とりあえず書いたら動いたけど、なんだかよくわかっていないのでコードだけ貼っておきます。元ネタはここ。元ネタではAppContextで頑張って配線しているけど、めんどうくさいのでどうにかならんのか、と思っていろいろいじってたらこうなった。

いろいろ分かったらまとめて記事を書きます。XcodeのPlaygroundで動いたので、貼っていじってみるとよいのではないですかね。