24/7 twenty-four seven

iOS/OS X application programing topics.

Auto Layoutの静的な制約で実現するカラム幅が可変のテーブル

次に示すような見出しと各カラムが右寄せ、ラベルの文字数によってカラムの幅が伸縮し、広くなった場合は隣の列を押し出し、短くなった場合は少なくとも見出しの幅に収まり、各列の間には一定のマージンを置くというテーブルレイアウトを、静的なAuto Layoutの制約だけで作ることを考えます。

https://user-images.githubusercontent.com/40610/47085526-d7d45180-d251-11e8-9293-7a82bbc6d6c7.gif


このような、UIコンポーネントが持つコンテンツの大きさによって隣接するコンポーネントを押し出すような場面ではAuto Layoutがとても効果的に働きます。

Auto Layoutなしで実現しようとすると、列ごとの各行の文字幅を計算し、最大の幅に合わせて再配置する、という処理をコンテンツが変わるたびに行うということになりますが、Auto Layoutの制約を使用する場合ではそもそもレイアウトの再計算を自分でやる必要はないので、コンテンツの変わったタイミングなどを気にする必要はありません。

ただデータを再代入するだけで適切にレイアウトが変化します。状態を監視してバインドするような仕組みがあれば完全に宣言的に書けるでしょう。

サンプルコードは下記のリポジトリで公開しています。

github.com

見出し行を作る

まず見出しとなる行を作ります。 UIViewに適当な高さ(今回の例では44pt)の制約を付けて上端と左右両端を固定します。行を示すこのビューは厳密には無くてもよいですがあったほうが作りやすいです。多くのビューに対してAuto Layoutの制約を組み立てる場合はフラットな構造よりも、適当にビューでグルーピングすると問題がシンプルになります。

高さも固定ではなくコンテンツの高さで決まるようにもできますが、今回の本質とは関係がないので簡単に44pt固定の行ということにします。

ラベルを3つ配置して、一番左のラベルは左寄せ、残りの2つは右寄せに設定します。

それぞれをタテ方向の中央配置にして、左右のラベルはそれぞれビューの左端と右端に固定、真ん中のビューは右のラベルから10pt固定のマージン、左のラベルに10pt以上のマージンの制約を付けます。

制約が成り立つかどうかだけでいうと、最後に付けた左のラベルと真ん中のラベルの10pt以上のマージンは必要ありませんが、コンテンツの幅が想定よりも大きくなったときでも、ラベル同士が重なってしまわないようにするためのものです。

f:id:KishikawaKatsumi:20181106031637p:plain

実際のStoryboardは次のようになります。

f:id:KishikawaKatsumi:20181106031824p:plain:w320

データ行を作る

データ行のレイアウトは見出し行と一緒なので見出し行のビューごとコピーして、それを修正していく形で作ると簡単です。

コピーしたビューは自身の内部の制約は保持されますが、外部との制約はすべて外れている状態なので、まず同様にビューの両端と、見出し行の下端と自身の上端を合わせる制約を付けます。

f:id:KishikawaKatsumi:20181106033042p:plain

実際のStoryboardは次のようになります。

f:id:KishikawaKatsumi:20181106033109p:plain:w320

ここで、ラベルにとても多くの文字を入力してみます。残念ながら10pt以上のマージンの制約があるにもかかわらず、ラベルのサイズがそれ以上に大きくなってしまうため制約がコンフリクトしてラベルが重なってしまいました。

f:id:KishikawaKatsumi:20181106033245p:plain:w320

この問題は、水平に並んだ2つ以上のラベルのうち、どれかのContent Compression Resistance Priorityを下げる(もしくは他のラベルのContent Hugging Priorityを上げる)ことで解決します。 そうするとそれぞれのラベルのサイズの合計が外側のビューより大きくなったとしても、外側のビューに収まるように優先度の低いラベルが小さくなって制約が解決します。

f:id:KishikawaKatsumi:20181106034208p:plain:w320

同様にして、残りの行を表すビューを追加します。行のビューはそれぞれをUIStackViewに入れるとさらに簡単です。サンプルコードではタテのレイアウトはUIStackViewに任せています。

f:id:KishikawaKatsumi:20181106034550p:plain:w320

カラム幅を最大文字数に合わせて揃える

さて、ここまでの状態ではカラム幅が不揃いなので、文字数に合わせて揃うようにしていきましょう。

f:id:KishikawaKatsumi:20181106034912p:plain:w320

見出し行と各データ行について、右端のラベルをすべて選択します。その状態で互いにEqual Widthの制約を付けます。つまり、同じ列のラベルはすべて同じ幅になるという制約になります。

f:id:KishikawaKatsumi:20181106035207p:plain:w320

この時点でStoryboardを見てわかるように、もっとも幅の大きいラベルに合わせて、列の幅が揃うようになりました。

真ん中のカラムの各ラベルに対しても、同様にEqual Widthの制約を互いに付けます。

f:id:KishikawaKatsumi:20181106035641p:plain:w320

これで完成です。 Storyboard上でラベルにいろいろなテキストを入力して意図したとおりにレイアウトが変わるかどうか試してみてください。 実行せずにさまざまな状態を確認できることはStoryboardで制約を組み立てる大きな利点です。

f:id:KishikawaKatsumi:20181106035932p:plain:w320

f:id:KishikawaKatsumi:20181106035928p:plain:w320

いかがでしょうか。文字数が多い場合でも少ない場合でもきれいに列の幅が調整され、端が揃っていることがわかります。

最初に制約を付けておくだけで、あとはコンテンツに応じて自動的に調整されるので、コードを書く必要がなく、バグが入り込む余地が少なくなります。

アップルのAuto Layout Cookbookというガイドには、今回紹介したテクニックをはじめ、具体的なレイアウトのサンプルに対する制約の例がたくさん載っています。 ひと通り手を動かしながら読んでみると、Auto Layoutをより高いレベルで使いこなせるようになるのでオススメです。

developer.apple.com

Auto Layoutの静的な制約で実現するテキストの量によって折りたたみ可能なテキストビュー

長いテキストが初期表示では折りたたまれて表示されていて、「つづきを読む」ボタンを押すことで表示エリアが拡大して全文が表示されるという挙動を、できるだけ動的な要素を排除して実現してみます。


f:id:KishikawaKatsumi:20181105035723g:plain:w320


サンプルコードは下記のリポジトリで公開しています。

github.com


今回の例ではUIStackViewを活用します。UIStackViewは内部のビューのisHiddenプロパティによってビューのサイズをゼロにできるので、うまく活用すればあたかも複数のレイアウトを切り替えているような挙動を実現できます。

テキストビュー(またはラベル)の左右両端と上端が一致するようにStack Viewに制約を付けます。

さらにテキストビューとStack Viewの高さが一致する制約を付けます。

Stack Viewの中のビューに高さの制約として折りたたんだときのサイズを指定します。例では200ptです。 テキストの分量がその高さに満たない場合はテキストの高さに合わせるべきですので、制約は等号ではなく以下を示す(<=)にしておきます。

これでテキストビューとまったく同じ位置にStack Viewが表示されます。


f:id:KishikawaKatsumi:20181105042813p:plain

Storyboardで作っている場合はここでStack Viewの中のビューのisHiddenプロパティを切り替えてみましょう。

isHiddentrue(チェック)にすると、テキストビューが本来のサイズまで高さが伸びることがわかります。

Stack Viewの中のビューのisHiddentrueになることで、Stack ViewのIntrinsic Content Sizeがゼロになります。そのため最終的にテキストビュー自身の高さにAuto Layoutが解決されます。テキストビューとStack View間の制約は、高さが同じというものですので、その場合でも問題なく制約を満たすことができています。


Stack Viewの便利な点はこのようにビューと制約の有効化・無効化をisHiddenのプロパティだけで行えることにあります。 アクションや状態の変化をisHiddenのON/OFFにマッピングすることで宣言的に異なるレイアウトに切り替えることができます。

このテクニックも応用範囲が広いので使いこなせると便利です。 下記の記事と合わせてご覧ください。

blog.kishikawakatsumi.com

Auto Layoutの静的な制約で実現する伸び縮みするヘッダービュー

TL;DR,

  • 優先度の異なる複数の制約を同時に定義することで、静的な定義だけで動的な振る舞いを実現できる
  • 動的な要素の少ない構造のビューはより堅牢である

はじめに

読みやすくメンテナンスしやすいソフトウェアを作るために重要なことの一つは構造をシンプルに保つことです。

iOSアプリのビューは壊れやすいソフトウェアの代表ですが、できるだけシンプルに作ることで変化に強い、堅牢で壊れにくいソフトウェアにできます。

動的な要素が少ないということは、ビューがシンプルであるということの指標の1つと言えます。

この記事では下記に示すような、スクロールに合わせて伸び縮みするヘッダーを、動的な要素を無くし、Auto Layoutの静的な制約のみで実現する方法を解説します。

https://user-images.githubusercontent.com/40610/47085331-48c73980-d251-11e8-8ffd-25c6c75b6044.gif

動的な要素とは、実行時におけるビューおよび制約の追加・削除、Frameや制約を更新することと、機種やスクリーンサイズ、標準UIコンポーネントのサイズなどを仮定して、条件によって処理を分岐することです。

スクリーンサイズや機種を判定して処理を変えたり、ナビゲーションバーの高さなどを固定値だと仮定したコードは変更に弱く、壊れやすいビューとなってしまいます。 また、ビューの追加削除をしたときに、制約がどのように変化するかは自明ではありません(実際にはそのビューに付いていた制約はすべて外れることと決まっていますが、それを頭の中でシミュレートしなければならないので大変です)。

動的な要素がなければ、そもそもそういったことは起こらないので、常に一定の結果を期待できます。

今回のサンプルコードは下記のリポジトリで公開しています。まずStoryboardを眺めて、動かしてみるとわかりやすいです。

github.com

サンプルコードではわかりやすさのためにすべての制約をStoryboardで記述しています。スクロールビューのcontentOffsetなどを監視するようなコードは一切ありません。ヘッダーの伸び縮みや、ナビゲーションバーの高さで固定する挙動はすべて制約によって実現されます。

ViewControllerにも処理は書かれていますが、レイアウトには関係のないスタイルの変更などであることがわかります。

Storyboardを使わなければいけないわけではなく、例えばviewDidLoad()loadView()だけに記述されていればそれは静的な制約と言えます。

それでは具体的な作り方をみていきます。

伸び縮みせず普通にスクロールするレイアウトを作る

まずは伸び縮みする効果は忘れて、Image Viewをスクロールビューの上部に普通に配置します。

f:id:KishikawaKatsumi:20181104164611p:plain:w320

スクロールビューをRoot Viewと同じ大きさになるように上下左右のエッジに合うように制約を付けます。 Image Viewをスクロールビューのサブビューに追加し、左右両端と上端に合うように制約を付けます。これだけではタテ方向の制約が足りないのでAspect Ratioの制約を追加します。

スクロールビューとサブビューの制約は通常のビューに対する制約と異なりスクロールビューのスクロール領域に対する制約になります。

スクロールビュー自体のサイズに対する制約とはならないので、スクロールビューのサイズを決定するために制約は端から端まで繋げて、サブビューのサイズでスクロールビューのスクロール領域のサイズが決定するように制約を付けます。

Image Viewはスクロールビューの左右のエッジに加えて、Viewの左右両端にも合うように制約を付けます。 下方向の制約はスクロールビューの下端まで切れ目なく繋がるように制約を付けます。

ここまでで次のような挙動のレイアウトになります。

f:id:KishikawaKatsumi:20181104172132g:plain:w320

Image Viewをスクロールによって伸び縮みさせる

これを基本形として、Image Viewがスクロールによって伸び縮みするように制約を変更していきます。 まず準備としてImage Viewをただのビュー(Image Contaner Viewとします)に置き換え、同じように制約を付けます。


f:id:KishikawaKatsumi:20181104173037p:plain:w320

これで実際にスクロールに追随して動くのはImage Viewではなく、Image Container Viewになります。 (Image Viewに付いていた制約はいったん全部外します。)

次にImage Viewをスクロールに追随せず、上部に固定したままになるように制約を追加します。

Image ViewとImage Container Viewの左右両端と下端を合わせる制約を追加します。 さらにImage Viewの上端と View の上端を合わせる制約を追加します。


f:id:KishikawaKatsumi:20181104173619p:plain:w320

この時点で下方向にスクロールしたとき、Image ViewはImage Container Viewとの制約によりImage Container Viewに合わせてスクロールしようとしますが、上端はViewとの制約により固定されているので、結果として上端は固定されたまま、下端が下がることで高さが大きくなり、スクロールに合わせて伸び縮みするようになります。

f:id:KishikawaKatsumi:20181104174245g:plain:w320

しかし、見てわかるようにこの時点では問題があり、上方向のスクロールができなくなってしまいます。(また上にスクロールした際にAutoLayoutの制約が衝突するエラーが出ていることがわかります。) これはImage ViewをViewの上端に固定したことによります。

これを直すために、AutoLayoutの優先度を調整して、制約がコンフリクトした際に適切に制約がブレイクして、別の制約に切り替わるようにします。


f:id:KishikawaKatsumi:20181104175350p:plain:w320

Image ViewとViewに付けた制約の優先度をRequired (1000)からHigh (750)に変更します。

これでこの制約を満たすことは必須ではなくなったので、上にスクロールしてImage View.Top == View.Topの制約が満たせなくなっても単にその制約が無視されるだけで問題ありません。これで以前のようにスクロールできるようになります。

Image Viewに対する上端の制約の優先度を下げたので、上にスクロールした際に高さが維持されるように、Image ViewとImage Container Viewに対して高さが同じかそれ以上(>=)になる制約を追加します。

これにより、上方向にスクロールした際は、Image View.Top == View.Topの制約をブレイクしつつ、Image ViewとImage Container Viewの高さが同じという制約は満たせるので、Image Viewは高さを維持したままスクロールに追随します。

下方向にスクロールした場合はImage View.Top == View.Topの制約とHeight >=の制約の両方が満たせるのでImage Viewは上部に固定されたままスクロールに合わせて伸び縮みするようになります。

f:id:KishikawaKatsumi:20181104180052g:plain:w320

Image Viewがナビゲーションバーの高さで固定するようにする

応用編として、先ほど紹介したテクニックを使って、上方向にスクロールした際、ナビゲーションバーと同じ高さにImage Viewが固定されて残るようにしてみましょう。

Navigation BarはViewとの直接の親子関係にないので、そのままNavigation Barと制約を付けることができません。そのため、サブビューとしてNavigation Barと同じ位置・サイズを維持するようなビューを追加します。

Navigation Barは存在する場合、その位置と大きさは、ビューの左右両端と上端、および下端がSafe Areaの上端に一致します。ビューにそのような制約を付けるとナビゲーションバーとまったく同じ位置とサイズをもつビューになります(Navigation Bar Backingとします)。


f:id:KishikawaKatsumi:20181104183143p:plain:w320

Image Viewがナビゲーションバーの位置までスクロールされてきたときに、ナビゲーションバーの下端で止まってほしいので、Navigation Bar Backingの下端に対して、ImageViewの下端を合わせる制約を追加します。これは上方向にスクロールしてナビゲーションバーの位置まできたときだけ有効になってほしいので優先度を下げます(Low (250))。これだけでは制約が足りないので、Navigation Bar BackingとImage Viewの高さを一致させるという制約を同様に優先度を下げて(Low (250))追加します。

最後に、Image ViewとImage Container Viewの高さが同じかそれ以上という制約の優先度を下げます(High (750))。

(ここまでの制約は図で示すことが難しいのでサンプルコードをみて実際に試してみてください。)

そうすると、上方向にスクロールして優先度の低い制約を満たせる状況になった際に制約が有効になり、Image Viewがナビゲーションバーの位置に固定されるようになります。 下方向にスクロールした場合は単に制約が満たせなくなり、優先度が低いため単に無視されて別の制約が満たされることになります。


f:id:KishikawaKatsumi:20181104184220g:plain:w320

まとめ

このように複数の制約が適宜ブレイクしてスイッチするように定義することで静的な制約だけでも動的な振る舞いを持たせることができます。

ここで紹介した複数の制約を優先度によって切り替えるテクニックは応用範囲が広く、使いこなせるようになると、レイアウトに関する動的な要素をかなり排除できます。

次に示す記事は、同様の内容をさらに詳しくステップbyステップで解説してくれているので、合わせて読むことをオススメします。

参考

iOSDC 2018で「堅牢なレイアウトを作るためのグッドプラクティス」というテーマで話します。

デバイス・OSバージョンの依存が少なく、メンテナンスしやすいビューを作る by Kishikawa Katsumi | プロポーザル | iOSDC Japan 2018 - fortee.jp

iOSのビューをメンテナンスし続けるのはとても大変です。

アプリケーションが提供する機能や扱う情報が複雑化するに伴って、UIも複雑になっています。

10年前とは異なり、さまざまなサイズのデバイスが使われるようになり、インタラクションの手段も増えました。 一つのアプリケーションをチームで開発することが主流になり、分担して開発する必要が出てきました。

そのような状況で、既存のコードを壊さないようにソフトウェアを継続的に改善していくということは簡単ではありません。 特に、ビューはもっとも壊れやすく、かつ壊れていることに気づくことが難しい種類のコードです。

現在私が所属しているFOLIOという会社で携わっているアプリケーションでも同じ課題を抱えています。

そこで、FOLIOのアプリケーションで実際にどのような問題・失敗があったのか、ビューはなぜ壊れやすいのか、具体的な事例を挙げながら、その問題にチームとしてどのように対処しているのか、問題に対処するための技術やツールをどう活用しているのかをお話しします。

絶対的な正解がある問題ではなく、私たちがやっていることがそのまま誰にでも応用できるわけではありませんが、技術や方法論は大いに参考にしていただけると考えています。

StoryboardとAuto Layoutは是々非々の意見があるツールですが、否定的に捉えている方にも改めて見直してみようと思っていただけるはずです。

それに先だち、私たちのチームで活用している、動くドキュメントとしてのUIコンポーネントサンプルコード集を会社のリポジトリで公開します。

github.com

そのまま使えるライブラリというわけではではありませんが、コンポーネントの分割する粒度や、@IBDesignable/@IBInspectableを活用したライブレンダリングの威力、ユニットテストの書き方など、大いに参考にできると確信しています。

日にちは最終日の9/2 13:30〜です。

デバイス・OSバージョンの依存が少なく、メンテナンスしやすいビューを作る by Kishikawa Katsumi | プロポーザル | iOSDC Japan 2018 - fortee.jp

ぜひ聴きに来てください。

iOSDCでテストしづらいコードをテストしやすくするための方法について話しました

speakerdeck.com

日本で開催されるもっとも大きなiOSに関するカンファレンスの1つであるTop | iOSDC Japan 2017に参加し、表題の内容で発表しました。

聴いてくださった方々からは好評のようでよかったです。発表資料は本題と関係のない話がちょこちょこ挟まったり、口頭の説明がないとわからないページがあり、スライドだけでは意図がよく伝わらない恐れがあるので、こちらで内容について補足します。

伝えたかったテーマは「依存が大きく複雑で、単体でテストしづらいコードを単体で動かしてテストできるようにするには」ということです。その題材として一般的に依存が複雑でテストしづらいコンポーネントであるビューを例として取り上げました。ですのでビューやUIをテストするということに絞った話ではなく、どのレイヤーに対しても複雑にいろいろな依存関係があってユニットテストが書けないという状況を改善するための基本的な考え方です。

このことをチームで定式化したりシステマチックにやるならMVVMやMVP、などのデザインパターンやフレームワークを適用するという考えに発展します。発表のあとで何人かと話したり私がいろいろなところで聞いている印象だと、MVVMやMVP、VIPERを使っているが特にテストは無い、というプロジェクトはそこそこあると思っています。

発表でも触れましたが、分割するということは最初の大きな一歩なのでそれを否定するものではありませんが、やはりVをVMやPに分けるということはテストを書けるようにするということが基本的なモチベーションであることと、テストが書けるかどうかはm本当に依存を切り離せて疎結合になっているかどうかを確かめるもっとも簡単な方法ですので、単純に分割しただけでは、何のためにそうしているのかという問いに答えるのは難しいのではないでしょうか。

発表内容について

発表では実装の一例として私がメンテナンスしているOSSの (SpreadsheetView)https://github.com/kishikawakatsumi/SpreadsheetViewというライブラリのlayout()メソッドをテストしやすい形にリファクタリングする様子を使いました。誤解しないでほしいことは、ビューを使ったのはあくまでも実装の一例であって、内容はビューだけに関係する話ではなく、一般的に複雑な依存を持つコードをテストしやすい形にするということをお話しています。実装なしで、単に「依存が大きく複雑なコードをテストするには依存を取り除く必要がある」と話してもピンとこないと思うのであくまでも実装の例として使ったということです。

テストしやすいコードとは

layout()メソッドは下記のようにSpreadsheetViewScrollViewに依存しています。layout()メソッドはSpreadsheetViewScrollViewのプロパティやメソッドを必要とするので、このメソッドを実行するにはこの2つのビューのインスタンスを用意して、正しく設定しなければならず、単体で実行することはできなくはないが、とても大変なので現実的ではないという状況です。

final class LayoutEngine {
    private let spreadsheetView: SpreadsheetView
    private let scrollView: ScrollView
    ...

    init(spreadsheetView: SpreadsheetView, scrollView: ScrollView) {
        self.spreadsheetView = spreadsheetView
        self.scrollView = scrollView

        intercellSpacing = spreadsheetView.intercellSpacing
        defaultGridStyle = spreadsheetView.gridStyle
        circularScrollingOptions = spreadsheetView.circularScrollingOptions
        ...
    }
    
    func layout() {
        guard startColumn != columnCount && startRow != rowCount else {
            return
        }
        
        let startRowIndex = spreadsheetView.findIndex(in: scrollView.rowRecords, for: visibleRect.origin.y - insets.y)
        cellOrigin.y = insets.y + scrollView.rowRecords[startRowIndex] + intercellSpacing.height
        
        for rowIndex in (startRowIndex + startRow)..<rowCount {
            ...

                scrollView.insertSubview(cell, at: 0)
        }
        ...
    }


    private func enumerateColumns(currentRow row: Int, currentRowIndex rowIndex: Int) -> Bool {
        ...
        while columnIndex < columnCount {
            ...
            scrollView.insertSubview(cell, at: 0)
        }
    }

    ...
}

つまり、単体でテスト可能にするためには依存を取り除く必要があります。

ここで依存関係には2種類あり、依存関係の状態(プロパティ)に依存している場合と、振る舞い(メソッド)に依存している場合があります。layout()メソッドは両方に依存しています。

状態の分離

状態への依存を取り除くためには状態をモデルに分離します。これは大規模であってもそれほど難しくはありません。

struct SpreadsheetViewConfiguration {
    let intercellSpacing: CGSize
    let defaultGridStyle: GridStyle
    let circularScrollingOptions: CircularScrolling.Configuration.Options
    ...
}

struct DataSourceSnapshot {
    let frozenColumns: Int
    let frozenRows: Int
    ...
}

init(spreadsheetViewConfiguration: SpreadsheetViewConfiguration,
     dataSourceSnapshot: DataSourceSnapshot,
     scrollViewConfiguration: ScrollViewConfiguration,
     scrollViewState: ScrollView.State) {
    ...
}

依存関係が持つプロパティからメソッドの実行に必要なものを抽出し、性質によって分類、3つのモデル(Struct)として分離しました。こうすることで、ビュー自体をセットアップするという複雑な手順ではなく、モデルに必要な値を設定して渡せるようになりました。

振る舞いの分離

一方、振る舞いの依存を分離するためにはモックに置き換えるという方法があります。今回はscrollView.insertSubview(cell, at: 0)というメソッドの呼び出しをモックに置き換えます。

本当のオブジェクトではなくモックを渡せるようにするには、オブジェクト自体ではなくインターフェースの依存に変更し、同じインターフェースを持つ別のオブジェクトを渡せるようにします。実装ではなくインターフェースに依存することで、別の実装を渡せるようにするということです。

scrollViewオブジェクトが持つすべてのメソッドをインターフェースに分離するのは大変で、コストが合わないかもしれないので、今回はscrollView.insertSubview(cell, at: 0)だけをモックにするというテクニックを紹介しました。

基本的なやり方は変わらず、まず共通のインターフェースを用意します。scrollViewの代わりにこのインターフェースに依存します。そうすることでこのインターフェースに適合していればscrollViewではないオブジェクトを適当に作って渡すことができます。

protocol ViewLayouter {
    mutating func layout(cell: Cell)
}

プロダクションのコードでは次のようにscrollViewを内部にもち、もともとのscrollView.insertSubview(cell, at: 0)を呼び出すオブジェクトを渡すように変更します。元の動作はまったく変わっていません。

struct Layouter: ViewLayouter {
    let scrollView: ScrollView

    func layout(cell: Cell) {
        scrollView.insertSubview(cell, at: 0)
    }
}

テストコードでは実際のビューの代わりにビューのメタデータだけを保持する別のオブジェクトを渡します。こうして依存関係を取り除き、このメソッドの実行には実際のビューを用意する必要はなくなりました。また、ビューがレイアウトされた結果をビューではなく、別のわかりやすいデータ構造で検証できるようになりました。

struct DebugLayouter: ViewLayouter {
    var cells = [CellInfo]()

    mutating func layout(cell: Cell) {
        cells.append(CellInfo(frame: cell.frame,
                              indexPath: cell.indexPath))
    }
}

まとめ

発表した内容で重要な部分をまとめました。

テストしやすいコードとは良いコードで、複雑なコードをテストしやすくするには、依存関係を分離していくことが有効です。

当たり前のことを話しているだけですが、ソフトウェア開発の複雑さに対抗する手段として、非常い広く応用できる基本的な考え方であることがおわかりいただけると思います。なんとなくMVVMやMVPを使っていたのなら、これまでよりも明確に目的を持って使えるようになると思います。

より詳しくはスライドと、後日公開される発表の録画をご覧ください。質問や批評などがありましたらいつでも連絡してください。

try! Swift Tokyo 2017をもっと楽しむために

いよいよ明日はtry! Swift Tokyo 2017が開催されます。 try! Swift Tokyo 2017を最大限楽しんでいただくために、ちょっとしたコツをお話しします。

公式アプリ

try! Swift公式アプリがAppStoreから配信されています。タイムテーブルやセッション概要などが掲載されていますので、事前にインストールしておきましょう。Apple Watchを持っていれば時計の文字盤に情報を表示することもできます。

try!

try!

  • Natalya Murashev
  • ソーシャルネットワーキング
  • 無料

ソースコードはこちらです。興味のある方はPRを送ってください。

github.com

github.com

公式Slackチャンネル

参加者のみなさんをtry! SwiftのSlackチャンネルにご招待しています。もし、招待メールが届いていない方は info@tryswiftconf.comにご連絡ください。Slackでは自己紹介や、質問など、自由に参加者および講演者の方とコミュニケーションをとっていただいて構いません。食事やお茶、オフィス見学に誘ってみるのも良いでしょう😉

来場はお早めに

会場はベルサール新宿セントラルパークです。付近のエリアには「ベルサール」と名のつく会場が3つありますので間違えないようにご注意ください。

できるだけ早めにお越しください。700人以上の方が来場されますので、開会直前に多くの人が来られますと、時間内に受付が終了しない恐れがあります。会場には軽食やお菓子を用意してますので、早めに着いてゆっくり準備をすることをお勧めします。

人と話そう

カンファレンス、と言っても講演の内容は後からスライドとビデオが公開される予定ですし、技術的なことは参加者のレポートなどを読むだけでもついていけると思います。ただ行って話を聞いて「ああいい話を聞いた」って帰ってしまうのはやはりもったいないと思います。

やはり、安くない参加費を払って、わざわざ会場に足を運ぶということは、その時しか得られない何かを持って帰るためだと思いますので、ぜひ積極的に人と話しましょう。 try! Swiftでは講演以外の直接スピーカーや他の参加者の方と話すことのできる時間をできるだけ多く設けています。

特にこのカンファレンスでは海外の講演者および参加者の方がたくさんいらっしゃいます。実はモバイル開発における日本は世界から非常に注目されていて、日本のことを知りたいと、みんな大変な興味を持っています。

Q&Aルーム

まず、すべてのスピーカーについて、講演後の1〜2時間はスピーカーに直接質問をすることができるQ&Aセッションの時間があります。Q&Aセッションはホールの外の控え室で行われるため、通常のセッションは聞き逃してしまいますが、うまく使えばセッションをただ聞くだけよりも有意義な時間の使い方になるでしょう。

MacBookを持っていけば、コードについて質問したり議論することもできます。WWDCやTech Talkに参加されたことがある方は、ラボのようなものだと思ってください。

そして、Q&Aセッションには通常のセッションと同様にプロの通訳がつきます!なので言葉に不安がある方でも問題ありません。 また通訳の費用はみなさんのチケット代、およびスポンサー料から賄われていますので、利用しなければ損です🤑

Q&Aセッションに来られる方が少なければ、いろいろ準備して日本に来られるスピーカーの方々も残念に思うでしょうし、私たちも準備したかいがありません。ぜひ積極的に話をしにいってみてください!

スピーカーディナー・懇親会

2日目(3/3)には参加者なら誰でも参加可能なオフィシャルパーティ(懇親会)があります。

パーティの会場はキリストンカフェ 東京です。セッションの話やSwiftの話で大いに盛り上がってください。

朝食・ランチ・コーヒータイム

意外と狙いどころは、セッションが始まる前の朝食タイムと、ランチタイム、そしてコーヒーブレイクの時間だと思います。こういった時間はどうしても知った顔同士で一緒になりがちですが、勇気を出して講演者の方や知らない参加者の方に声をかけてみましょう。

コーヒーやお弁当を片手に行けば自然と一緒に話をする流れになるでしょう。

人がいっぱいの懇親会に比べて、こういう時間に話した人はけっこう後になっても覚えているものです。たくさんコネクションを作って帰ってください。

通訳ボランティア

今回から参加者による通訳ボランティアを募ろうと考えています。ボランティアをやってくださる方は目印としてリストバンドをつけてもらいます。リストバンドをつけている方に声をかけて外国の方と話すときは手伝ってもらうことができます。

iPhoneに自分の作品を入れておく

try! Swiftに来ているのはもちろんiOS/OS Xのデベロッパーですし、Swiftが好きな人ばかりなので、話のきっかけさえつかめれば、楽しく話せると思います。

言葉やヒアリングが苦手でも、作ったものを見せればいいです。 みなさんはモバイルの開発者なので、iPhoneがあればすぐに作ったものをデモすることができます。来場の前にiPhoneに自分が作ったものを準備しておきましょう。

try! Swift Tokyo 2017を開催します

try! Swiftは世界中のSwiftデベロッパーが一堂に会し、Swiftに関する知見を共有するカンファレンスです。国内外からSwiftデベロッパーが参加する、世界最大級のコミュニティでもあります。 会期は2017年3月2日〜4日の3日間、うち2日、3日は招待講演とライトニングトーク、4日はハッカソンを行います。

TOKYO - try! Swift

今回はより多くの方に来場していただけるように広い会場を確保しました。およそ前回の1.5倍(800〜900人)の方にお越しいただけます。 すべての講演とQ&Aにはプロによる同時通訳を提供いたしますので、英語に自信がなくても問題なく楽しんでいただけます。

現在(Webサイト)https://www.tryswift.co/tokyo/jpには18名の講演者が掲載されていますが、さらに4名、合わせて22名の講演を予定しています。

たとえば、Fastlaneの作者であるFelix Krauseや2015年からNSHipsterの編集長を務めていてSwiftDoc.orgの作者でもあるNate Cook、CocoaPodsのOrta Theroxなど、誰でも知っているエンジニアが講演者として来日します。

彼らと直接コミュニケーションがとれる非常に貴重な機会です。みなさんふるってご参加ください!

コミュニケーション

前回の経験を活かし、ボランティアスタッフによる通訳など、コミュニケーションをサポートする仕組みについても、より充実したものにする予定です。

try! Swiftのおもしろいところとして、各カンファレンスにおいて参加者専用のSlackが用意されます。Slackには講演者の方々も参加しており、参加者同士のコミュニケーションに利用されます。海外から来られる方は、会期より長く滞在される方もたくさんいますので、ランチに誘ったりすると楽しいです!

自分から誘うのが恥ずかしくても、誰かが呼びかけてるのを見つけたら積極的に乗っかってみましょう。

下のスクリーンショットは前回のSlackの様子です。

f:id:KishikawaKatsumi:20161117140444p:plain:w480

f:id:KishikawaKatsumi:20161117140449p:plain:w380

f:id:KishikawaKatsumi:20161117140453p:plain:w260

f:id:KishikawaKatsumi:20161117140456p:plain:w400

参加費・チケット

チケットは公式サイトから購入できます。参加費にはランチやコーヒー、懇親会の費用も含まれています。 一般のチケットは最大でおよそ700枚ほどになりますが、前回よりも早いペースで購入していただいているので、早めのご購入をお勧めします。

ライトニングトーク

今回は招待講演に加え、参加者の皆さんによるライトニングトークを実施します。チケットを購入された方はライトニングトークのCFPに応募することができます。ライトニングトークの募集については、準備ができしだいお知らせしますので楽しみにお待ちください。

スポンサー募集

イベントの開催、およびコミュニティを支援していただくスポンサーを募集しています。来年も再来年もずっとこのコミュニティが成長していけるよう、ご支援をお願いします。 詳細については info@tryswift.co までお問い合わせください。担当者からご連絡いたします。

過去のtry! Swiftの各講演のビデオ

try! Swiftカンファレンスの各講演は録画されています。前回のtry! Swift Tokyoと9月に開催されたtry! Swiftニューヨークの講演は下記にてご覧いただけます。一部の講演については日本語訳も公開されています。

realm.io

懇親会における英会話のプロトコル

前回の記事では、カンファレンスをより楽しむために積極的に人と(特に海外の人と)話そうと書きました。しかしそうはいっても、言葉に自信がなかったりしてなかなか積極的に話しかける勇気が持てないかもしれません。

でも心配いりません。懇親会(ミートアップ)の会話はほとんど決まった形で始まるので、それを覚えておけばとりあえずなんとかなります。

挨拶と自己紹介のプロトコル

とりあえずこの手順だけ覚えておきましょう。以下の流れから外れることは90%ありません。

  1. (相手を見て)声をかける「Hi」
  2. 名前を言う。「I'm Katsumi」/「My name is〜」
  3. 「どこで働いてる/何をしている」か聞かれるので答える。「I'm iOS developer, work at Realm」/「I'm working at Realm. I develop〜」

要するに、1. 声をかけて、2. 名乗って、3. 自己紹介、これだけです。最初のうちは口がうまく回らないので、これだけでも大変かもしれませんが、何人か繰り返すうちにすぐに慣れます。これだけができるようになったらもう海外のミートアップでも大丈夫です。

ここまで済んだら後は流れでなんとかなります。がんばってください。大変だったらここで「また後でね」「Thank you. Talk to you later.」って言って去ってもいいですが、せっかくなのでもう少し話してみましょう。

伝わりやすい話のネタ

仕事や開発してるものの話をするのがやりやすいです。何をしている会社か説明が難しい場合は、海外の似たようなサービスの名前を出すのが伝わりやすいです。(〜と似たようなものだよ、と言うのは抵抗があるかもしれませんが、そこは割り切って)

うまく言葉が出てこなければ、自分が作ってるものや仕事で担当しているものを見せるのがいいです。動くものがある場合はデモができるように自分のiPhoneに準備しておきましょう。データとかも英語で、かつ本物っぽくしておく方が伝わりやすいです。

コードを公開している人は、自分のGitHubリポジトリを見せるのも良いです。何をするライブラリなのか、ある程度スラスラと言えるように準備しておきましょう。

大ざっぱにまとめると、まず話しかけましょう。もしくは目を合わせましょう。向こうから寄ってきます。人と話したくない人はそもそもカンファレンスや懇親会に出てこないので、気にすることありません。どんどん行きましょう。

そしてお互いに名乗って、自己紹介をしたら後はがんばってください。話を続けるのに困ったら、作ったものを動かして見せましょう。これはどこに行っても使えるテッパンネタです。

デモは事前にいくらでも準備ができるので言葉に自信がなくても大丈夫でしょう?

英語力や発音を気にしない

英語の能力や発音は一夜づけではどうにもなりません。諦めましょう。

しかし、不思議なことですが、こちらの発音や文法がどれだけ拙くても向こうの人にはちゃんと伝わっています。私たちは聞き取りにかなり苦労しているのになぜでしょうね。

とにかく、文法がおかしかろうが、単語を並べただけだろうが、通じるのでどんどん話しましょう。ちょっと失敗しても人はたくさんいます。何人かと話すとだんだん口が回ってきてあまりつっかえずに話すことができるようになります(ほんの十数分の間で😳)。

相手も母国語が英語でない人はいっぱいいますし、変な英語を話す人もいっぱいます。向こうは流暢な英語が出てこないことは十分わかっているので、いちいち英語が未熟なことについて最初に断る必要はありません。気にせず話しましょう。

もちろん細かいニュアンスが必要ならそれなりにきちんと話す必要がありますが、そう言った会話は日本語で日本人同士が話してもなかなか伝わらないものです。ましてや初対面で。そう考えると、細かいことを気にしてもしょうがないと思いませんか?

わかるまで何度でも聞き直そう

ヒアリングは、一朝一夕ではなんともなりません。何度聞き直しても聞こえない部分はどうしてもあります。 聞き返すのは別に失礼でもなんでもないので、わからなかったら遠慮なく何度でも聞き直しましょう。聞き直すには「Sorry?」と語尾を上げるだけでもいいですし、「Could you say that again?」と丁寧に言ってもいいです。

別に会議をしているわけではなくて1対1の会話なので、相手の言葉が聞き取れなければ気にせず何度でも聞き直しましょう。誰も鬱陶しく思ったりしません。 むしろほとんどの人は自分の発音やしゃべり方が悪いから聞き取れないんだと思っています。

特定の単語がどうしても聞き取れなくて意味がよく分からない、ということもよくあります。その時は相手の言葉を繰り返して「〜〜, what?」と分からなくなったところで「what?」と言えばそこだけはっきり言ってくれたり、別の言葉に言い変えたりしてくれます。

発音のバリエーションに気をつける

話す人によって、同じ単語の発音でも結構違います。Dataが「ダタ」になったりMakeが「マイク」、Arrayは「アライ」という人は結構います。備えてないと、単語が聞き取れなくて、全体の意味が分からなくなってしまいますが、頭の片隅にこのような発音の変化がある、ということを入れておけばけっこう聞き取れます。

私の最近の経験ではEither/Neitherを「アイザー/ナイザー」って言われて聞き取れなくて、なんども聞きなおすことがありました。

あとNSErrorとNSArrayが一緒に聞こえてよく分からなくなった、なんてこともありました。

困ったら頼ってください

もしどうしても最初に話しかけることができなかったり、言葉に困った場合は私やスタッフに声をかけてください。流暢に会話ができるわけではありませんが、できる限りお手伝いします。

try! Swift 2016を200%楽しむために

いよいよ今週はtry! Swift 2016が開催されます。

せっかくの機会ですので貴重なチケットを手に入れられた方にtry! Swift 2016を最大限楽しんでいただくために、ちょっとしたコツをお話しします。

公式アプリ

try! Swift公式アプリがAppStoreから配信されています。タイムテーブルやセッション概要などが掲載されていますので、事前にインストールしておきましょう。Apple Watchを持っていれば時計の文字盤に情報を表示することもできます。

try!

try!

  • Natalya Murashev
  • ソーシャルネットワーキング
  • 無料

公式Slackチャンネル

参加者のみなさんをtry! SwiftのSlackチャンネルにご招待しています。もし、招待メールが届いていない方は info@tryswiftconf.com にご連絡ください。Slackでは自己紹介や、質問など、自由に参加者および講演者の方とコミュニケーションをとっていただいて構いません。食事やお茶、オフィス見学に誘ってみるのも良いでしょう😉

来場はお早めに

会場は渋谷マークシティ、サイバーエージェントのセミナールーム13Fです。実際の行き方は少しわかりにくいので、こちらのスッキリわかる渋谷マークシティ入門!を参考にして、できるだけ早めにお越しください。500人近い方が来場されますので、開会直前に多くの人が来られますと、時間内に受付が終了しない可能性があります。会場にはサンドイッチやお菓子を用意してますので、早めに着いてゆっくり準備をすることをお勧めします。

人と話そう

カンファレンス、と言っても講演の内容は後からスライドとビデオが公開される予定ですし、技術的なことは参加者のレポートなどを読むだけでもついていけると思います。ただ行って話を聞いて「ああいい話を聞いた」って帰ってしまうのはやはりもったいないと思います。

やはり、安くない参加費を払って、わざわざ会場に足を運ぶということは、その時しか得られない何かを持って帰るためだと思いますので、ぜひ積極的に人と話しましょう。 try! Swiftでは講演以外の直接スピーカーや他の参加者の方と話すことのできる時間をできるだけ多く設けています。

特にこのカンファレンスでは海外の講演者および参加者の方がたくさんいらっしゃいます。実はモバイル開発における日本は世界から非常に注目されていて、日本のことを知りたいと、みんな大変な興味を持っています。

Q&Aルーム

まず、すべてのスピーカーについて、講演後の1〜2時間はスピーカーに直接質問をすることができるQ&Aセッションの時間があります。Q&Aセッションは17Fの別の部屋で行われるため、通常のセッションは聞き逃してしまいますが、うまく使えばセッションをただ聞くだけよりも有意義な時間の使い方になるでしょう。

MacBookを持っていけば、コードについて質問したり議論することもできます。WWDCやTech Talkに参加されたことがある方は、ラボのようなものだと思ってください。

そして、Q&Aセッションには通常のセッションと同様にプロの通訳がつきます!なので言葉に不安がある方でも問題ありません。 また通訳の費用はみなさんのチケット代、およびスポンサー料から賄われていますので、利用しなければ損です🤑

Q&Aセッションに来られる方が少なければ、いろいろ準備して日本に来られるスピーカーの方々も残念に思うでしょうし、私たちも準備したかいがありません。ぜひ積極的に話をしにいってみてください!

スピーカーディナー・懇親会

2日目(3/3)には招待制のスピーカーディナー(主にスポンサー企業の参加者やボランティアスタッフが招待されています。)、最終日(3/4)には参加者なら誰でも参加可能なオフィシャルのパーティ(懇親会)があります。

最終日の懇親会の会場はTOMBOY INDIAN LOUNGE DINING 渋谷106道玄坂店です。

慣れていなければ、懇親会で外国の方に英語で話しかけるのは難しいかもしれませんが、心配はいりません。懇親会における会話の手続きはだいたい決まっているので、それを覚えておけば大丈夫です。そのあたりのテクニックについては次の記事で紹介します。

それに懇親会なので相手は人と話そうと思っているし、前述のとおり、海外の方は日本の事情をぜひ知りたいと思ってきているので、普通にやってれば話は弾みます。

朝食・ランチ・コーヒータイム

意外と狙いどころは、セッションが始まる前の朝食タイムと、ランチタイム、そしてコーヒーブレイクの時間だと思います。こういった時間はどうしても知った顔同士で一緒になりがちですが、勇気を出して講演者の方や知らない参加者の方に声をかけてみましょう。

コーヒーやお弁当を片手に行けば自然と一緒に話をする流れになるでしょう。

人がいっぱいの懇親会に比べて、こういう時間に話した人はけっこう後になっても覚えているものです。たくさんコネクションを作って帰ってください。

iPhoneに自分の作品を入れておく

try! Swiftに来ているのはもちろんiOS/OS Xのデベロッパーですし、Swiftが好きな人ばかりなので、話のきっかけさえつかめれば、楽しく話せると思います。

言葉やヒアリングが苦手でも、作ったものを見せればいいです。 みなさんはモバイルの開発者なので、iPhoneがあればすぐに作ったものをデモすることができます。来場の前にiPhoneに自分が作ったものを準備しておきましょう。 私がユビレジにいたときはiPadを持って会場をウロウロしていました(ユビレジはiPadアプリしかないので😅)

2016年3月2日〜4日、try! Swiftカンファレンスを開催します。

try! Swiftはエンジニアが主役のSwiftに関するカンファレンスです。今回は会期を3日間(!)、著名エンジニア(海外・国内)による招待講演を予定しています。

http://www.tryswiftconf.com/

講演とプログラムについて

現在Webサイトには12人の講演者が掲載されていますが、さらに21人、合わせて33人の講演を予定しています。 会期中は、セッション以外にもオフィスアワー、アフターパーティ(懇親会)なども検討しています。

特に海外から来られる講演者の方々は皆、日本のデベロッパーとコミュニティのことを知りたいと強く考えています。 そのため、オフィスアワーや懇親会の時間以外でも、ランチタイムや朝食の時間などに講演者の方と直接話すことのできる機会を多く設ける予定です。

日本にいながら、世界のトップレベルのエンジニアの方々と直接コミュニケーションをとれる機会は非常に貴重だと考えています。 講演者の方々も日本のデベロッパーのみなさんと話すことを強く望まれていますので、ぜひ話を聞くだけでなく、積極的に質問したり、意見を交換して実りのある時間を過ごしてください。

会話に不安のある方に対しても、ボランティアスタッフによる通訳などコミュニケーションをサポートする仕組みを準備しています。初めてのことなので、完璧にはできないかもしれませんが、気後れせず、エンジニア同士のコミュニケーションを楽しんでください。

また、すべての講演について、プロフェッショナルの同時通訳を予定しています。せっかく来たのに話していることが分からなかった、ということは起こらないと思いますので安心してご参加ください。

参加費、およびチケットについて

参加費につきまして、日本で行われるカンファレンスとしては高額になってしまいましたが、3日間という期間と同時通訳や招待講演、懇親会などの費用のため、この金額になっています。無料のランチやコーヒーの費用もチケットに含まれています。

また本日から1日チケットの販売が開始しました。全日の参加が難しい方はご検討くださいませ。

個人で気軽に出せる金額ではないことは承知しておりますので、参加者の方々にはぜひ会社の支援が受けられますように願っています。

参加費だけではすべての費用を賄うことができませんので、スポンサーのお申し出も引き続き受け付けております。

講演者について

Webサイトに未掲載の講演者も含めて全員をここでご紹介します。世界トップレベルのエンジニアがこれだけ集まるカンファレンスは海外と比較しても非常に稀です。来年も同様のカンファレンスが日本で開催できるかどうかは今回の成功にかかっています。ぜひ多数のご参加をお待ちしています!

Chris Eidhof @chriseidhof

objc.ioの設立者で、Functional Programming in SwiftAdvanced Swiftの著者の一人です。みなさん、ご存知ですよね?

Daniel Eggert @danielboedewadt

objc.ioの設立者の一人です。過去5年間アップルで働いていました。

Boris Bügling @NeoNacho

言わずと知れたCocoaPodsのTriagemaster Generalです。CocoaPodsにIssueを書いたら彼からコメントをもらった方がいると思います。

Jesse Squires @jesse_squires

InstagramのiOSエンジニアです。ブログでSwift、Objective-CやiOSに関する有用な記事を数多く書いてくださっています。オープンソース活動を積極的に行っており、JSQMessagesViewControllerは6000スター以上の非常に人気のあるライブラリです。

JP Simard @simjp

RealmのiOSリードエンジニアで、JazzySwiftLintの開発者です。SwiftやObjective-CのASTに興味がある方はぜひ彼と話してください!

Jeff Hui @jeffhui

QuickNimbleのコアコミッタです。最近の講演には Swiftでの自動テストがあります。

Adam Bell @b3ll

Jailbreak界で著名なFacebookのエンジニアです。StrideIgnitionなどの脱獄アプリ開発者としても有名ですが、最近ではAppleWatchやiOS 9ハックでも有名に。

Ayaka Nonaka @ayanonagon

VenmoのリードエンジニアでSwift での自然言語処理Swift Scriptingの講演をご覧になった方も多いのではないでしょうか?東京出身で今回は初めて(!)日本語で講演をしてくださいます。

Wayne Bishop @waynewbishop

Swift Algorithms & Data Structuresの著者です。彼のWebサイトでも読むことができます。エンジニアの方は必見です。

Ash Furrow @ashfurrow

Your First Swift AppFunctional Reactive Programming on iOSなど多数の著書と、最近の講演のEmerging Best Practices in SwiftはSwiftエンジニア必見の内容です。

Cate Huston @catehstn

元GoogleかつIBM出身のモバイルエンジニアです。Girl Geek Dinners KWの主催者の一人です。

Daniel Haight @Daniel1of1

iOSデベロッパーでNSLondon、⌘R ConfAltConfの主催者の一人です。

Gwendolyn Weston @purpleyay

PlanGridで、建築用設計図の版管理をするサービスを開発しています。「アプリが寝てる間に…: Background Transfer Services」で最近の彼女の講演を見ることができます。

Kristina Thai @kristinathai

IntuitのiOSエンジニアです。今はwatchOSアプリの開発にハマっていて、彼女のブログにはwatchOSアプリの記事が多数掲載されています。スクリーンショットが多く、とてもわかりやすいので、ぜひ一度ご覧になってください。

Daniel Steinberg @dimsumthinking

ベストセラーになった「A Swift Kickstart」と「Developing iOS 7 Apps for iPad and iPhone」の著者です。(この2冊の本は、iTunes Uで人気のスタンフォード大学のiOS開発の講義の公式の参考書になっています。)iPhoneとiPadアプリの開発をSDKが出た当初から始め、Macアプリに至ってはSyntem 7の頃から携わっています。

Syo Ikeda @ikesyo

JSONマッピングライブラリHimotokiの作者で、CarthageReactiveCocoaのコミッタでもあります。最近のCarthageの改善はほとんど彼によるものと言っても過言ではありません。

Tim Oliver @TimOliverAU

コミックリーダーアプリiComicsの作者でオーストラリア出身のiOSエンジニアです。現在はRealmで働いている私の同僚です。彼の前職のPixivでも少し一緒に仕事をして奇妙な縁を感じています。日本が大好きでゲームとカラオケが得意です。ぜひ彼に日本語で話しかけてください!

Yasuhiro Inami @inamiy

ReactKitSwiftTaskの開発者です。サンフランシスコのSwiftユーザーグループでの講演「State, Promise & Reactive プログラミング」はすごく勉強になりました。

Matthew Gillingham @gillygize

Tokyo iOS Meetupの主催者です。iOSエンジニアとして5年以上のキャリアがあり、本当の意味でのフルスタックエンジニアとして活躍中です。

Hiroki Kato @cockscomb

Swift大好き、はてなのすこんぶさんです。きっと今から気合の入った準備をしてくださっています。

Yosuke Ishikawa @_ishkawa

みなさんよくご存知の石川さんです。APIKitは非常に人気のライブラリですね。

YUTA KOSHIZAWA @koher

QiitaでSwiftの関数型言語としての側面について興味深い記事を多数掲載されています。彼の記事を読んでモナドに興味を持たれた方も多いのではないでしょうか。

Hector Matos @allonsykraken

Capital OneでiOSとAndroid向けのモバイルアプリを開発しています。「Swiftのメモリ管理 - Weak、Strong、Unowned Referenceについて」の講演は非常にわかりやすいです。

Helen Holmes @helenvholmes

デザイナーで、現在はMozillaで開発ツールの改善に携わっています。try! SwiftのWebサイトも手伝っていただいています。

Jonathan Blocksom @jblocksom

Big Nerd Ranchのメンバーの一人です。彼の記事で勉強した人も多いと思います。

Maxim Cramer @mennenia

PIE Mappingのリードエンジニアで、SwiftKeyやMaximと言った人気アプリの開発者でもあります。

Michele Titolo @micheletitolo

彼女はCapital Oneのリードエンジニアであり、Women Who CodeのCTOでもあります。最近の講演はこちら「The Worst Code How to Build a Highly Effective Team」で見られます。

Veronica Ray @nerdonica

LinkedInのソフトウェアエンジニアです。ドローン、AI、iOS、セキュリティ、ウェアラブル、関数型プログラミングに興味があります。

Natalia Berdys @batalia

Tutu LabのリードiOSエンジニアです。彼女のアプリは47か国で1位を獲得しました。WWDCで講演した経験もあります。

Stephanie Shupe @steph_shupe

Lookoutのソフトウェアエンジニアで、Women Who Codeのセキュリティアドバイザーでもあります。

Diana Zmuda @dazmuda

thoughtbotに所属しているiOSエンジニアです。iOS on Railsと呼ばれるAPIと連携するモバイルアプリ開発に関する書籍の著者の一人です。また、App Camp for Girlsのインストラクターもしています。

接続先がATS (App Transport Security)に対応しているか、または例外の設定をnscurlコマンドで簡単に調べる

TL;DR,

$ nscurl --ats-diagnostics --verbose https://kishikawakatsumi.com/のようにnscurlコマンドに--ats-diagnostics --verboseオプションをつけて実行すると、指定したドメインがATSの要件を満たしているかどうかをチェックし、デフォルトの設定でエラーが起こる場合はエラー回避するための設定まで教えてくれます。

developer.apple.com


iOS 9からATS (App Transport Security)の仕組みが導入され、HTTP(HTTPSでない)通信はブロックされ、HTTPSでも接続先がATSの要件を満たしてない通信についてはデフォルトで失敗するように変更されました。

HTTPの通信はブロックされます。

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection." UserInfo={NSUnderlyingError=0x7f9bb4099c00 {Error Domain=kCFErrorDomainCFNetwork Code=-1022 "(null)"}, NSErrorFailingURLStringKey=http://kishikawakatsumi.com/, NSErrorFailingURLKey=http://kishikawakatsumi.com/, NSLocalizedDescription=The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.}


HTTPSの通信でもTLS 1.0を使用しているなど、接続先がATSの要件を満たしていない場合はエラーになります。

CFNetwork SSLHandshake failed (-9824)
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={_kCFStreamErrorCodeKey=-9824, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, NSUnderlyingError=0x7fd8e9d09ae0 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9824, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9824}}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://kishikawakatsumi.com/, NSErrorFailingURLStringKey=https://kishikawakatsumi.com/, _kCFStreamErrorDomainKey=3}


この問題に対応するベストな方法はATSの要件を満たすように接続先を対応することですが、自分が管理しているサイトではないなどの場合は、クライアント側でATSをオフにするか、ドメインごとにATSの例外を設定することで通信できるようになります。

ATSをすべてオフにしてしまうのは乱暴なので、できるだけドメインごとの例外で対処したいところです。 ドメインごとの対応状況を調べて、またATSの例外設定のPlistをどのように記述したらいいのかまで教えてくれるのがnscurlコマンドです。

下記のように--ats-diagnosticsオプションを付けてnscurlコマンドを実行します。

$ nscurl --ats-diagnostics https://kishikawakatsumi.com/


すると、次のように順に設定を変えながら接続をテストして結果が報告されます。 Result : PASSになっていれば、その設定で通信が成功したことを示します。

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://kishikawakatsumi.com/.
A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.
Use '--verbose' to view the ATS dictionaries used and to display the error received in URLSession:task:didCompleteWithError:.
================================================================================

Default ATS Secure Connection
---
ATS Default Connection
Result : PASS
---

================================================================================

Allowing Arbitrary Loads

---
Allow All Loads
Result : PASS
---

(略)

---
TLSv1.1 with PFS disabled and insecure HTTP allowed
2015-10-19 12:01:31.289 nscurl[18324:1989232] CFNetwork SSLHandshake failed (-9801)
2015-10-19 12:01:31.337 nscurl[18324:1989232] CFNetwork SSLHandshake failed (-9801)
2015-10-19 12:01:31.383 nscurl[18324:1989232] CFNetwork SSLHandshake failed (-9801)
2015-10-19 12:01:31.384 nscurl[18324:1989232] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9801)
Result : FAIL
---

---
TLSv1.0 with PFS disabled and insecure HTTP allowed
Result : PASS
---

================================================================================


Result : FAILがある場合、接続先がATSの要件を満たしていないので、そのドメインを例外として設定する必要があります。 このとき、先ほどのコマンドに--verboseオプションを付けて実行すると、例外の設定をPlistにどう書けばいいかを合わせて出力してくれます。

$ nscurl --ats-diagnostics --verbose https://kishikawakatsumi.com/


下記の例だと、NSAllowsArbitraryLoads = trueはATSをオフにすることになるので、当たり前ですが通信に成功します。 順に設定を調べて行って、一番最後の、TLSの最低バージョンを1.0 NSExceptionMinimumTLSVersion = "TLSv1.0" とし、ForwardSecrecyを必須にしない NSExceptionRequiresForwardSecrecy = false とすることで接続できることがわかります。 設定のPlistをそのままダンプした形式で出力されているので、このままアプリケーションの設定に転記すればOKです。

================================================================================

Allowing Arbitrary Loads

---
Allow All Loads
ATS Dictionary:
{
    NSAllowsArbitraryLoads = true;
}
Result : PASS
---

================================================================================

Configuring TLS exceptions for kishikawakatsumi.com

---
TLSv1.2
ATS Dictionary:
{
    NSExceptionDomains =     {
        "kishikawakatsumi.com" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
        };
    };
}
2015-10-19 12:03:48.189 nscurl[18336:1992700] CFNetwork SSLHandshake failed (-9824)
2015-10-19 12:03:48.190 nscurl[18336:1992700] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
Result : FAIL

(略)

---
TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "kishikawakatsumi.com" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

StoryBoardまたはXIBについて、ファイルごとにDeployment Targetを設定する

昨日はじめて知ったのですが、StoryBoardやXIBファイルはプロジェクトやターゲットのDeployment Targetとは別に、各ファイルごとに個別にDeployment Targetを設定することができます。

例えば、iOS 8以上にしか存在しないUIVisualEffectViewや、iOS 9以降でしか使えないUIStackViewをStoryBoardで配置して、プロジェクトのDeployment Targetを7.0(や8.0)にすると、下記のエラーでビルドに失敗します。

Main.storyboard: error: Class Unavailable: UIVisualEffectView prior to iOS 8.0

これを避けるためにはStoryBoardを使うことをあきらめ、コードでOSバージョンを分岐して、コードでUIコンポーネントを配置する必要があると思っていました。

しかし、StoryBoardにはファイルごとにDeployment Targetを設定できるので、新しいUIコンポーネントを配置しているStoryBoardについては、プロジェクトのDeployment Targetよりも高い値を指定します。

StoryBoardを選択して、File Inspectorの「Interface Builder Document」セクションの「Build For」がそれになります。

↑ たとえば、上記の例ではプロジェクトのDeployment Targetは7.0なのでiOS 8以上でしか使えないUIVisualEffectViewをStoryBoardで配置するとビルドエラーになりますが、そのStoryBoardのDeployment Targetを8.0以上にすることでビルドできるようになります。

もちろん、古いOSでStoryBoardを使ってしまうと実行時にクラッシュしてしまうので、OSのバージョンを判断して、ロードするStoryBoardを変更するようにします。

let storyboard: UIStoryboard

if #available(iOS 9, *, *) {
    storyboard = UIStoryboard(name: "Detail-iOS9", bundle: nil)
} else {
    storyboard = UIStoryboard(name: "Detail-iOS8", bundle: nil)
}

if let controller = storyboard.instantiateInitialViewController() {
    self.navigationController?.pushViewController(controller, animated: true)
}

↑ このようにすると、古いOS用のUIと新しいOS用のUIがStoryBoard単位で分けられますし、レイアウトはStoryBoardが使えるので、スッキリした構成になります。

iOS 9ではUIStackViewを使い、古いOSではUICollectionViewを使うように切り替えるなども簡単にできます。

【参考】

stackoverflow.com

WebとiOSアプリでパスワードを共有する

iOS 8からWebサービスとアプリ間でiCloudキーチェーンを通じてパスワードなどアカウント情報を共有できるようになりました。 (ただし、現状ではiCloudキーチェーンを使えるのはSafariのみのため、MacのSafariとiOSアプリの間に限る)

昨今ではそれぞれ別のサービスで同じパスワードを再利用せず、サービスごとに固有のできればランダムなパスワードを登録して、パスワードマネージャで管理することが推奨されています。 Webサービスを使うだけならブラウザのパスワード管理などを利用すればいいのですが、そのサービスのiOSアプリを利用しようとするとパスワードを調べるのが大変でした。

iOS 8では(Safari限定ではありますが)Webサービスで入力してキーチェーンに保存したアカウント情報を、iOSアプリでも利用することができます。

パスワードなど機密性の高い情報を共有するため、どのアプリでも自由に共有できるというわけではありません。 iCloudキーチェーンをアプリから読み出すには、そのアプリがWebサービスと連携しているということを証明する必要があります。 その証明にはWebサービス側と、アプリ側の双方に手続きが必要です。

試験用に今回用意した環境の準備について

SSL

SSLはStartSSLが提供している無料の証明書を利用しました。StartSSLはアップルのTrusted Listに含まれているのでHandoffあるいはShared Credentialに使用できます。

メールサーバ

SSLの証明書を取得するのにドメインと同じホスト名のメールアドレスが必要なので、Zohoのメールサービスをを利用しました。無料です。

HTTPSサーバ

サーバはDigital OceanでUbuntuの一番安いプランを利用しました。時間単位で課金の格安VPSです。

一か月放置したとしても500円程度で済みます。1日、2日利用する程度ならほぼ無料です。

今回のケースでは単にパスワードの入力フィールドがあるSSLの静的なページがひとつあれば十分ですので、nginxにSSLの設定をして、1枚のHTMLファイルを配置するだけにしました。

DNS

メールアドレスのMXレコードを設定するためにDNSが必要なので、DOZENS を利用しました。設定できるレコード数に制限がありますが、1人で使うぶんには普通は無料の範囲で利用できます。

Webサービス側の準備について

Webサービス側に必要な手続きから説明します。

Webサービス側にはiOSアプリのIDを列挙したファイルをWebサイトのルートに決まった名前で配置する必要があります。 アプリのIDはTeam ID + Bundle Identifierです。Team IDはたいてい1ベンダーにつき1つですが、昔は複数持つこともできたので、古くからのデベロッパーの方は注意が必要です。

アプリケーションが複数ある場合は、必要なぶんだけ記入することができます。

{
    "webcredentials": {
        "apps": [ "D3KQX62K1A.com.kishikawakatsumi.Example-iOS",
                  "D3KQX62K1A.com.kishikawakatsumi.Demo-iOS" ]
    }
}

このファイルを関連付けるWebサイトのルートからアクセスできるように配置します。 これでWebサービス側の作業は完了です。

iOSアプリ側の準備について

一方iOSアプリ側では連携するドメインを列挙したEntitlementファイルを用意します。 Xcodeのターゲット>Capabilitiesのところに"Associated Domains"が追加されているのでONにします。 Entitlementファイルが無ければ自動的に追加されて、それを参照するようにビルド設定が更新されます。(すでに存在する場合は追記されます)

「+」ボタンを押して連携するドメインを「サービス:ドメイン」という形式で記述します。 サービス名は"webcredentials"と決まっています。 ドメインが複数ある場合は必要なぶんだけ記入します。サブドメインが異なる場合もキーチェーンには別ドメインとして保存されているので、必要ならすべて記入します。

f:id:KishikawaKatsumi:20150120020454p:plain

iOSアプリ側の作業は以上です。基本的にXcodeがうまくやってくれるので難しいことはありません。

iOSアプリとWebサイトの関連付けについて

Webサイト、アプリの両方の条件が整っていると、アプリケーションのインストール時、アップデート時にアプリとWebサイトの関連付けが行われます。

iOSからのリクエストに対して、Webサーバが200のステータスコードを返し、かつIDが一致し、署名が正しければ、そのアプリケーションはWebサイトと関連があるとみなされます。

ステータスコードが300〜499を返したとき、あるいはIDや署名が正しくない場合は、そのWebサイトと無関係とみなされます。

ステータスコードが500のときは一時的にサーバに障害が発生していると判断され、3時間後に再度リクエストが発行されます。

無事にアプリとWebサイトが関連していると認められれば、アプリからiCloudで同期されたSafariのキーチェーン(Shared Web Credential)を読み出すことができます。

注意事項

この関連付けの処理は、アプリケーションのインストール時、アップデート時にのみ行われます。一度関連付けが済めば、後でWebサイト側の設定を増やしたり、消したりしても、それがアプリのほうで更新されるのは再度アップデートしたときや、一度消してインストールし直したときになります。

また、こちらのクックパッド開発者ブログの記事に書かれているように、Handoffの場合と同様に、自動アップデートがあった場合はそのタイミングで一斉にアクセスがあることが予想されます。もしリクエストを処理しきれないとき、ステータスコード500を返せれば自動的に3時間後にリトライされますが、うまくレスポンスを返せなかった場合は関連付けに失敗したことになるので注意が必要です。

MacからiPhoneに遷移させよう - クックパッド開発者ブログ

iOSアプリからShared Web Credentialの利用するには

認証情報を取得する

Shared Web Credentialから認証情報を取得するにはiOS 8から新たに追加されたAPIを利用します。

func SecRequestSharedWebCredential(
                                   fqdn: CFString!,
                                   account: CFString!,
                                   completionHandler: ((CFArray!, CFError!) -> Void)!
                                  )

fqdn, accountパラメータはそれぞれオプションで指定すると、検索条件として作用します。 (例えばfqdn"kishikawakatsumi.com"と指定すると、"kishikawakatsumi.com"ドメインとして登録された情報のみ対象になります。accountパラメータも同様です。どちらか片方という使い方もできます。)

このメソッドを呼び出してShared Web Credentialから情報が取得できる場合は、OSにより自動的にユーザーに選択肢が表示されます。

f:id:KishikawaKatsumi:20150120023348p:plain

ここでユーザーはShared Web Credentialから情報を取り出すこと自体を拒否することができます。 また、複数のアカウントが存在する場合は、利用するアカウントを選択することができます。

ユーザーが拒否した場合は、コールバックには何も渡ってきません。 Webサイトとアプリが関連していれば好きなように認証情報を使えるわけではなく、あくまでもユーザーが許可した場合のみ利用できる仕組みになっています。

ユーザーがいずれかのアカウントを選択した場合、コールバックにはドメイン、アカウント、パスワードの情報が入ったDictionaryの配列が渡ってきます。 現状、複数のアカウントが存在しても、渡ってくるのはユーザーが選択したひとつだけなので、配列の形式になってはいますが、複数組の認証情報がくることは無いと思います。

アプリとWebサイトの関連づけに問題がある場合は、エラーオブジェクトの内容から問題を調べることができます。

もし、1件も認証情報が見つからなかった場合は、選択肢のUIは表示されず、即座にコールバックが呼ばれます。(そのときのエラーオブジェクトはItem not foundとなります)

無事に認証情報が取得できれば、その情報を使ってログイン処理などを行います。 成功したら認証情報をアプリのキーチェーンに保存し、次からはアプリのキーチェーンの情報を使用します。

認証情報を更新する

もし、アプリからパスワードなど認証情報の変更ができる場合、下記のメソッドを使ってアプリ側から変更をShared Web Credentialに同期することができます。

また、アプリからサインアップが可能な場合も、同じ方法でShared Web Credentialに追加しておくとSafariですぐにログインできて便利です。

func SecAddSharedWebCredential(
                               fqdn: CFString!,
                               account: CFString!,
                               password: CFString!,
                               completionHandler: ((CFError!) -> Void)!
                              )

変更の場合は、OSによって自動的にユーザーに変更の許可を求められます。 ユーザーが許可した場合はShared Web Credentialが更新されiCloudによって同期されます。 追加の場合には何も表示されません。

認証情報を削除する

もし、Shared Web Credentialの情報を削除したい場合は、passwordパラメータにnilを渡すことで削除になります。

削除の際は、変更と同様にユーザーの許可が求められます。

ただし、単にログアウトしたときなどに、削除する必要はありません。削除を行うのはユーザーがサービスを退会したときなどにすべきです。

Shared Web Credentialから情報を取得するコードは下記になります。

SecRequestSharedWebCredential(nil, nil) { (credentials, error) -> () in
    if let error = error {
        return
    }
    
    if CFArrayGetCount(credentials) > 0 {
        let credential: CFDictionary = unsafeBitCast(CFArrayGetValueAtIndex(credentials, 0), CFDictionary.self)
        let domain = CFDictionaryGetValue(credential, unsafeAddressOf(kSecAttrServer))
        let account = CFDictionaryGetValue(credential, unsafeAddressOf(kSecAttrAccount))
        let password = CFDictionaryGetValue(credential, unsafeAddressOf(kSecSharedPassword.takeUnretainedValue()))
        
        println("domain: \(unsafeBitCast(domain, CFString.self)), account: \(unsafeBitCast(account, CFString.self)), password: \(unsafeBitCast(password, CFString.self))")
    }
}

実際には、アプリのキーチェーンから認証情報を探す、見つかったらログイン。 見つからなかった場合はShared Web Credentialに問い合わせる、見つかったらログイン。 見つからなかった場合は、認証画面を表示して入力を受けつける。 入力された認証情報をキーチェーンとShared Web Credentialの両方に保存。

便利なラッパーライブラリについて

キーチェーンのAPIはなかなかに面倒なので、KeychainAccessなどラッパーライブラリを使うと簡単です。

KeychainAccessを使用すると、上記のフローは下のように書けます。

let keychain = Keychain(server: "https://www.kishikawakatsumi.com", protocolType: .HTTPS)

let username = "kishikawakatsumi@mac.com"

if let password = keychain.get(username) {
    // If found password in the Keychain,
    // then log into the server
} else {
    // If not found password in the Keychain,
    // try to read from Shared Web Credentials
    keychain.getSharedPassword(username) { (password, error) -> () in
        if let password = password {
            // If found password in the Shared Web Credentials,
            // then log into the server
            // and save the password to the Keychain

            keychain[username] = password
        } else {
            // If not found password either in the Keychain also Shared Web Credentials,
            // prompt for username and password

            // Log into server

            // If the login is successful,
            // save the credentials to both the Keychain and the Shared Web Credentials.

            keychain[username] = password
            keychain.setSharedPassword(password, account: username)
        }
    }
}

まとめ

以上、Webサービスとアプリ間で認証情報を共有できるShared Web Credentialを紹介しました。 現状ではSafari限定ということもあり、それなりの手間をかけるのに見合わないと思うかもしれません。

しかし逆説的になりますが、対応しているアプリがないがゆえに、iCloudキーチェーンを使うメリットが少なく、さらに普及が遅れるということもあると思います。

私のようにパスワードを完全にSafariの自動生成とキーチェーンで管理してるような者にとっては、ひとつでも対応アプリが増えると非常に便利に感じます。

実際、機種変更時などアプリの再ログインが必要なタイミングでパスワードを探すのが面倒で使わなくなったアプリもあります。

Shared Web Credentialを活用すると、アプリかWebのどちらかひとつでログインすればその後はパスワードの入力が不要になるというスマートな体験を提供することができます。 ぜひ、積極的に使ってみてください。

参考情報

Shared Web Credentials Reference

WWDC 2014 Session 506 - Your App, Your Website, and Safari - ASCIIwwdc

UIKeyCommandを使ってバーコードリーダーの入力を受け取る

TL;DR

  • ●バーコードリーダーは外部キーボードとして扱える
  • UITextFieldなどの入力コンポーネントを使って入力を受け取れる
  • UITextFieldなどを使いたくない場合がある
  • UIKeyCommandを使うと入力コンポーネントを使わずに入力を受け取れる

ユビレジでは商品の入力に市販のバーコードリーダーを利用することができます。

一般的なBluetoothのバーコードリーダーはHID(Human Interface Device)とSPP(Serial Port Profile)の両方のプロファイルに対応しています。

HIDとして接続する場合は外部キーボードと同じ扱いになります。 外部キーボードが繋がっているのと同じなので、UITextFieldUITextViewを使って特別なSDKを必要とせずに入力を受け取ることができます。

ただし、このやり方は簡単なのですが、入力を受け取るにはUITextFieldなどの入力コンポーネントがアクティブになっている必要があるので少し不便です。

ユビレジでは商品点数の多い小売の店舗などではバーコードリーダーメインの入力として使っていますが、飲食店などではバーコードリーダーはまったく使いません。 このように利用者によって、使うところはメインの入力で使うし、使わないところはまったく使わないという状況なので、どちらにも使いやすくなっている必要があります。

そのため、バーコードリーダーの入力を受けるためにはテキスト入力エリアをタップする必要がある、というのは常時バーコードリーダーを利用するユーザーは面倒に感じるでしょう。 かといって常にテキスト入力エリアが自動的にアクティブになるようにすると、バーコードリーダーが接続されてない店舗では常にソフトウェアキーボードが表示されてしまいます。

そこで、ユビレジではバーコードリーダーの入力を受け取るのにUIKeyCommandというiOS 7から新しく追加されたAPIを利用しています。

UIKeyCommandは本来は外部キーボードが接続された状態でキーボードショートカットによる操作をサポートするためのAPIです。 任意のキー入力の組み合わせを受け取ることができるので、入力される可能性のあるすべての文字に反応するようにしておくことで、任意のキー入力を受け取る仕組みとして利用することができます。

UIKeyCommandについては前に別の記事でも書きましたのでそちらもご覧ください。

iOSアプリケーションでキーボードショートカットに対応する - 24/7 twenty-four seven

例えば一般的なバーコードリーダーの入力を受け取るには下記のように実装します。

@interface ViewController ()

@property (nonatomic) NSArray *commands;
@property (nonatomic) NSString *barcode;

@end

@implementation ViewController

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (NSArray *)keyCommands {
    if (!self.commands) {
        NSMutableArray *commands = [[NSMutableArray alloc] init];
        NSArray *characterSets = @[[NSCharacterSet characterSetWithRange:NSMakeRange(0x20, 0x7f - 0x20)],
                                   [NSCharacterSet newlineCharacterSet]];
        for (unichar i = 0x00; i < 0x7f; i++) {
            for (NSCharacterSet *characterSet in characterSets) {
                if ([characterSet characterIsMember:i]) {
                    NSString *string = [[NSString alloc] initWithCharacters:&i length:1];
                    UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:string modifierFlags:kNilOptions action:@selector(handleKeyCommand:)];
                    [commands addObject:command];
                    break;
                }
            }
        }
        
        self.commands = commands.copy;
    }
    
    return self.commands;
}

- (void)handleKeyCommand:(UIKeyCommand *)command {
    NSString *key = command.input;
    NSCharacterSet *newlineCharacterSet = [NSCharacterSet newlineCharacterSet];
    if ([key rangeOfCharacterFromSet:newlineCharacterSet].location != NSNotFound) {
        NSLog(@"Barcode: %@", self.barcode);

        self.barcode = nil;
    } else {
        if (self.barcode) {
            self.barcode = [self.barcode stringByAppendingString:key];
        } else {
            self.barcode = key;
        }
    }
}

@end

まず、UIKeyCommandでキー入力を受け取るにはcanBecomeFirstResponderメソッドでYESを返す必要があります。 そしてkeyCommandsプロパティをオーバーライドして、受け取りたいキーコンビネーションのUIKeyCommandインスタンスの配列を返すようにします。

上記の例ではすべてのASCII文字と、改行文字に反応します。

これで接続されたバーコードリーダーでバーコードを読み取るたびにhandleKeyCommand:メソッドが呼ばれます。 handleKeyCommand:メソッドは1文字ずつ連続で呼ばれるので、終わりが来るまで読み取った文字列を結合します。 たいていのバーコードリーダーでは終わりに改行文字を送ってくるため、それをバーコードの終わりと判断して、そこまでを一つのバーコードと扱います。

このコードはバーコードリーダーが接続されてない場合にはまったく影響ありません。

このように、UIKeyCommandを利用すると、バーコードリーダーを利用している場合は特別な操作を必要とせずにバーコードリーダーからの入力を受け取ることができ、利用しない場合は今までどおり使えるという先述の問題をキレイに解決することができます。

マイナーなAPIですが、意外と便利で、おそらくバーコードリーダー以外にも応用できる場面はあるのではないかと思います。

Swiftで使いやすいAPIを書くために気をつけていること

先日iOSオールスターズ勉強会でSwiftでライブラリを書く際に良いと思ってることについて話しました。そこで好評だったり同意してもらえた何点かについてあらためてまとめます。

オーバーロードを積極的に活用しよう

Swiftではメソッドのオーバーロード(引数の数、型、および戻り値の型が異なる同じ名前のメソッドが定義できる)が言語仕様としてサポートされています。これは上手に使うと利用する側にとってとても書きやすくなるので積極的に使っていきましょう。

例えば下記のような例では、Objective-Cではデータ型によって複数のメソッドを使い分ける必要がありました。 (引数の型がNSStringNSDataかでメソッドの名前が異なる)

- (BOOL)setString:(NSString *)string forKey:(NSString *)key;
- (BOOL)setData:(NSData *)data forKey:(NSString *)key;

Swiftでは次のように書けるので、利用者は型の違いを意識することなく、setメソッドのみを覚えていればよいことになります。

func set(value: String, key: String) -> NSError?
func set(value: NSData, key: String) -> NSError?

下記は引数の数が違う例です。

convenience init()
convenience init(service: String)
convenience init(accessGroup: String)
convenience init(service: String, accessGroup: String)

必須でない引数を省略したメソッドを用意することで、利用するときは次のように必要なときだけ引数を渡せばよいことになります。

let keychain = Keychain()
let keychain = Keychain(service: "com.example.github-token")
let keychain = Keychain(accessGroup: "12ABCD3E4F.shared")
let keychain = Keychain(service: "com.example.github-token", accessGroup: "12ABCD3E4F.shared")

デフォルト引数とオーバーロードならオーバーロードの方がわかりやすい

Swiftではデフォルト引数(引数を指定しなかった場合、あらかじめ指定したデフォルト値が設定される)もサポートされており、オーバーロードと似たような効果を与えることができます。

例えば下記のように3つの引数を取るメソッドのうち、必須でない2つの引数にデフォルト値を与えます。

convenience init(server: String, 
                 protocolType: ProtocolType = .HTTPS, 
                 authenticationType: AuthenticationType = .HTMLForm)

そうすると次のように必須の引数以外は省略して呼び出すことができます。

let keychain = Keychain(server: "https://github.com")

ただ、私はこのような場合はオーバーロードによって同様のことを実現する方が良いと考えています。

理由は2つあって、まず一つは、Xcodeの入力補完を利用する際に、常にすべての引数を取る補完候補が表示されてしまうので、引数がオプションであることがわかりづらい点です。

もう一つは、オプション引数を省略するケースの方が一般的な使い方の場合、補完された引数を消すという行為が必要になるため書くリズムが崩れる、というのがイマイチ使い勝手を悪くすると思うからです。

↑上記のメソッドは呼び出すときにこのように補完されます。引数を省略するには補完された引数をわざわざ消す必要があります。

↑オーバーロードで同じことを実現した場合はこのように補完されます。引数を省略できるメソッドが用意されてることがひと目でわかりますし、どのように書きたい場合でも補完から選ぶことができます。

さらにデフォルト引数として何が与えられているのかという情報も、利用する段階では欠如してしまうので、省略した場合にデフォルト値として何が指定されるのか明示したい、という用途にも今のところ使えないことになります。

convenience init(server: NSURL, 
                 protocolType: KeychainAccess.ProtocolType = default,
                 authenticationType: KeychainAccess.AuthenticationType = default)

↑デフォルト値はこのようにすべてdefaultと表示されるため何が指定されるのか、わからない。

例外: コールバックとしてのクロージャはデフォルト引数で空の実装を与えよう

非同期メソッドのコールバックとしてクロージャを与えるというインターフェースはよくあります。

この場合については、オーバーロードで省略形を用意するよりは、デフォルト引数として何もしないクロージャを与えるのが良いと思います。

func setSharedPassword(password: String,
                       account: String,
                       completion: (error: NSError?) -> () = { e -> () in })

理由としては、非同期メソッドのコールバックは省略することの方が少なそうであること(そうであるなら常に複数のメソッドが補完されるのは面倒)、省略された場合でも呼び出される方でクロージャがnilかどうかをチェックする必要がないのでスッキリ書ける、などです。

keychain.setSharedPassword(password, account: username)
keychain.setSharedPassword(password, account: username) { (error) -> () in
    println(error)
}

↑上記のメソッドはこのようにコールバックを必要に応じて省略できます。省略した場合でもクロージャはデフォルト引数によって必ず渡されるので、呼び出される側の実装は常にクロージャを呼べばよいです。

戻り値のオーバーロードはあまり便利じゃない

Swiftでは戻り値の型だけが異なるメソッドもオーバーロードとして同じ名前で定義できます。

func get(key: String) -> String?
func get(key: String) -> NSData?

しかし上記のようなメソッドを用意した場合、呼び出すときにどちらのメソッドを呼び出しているのか名前だけでは区別できないため、戻り値の型を指定するか、キャストが必要になってしまいます。

↓下のように左辺の変数の型を指定するか、

let data: NSData? = keychain.get("somedata")

↓戻り値のキャストを書く必要がある。

let data = keychain.get("somedata") as NSData?

たいていの場合、気にせず書いていたらコンパイルエラーが出たので直す、ということになってリズムが狂うので良くないと考えています。

私は上記のケースではオーバーロードは使用せずにメソッドの名前を変えることにしました。

func getString(key: String) -> String?
func getData(key: String) -> NSData?

↑スマートではないですが、補完を働かせることもできるのでこちらのほうが良いと考えました。

メソッドチェーンを上手に使おう

Objective-Cの文法ではメソッドチェーンは非常にやりにくいものでした。 (チェーンするなら、まずチェーンする数のカッコを開く必要がある。足りなかったら最初に戻ってカッコを足すことになる。)

Swiftでは自然な形でメソッドチェーンが利用でき、もちろん補完も働くので、有効に使えるケースでは上手に活用しましょう。

keychain
    .accessibility(.AfterFirstUnlock)
    .synchronizable(true)
    .set("01234567-89ab-...", key: "kishikawakatsumi")
keychain
    .accessibility(.WhenUnlocked)
    .set("01234567-89ab-...", key: "kishikawakatsumi")

エラーはEither型として返すことを検討しよう

Objective-Cでは複数の戻り値を返すことはできなかったので、戻り値を返すメソッドでエラーの情報も返す必要がある場合は、NSErrorのダブルポインタを引数に渡す方法が一般的でした。

- (NSString *)stringForKey:(NSString *)key error:(NSError * __autoreleasing *)error;

Swiftでもinoutパラメータを使えば同様のことができますが、errorの変数をわざわざ用意しなくてはいけなかったり、とても使いにくい仕組みなので、別の方法を考えましょう。

単純に戻り値とエラーをタプルで返すというのもアリですが、Swiftには強力なEnumが存在するので、Enumで実装したEither型を返すことをまず検討しましょう。

Either型とはScalaやHaskellなどの関数型プログラミング言語にはたいてい組み込み型として入っている、2つの別個の型の値のどちらかを返す型です。

戻り値としてEither型を返すメソッドのエラー処理は次のように書けます。

let failable = keychain.getStringOrError("kishikawakatsumi")

switch failable {
case .Success:
    println("token: \(failable.value)")
case .Failure:
    println("error: \(failable.error)")
}

基本的にはエラー処理をしてほしいということを型として明示できますし、正常系とエラー系の分岐を自然に書けます。

タプルで戻り値とエラーの両方を返す場合と異なり、正常時のエラー、あるいはエラー時の戻り値といった条件を考慮する必要もなくなります。

非同期メソッドのコールバックではObjective-Cの時と同様にエラーオブジェクトをコールバックの引数で返せばよいかと思います。

keychain.getSharedPassword(username) { (password, error) -> () in
    if error != nil {

    } else {

    }
}

コードを公開する際にはPlaygroundを同梱しよう

ライブラリを公開する際には、READMEなどのほかにPlaygroundをセットで公開すると、すぐに動かして試すことができて非常にわかりやすいのでオススメです。