24/7 twenty-four seven

iOS/OS X application programing topics.

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をセットで公開すると、すぐに動かして試すことができて非常にわかりやすいのでオススメです。

SwiftでKeychainを簡単に使うライブラリ "KeychainAccess" を書きました

kishikawakatsumi/KeychainAccess · GitHub

そろそろSwiftをちゃんと勉強しようと思って作りました。 Swiftで書かれたKeychainのラッパーの中ではもっとも高機能でかつ簡単に使えるものができたと思います。

機能としては下記を備えています

  • 簡単に使えるインタフェース
  • アプリ間のキーチェーン共有
  • アクセシビリティ(バックグラウンド動作時の制限など)属性のサポート
  • iCloudによるキーチェーンの同期
  • Touch IDによるキーチェーンの保護(iOS 8〜)
  • iOSとOS Xの両方の動作をサポート

インストール

github "kishikawakatsumi/KeychainAccess"

pod 'KeychainAccess'
CocoaPodsを使う場合、CocoaPodsのバージョンはbeta版の0.36が必要です。
Pod Authors Guide to CocoaPods Frameworks - CocoaPods Blog

  • マニュアルインストール

1. Lib/KeychainAccess.xcodeprojをプロジェクトに追加(ドラッグ&ドロップ)します。
2. KeychainAccess.frameworkをターゲットにリンクします。
3. Build PhasesにCopy Files Build Phaseを追加して、上記のKeychainAccess.frameworkをバンドルのFrameworksディレクトリにコピーするようにします。

使い方

詳しくは READMEを見てください。

基本

値の保存、取得、削除

保存

まずはじめにKeychainのインスタンスを作成します。 サービス名を指定しなかった場合は、自動的にアプリケーションのバンドルIDが設定されます。 そして、作成したキーチェーンのset(value: String, key: String) -> NSError?メソッドでキーと値を設定します。 setメソッドはNSDataを引数に取るメソッドも用意してあります。

Objective-Cでは型の違う引数によるオーバーロードはできなかったので、名前を変える必要がありましたが(例えばsetString:setDataなど)Swiftでは型違いの引数によるメソッドのオーバーロードがサポートされているので、型の違いを気にすることなく自然に書けます。

let keychain = Keychain(service: "com.example.github-token")
keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

Subscriptingも利用できます。Subscriptingについては型違いによるオーバーロードはサポートされていない(Xcode 6.0ではできていた)ので、Stringのみ受け付けます。

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"

エラー処理

値の保存に失敗した場合はNSErrorオブジェクトが返るので、必要ならエラー処理をします。

if let error = keychain.set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi") {
    println("error: \(error)")
}
取得

値を取り出すには、getで始まるメソッド群を使用します。 戻り値の型の違いによって以下の5つのメソッドが用意されています。

get(key: String) -> String?
getString(key: String) -> String?
getData(key: String) -> NSData?
getStringOrError(key: String) -> KeychainAccess.FailableOf<String>
getDataOrError(key: String) -> KeychainAccess.FailableOf<NSData>

String型として値を取得する場合はgetもしくはgetStringを使用します。

let token = keychain.get("kishikawakatsumi")
let token = keychain.getString("kishikawakatsumi")

NSData型として値を取得する場合はgetDataを使用します。

let data = keychain.getData("kishikawakatsumi")

Swiftでは戻り値の型の違いによるオーバーロードもサポートされていますが、戻り値だけが異なるメソッドのオーバーロードは、受ける側の変数で型を明示するか、戻り値をキャストする必要があるため(そうしないとどちらの呼び出しか区別できない)、却って書きにくくなると思ったので、メソッド名自体を変えることにしました。 入力補完が使えるぶん、この方が書きやすいと思います。

エラー処理

エラーを区別(キーに対応する値が無いのかエラーが起こって取得に失敗したのか)したい場合getStringOrErrorもしくはgetDataOrErrorメソッドを使用します。

このメソッドの戻り値の型はFailableOf<String>またはFailableOf<NSData>になります。 FailableOf<T>SuccessFailureのいずれかの状態を取るenumです(ScalaやHaskellなど他の言語ではEitherとして知られています)。 FailableOf<T>の状態がSuccessなら処理は成功しています。(キーに対応する値が無い場合も成功になります。その場合、値はnilになります。 ) FailableOf<T>の状態がFailureの場合は、何らかの理由によりキーチェーンのアクセスに失敗しました。 このとき、errorオブジェクトにはエラーの詳細がNSErrorオブジェクトとして格納されます。 値は必ずnilになります。

実際の使い方は下記のようになります。

まず、getStringOrError(またはgetDataOrError)メソッドを呼び出します。

let failable = keychain.getStringOrError("kishikawakatsumi")

failableオブジェクトはSuccessまたはFailureのいずれかの状態を取ります。 Successならばvalueプロパティから実際の値が取得できます。(キーに対応する値が無い場合はnilFailureならばerrorプロパティにNSErrorオブジェクトが格納されています。(valueプロパティは必ずnil

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

エラー処理についてはかなり悩んだのですが、Swiftにおいて同期処理で正常時には別の戻り値を返す必要があるという場合、Eitherを返すというのがたぶん良い方法だと思います。

非同期処理の場合はObjective-Cと同様に、コールバックのクロージャを分ける(onSuccessとonFailureなど)なり、エラーをクロージャの引数で渡すなどするのが良いかと思います。

単純に戻り値とエラーをタプルで返すという手もありますが、これはあまり使い勝手が良くなかったのでやめたほうが良いと思います。 ただし、下記にあるように現在のSwiftのコンパイラでは単にタプルを返すのに比べて多くのメモリが必要になる、という話もありますので注意が必要なケースもあります。

削除

removeメソッドを使うか、Subscriptingでnilを代入することで削除になります。 エラー処理はsetの時と同様です。

keychain["kishikawakatsumi"] = nil
keychain.remove("kishikawakatsumi")

設定を変えて保存する

アクセシビリティを変更したり、iCloudの同期を指定したりなど、項目によって設定を変えることができます。

下記はフォアグラウンド動作時のみ読み出せるようにして保存しています。 (この項目はバックグラウンド動作時は読み出せません。)

keychain
    .accessibility(.WhenUnlocked)
    .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

下記はiCloudによる同期を有効にする例です。

keychain
    .synchronizable(true)
    .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

メソッドチェーンによる設定の変更は、その時だけ一時的に有効になります。 共通の設定として使用する場合は、チェーンせずに変数に保管して使い回すようにします。

let keychain = Keychain(service: "com.example.github-token")
    .synchronizable(true)

keychain["kishikawakatsumi"] = "01234567-89ab-cdef-0123-456789abcdef"
keychain["hirohamada"] = "..."

Touch IDまたはパスコードで値を保護する(iOS 8〜)

iOS 8からTouch IDを使うAPIが追加されました。 同時にKeychainのAPIにもTouch IDのサポートが追加され、値を読み出すときと更新するときにTouch ID(またはパスコード)による認証を必要とすることができるようになりました。

KeychainAccessでは下記のようにaccessibilityの指定に加えてauthenticationPolicyを指定することで、その値はTouch IDによる保護が有効になります。 (authenticationPolicyで指定できるのは今の所UserPresenceだけです)

認証を必要とする可能性のある処理はバックグラウンドスレッドで実行する必要があります。 Touch ID(またはパスコード)による認証が必要な値にアクセスすると、システムが自動的にTouch IDまたはパスコードによる認証画面を表示します。 このとき、メソッドの実行はブロックされているので、認証画面を出そうとするけど、メインスレッドは止まっているという状態になってアプリケーション全体が止まってしまいます。 そのため、下記のようにバックグラウンドスレッドから実行するようにします。 (値の追加は認証を必要としませんが、すでに値が存在してそれが保護された値の場合、追加ではなく更新になるので認証が必要になります。つまり、削除以外はバックグラウンドスレッドから実行するようにしておくのが安全です。)

let keychain = Keychain(service: "com.example.github-token")

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    let error = keychain
        .accessibility(.WhenPasscodeSetThisDeviceOnly, authenticationPolicy: .UserPresence)
        .set("01234567-89ab-cdef-0123-456789abcdef", key: "kishikawakatsumi")

    if error != nil {
        // Error handling if needed...
    }
}

保護された値の取得は普通の値を取得するときと同様です。 ただし、認証を必要とするのでバックグラウンドスレッドから実行する必要があります。

let keychain = Keychain(service: "com.example.github-token")

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    let failable = keychain
        .authenticationPrompt("Authenticate to login to server")
        .getStringOrError("kishikawakatsumi")

    if failable.successed {
        println("value: \(failable.value)")
    } else {
        println("error: \(failable.error?.localizedDescription)")
        // Error handling if needed...
    }
}

削除については認証を必要としないので普通の値のときとまったく同様の書き方になります。

let keychain = Keychain(service: "com.example.github-token")

let error = keychain.remove("kishikawakatsumi")

if error != nil {
    println("error: \(error?.localizedDescription)")
    // Error handling if needed...
}

Swiftについて

Keychain APIはCのインタフェース(しかも使いにくい)しか用意されていなくて、Objective-Cでもラッパーが必要になるので、Swiftで書いたらまったくSwiftっぽくならなくて大変なだけじゃないのと思ってましたが、意外にもObjective-Cで書くよりも簡単に書けました。

例えば値を読み出すコードはObjective-Cだと下記のようになります。

NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
[query setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[query setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[query setObject:service forKey:(__bridge id)kSecAttrService];
[query setObject:key forKey:(__bridge id)kSecAttrGeneric];
[query setObject:key forKey:(__bridge id)kSecAttrAccount];

CFTypeRef data = nil;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &data);
if (status == errSecSuccess) {
    NSData *ret = [NSData dataWithData:(__bridge NSData *)data];
    if (data) {
        CFRelease(data);
    }
    return ret;
}

Swiftで書いた場合は下記のようになります。

var query = [String: AnyObject]()

query[kSecClass] = kSecClassGenericPassword
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecReturnData] = kCFBooleanTrue
query[kSecAttrService] = service
query[kSecAttrAccount] = key

var result: AnyObject?
var status = withUnsafeMutablePointer(&result) { SecItemCopyMatching(query, UnsafeMutablePointer($0)) }

switch status {
case errSecSuccess:
    if let data = result as NSData? {
        return data
    }
default: ()
}

変なのはSecItemCopyMatchingでポインタのポインタを引数に与えてるところだけで、それ以外は非常にスッキリと書けています。 SwiftのDictionaryNSDictionaryが透過的に扱えて、NSDictionaryCFDictionaryRefがtoll-freeブリッジということでこのように簡単に書けるのですが、これは私にとっては意外でした。

Optionalについても、このライブラリではUIKitはまったく使ってないので特に困ることはありませんでした。 サンプルコードで少しUIを書きましたが、きちんと考えればOptionalはCocoaと一緒に使っても、パズルを組み立てるような気持ち良さが感じられると思います。

そんな感じで、基本的にはやはりObjective-Cに比べると言語自体の表現力が格段に高いので、少ない記述でキレイに書くことができます。 必然的に外部に公開するインタフェースも使いやすいものが書きやすいと思います。

ただし、現状ではSwiftにはリフレクションの仕組みがほぼサポートされていないので、メタプログラミングによって簡単に使えるAPIを提供する、ということはできません。

また、コンパイラはまだまだ未完成なようで、このライブラリを書いてる間に2つほどコンパイラがクラッシュするコード[1][2]を見つけましたし、本来なら型推論で書かなくて済むはず、というコードが実際は型やパラメータを明示する必要があったり、などという動作が散見されました。

また、コードの書き方によってはリリースビルド(コンパイラの最適化が働く)のときだけキーチェーンの値が取れないという問題もあったので、SwiftがObjective-C並みに安定するまでにはまだしばらくかかりそうです。

しかし、やはり言語による強力な表現のサポートは魅力的で、実際に少ない記述で美しく書けるので、使えるところでは積極的に使っていって、フィードバックをどんどんすべきかと思います。

Swiftの言語仕様はObjective-Cに比べるとそれなりに巨大ですが、まったく新しく設計された言語ということで一貫性もあるし、作りながら1週間もやればだいたいなんとかなるんじゃないでしょうか。

ただ個人的にはObjective-C 3.0としてGenericsと賢いenumとstructを入れる、、、とかから始めたら良かったんじゃないのと思ってたりします。

Travis CIでビルドごとにiTunes ConnectのValidationを自動的に実行する

コマンドラインからiOSアプリケーションをiTunes Connectにアップロードする - 24/7 twenty-four seven

↑ こちらの記事で書いたように、コマンドラインからiTunes Connectへのアップロードや、バリデーションができるのを利用して、Travis CIを使ってビルドするたびに自動的にバリデーションを実行するようにしました。

これにより、プライベートAPIを利用していたり、必須なサイズのアイコンやLaunchImageが無いなどの理由でバリデーションエラーになってアップロードが失敗するということが未然に防げます。

ARCを使う場合、ヘッダに載っていないメソッドを呼ぶのはコンパイルエラーになるので、知らずにプライベートAPIを使ってしまうようなことは現在はほぼありません。 しかし、iTunes Connectのバリデーションはあまり賢くないので(おそらく単純な文字列のマッチング?)たまたまプライベートAPIと同名のメソッドを全然関係ないクラスに定義してしまった場合などでもエラーになることがあります。

通常なら申請する段階になって初めてわかるので、エラーの原因がサードパーティのライブラリにある場合など、すぐには直せずリリースを延期せざるを得ないこともあります。

バリデーションはAdHocの署名では失敗するようなので、AppStoreの署名でビルドしてバリデーションをするタスクを追加することにしました。 実際のコマンドは下記になります。

script:
  - bundle exec rake $(ACTION)
env:
  matrix:
    - ACTION="profile:install certificate:add distribute:CONFIG certificate:remove"
    - ACTION="profile:install certificate:add version:bump:patch validate certificate:remove"
    - ACTION=test

バリデーションのタスクは上から2番目のこのコマンドです。

profile:install certificate:add version:bump:patch validate certificate:remove

プロビジョニングプロファイルに署名をする必要があるので証明書を追加と、既存のバイナリとバージョン番号が異なっている必要があるのでバージョン番号の末尾を自動的にあげる処理を前処理として入れています。

実際のバリデーションを行うコマンドは下記になります。

task :validate do
  build_xcarchive(configuration: "Release")
  clean_ipa
  sh %[xcodebuild -exportArchive -exportFormat IPA -archivePath "#{ARCHIVE_FILE}" -exportPath "#{IPA_FILE}" -exportProvisioningProfile "Ubiregi App Store" | xcpretty -c; exit ${PIPESTATUS[0]}]
  validate_ipa(IPA_FILE)
end
def validate_ipa(ipa_file)
  sh %['/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' --validate-app --file "#{ipa_file}" --username ENV["DEV_APPLE_ID"] --password ENV["DEV_PASSWORD"]]
end

xcarchiveからipa形式にエクスポートして、そのipaファイルをaltoolに指定しています。

以下はわざとプライベートAPIを使うようにして失敗させた時のTravis CIのログです。

▸ Compiling UBDashboardCheckoutDetailCell.xib
▸ Compiling UBDataTransferViewController.xib
▸ Processing Ubiregi2-Info.plist
▸ Touching Ubiregi2.app
▸ Signing /Users/travis/Library/Developer/Xcode/DerivedData/Ubiregi2-gavhqalfhdytsueqelwxtefkbbio/Build/Intermediates/ArchiveIntermediates/Ubiregi2-Release/InstallationBuildProductsLocation/Applications/Ubiregi2.app
▸ Touching Ubiregi2.app.dSYM
xcodebuild -exportArchive -exportFormat IPA -archivePath "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.xcarchive" -exportPath "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" -exportProvisioningProfile "Ubiregi App Store" | xcpretty -c; exit ${PIPESTATUS[0]}
'/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool' --validate-app --file "/Users/travis/build/ubiregiinc/ubiregi-client/build/Ubiregi2.ipa" --username [DEV_APPLE_ID] --password [DEV_PASSWORD]
2014-11-02 11:16:00.289 altool[4121:d07] *** Error: (
    "Error Domain=ITunesConnectionOperationErrorDomain Code=1350 \"Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application.\" UserInfo=0x7fd788f56f40 {NSLocalizedRecoverySuggestion=Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application., NSLocalizedDescription=Your app contains non-public API usage. Please review the errors, correct them, and resubmit your application., NSLocalizedFailureReason=iTunes Store operation failed.}",
    "Error Domain=ITunesConnectionOperationErrorDomain Code=50 \"The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:\" UserInfo=0x7fd788f39310 {NSLocalizedRecoverySuggestion=The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:, NSLocalizedDescription=The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:, NSLocalizedFailureReason=iTunes Store operation failed.}",
    "Error Domain=ITunesConnectionOperationErrorDomain Code=-19000 \"If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/.\" UserInfo=0x7fd788d88170 {NSLocalizedRecoverySuggestion=If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/., NSLocalizedDescription=If you think this message was sent in error and that you have only used Apple-published APIs in accordance with the guidelines, send the app's nine-digit Apple ID, along with detailed information about why you believe the above APIs were incorrectly flagged, to appreview@apple.com. For further information, visit the Technical Support Information page at http://developer.apple.com/support/technical/., NSLocalizedFailureReason=iTunes Store operation failed.}"
)
rake aborted!
Command failed with status (3): ['/Applications/Xcode.app/Contents/Applicat...]
/Users/travis/build/ubiregiinc/ubiregi-client/Rakefile:202:in `validate_ipa'
/Users/travis/build/ubiregiinc/ubiregi-client/Rakefile:198:in `block in <top (required)>'
Tasks: TOP => validate
(See full trace by running task with --trace)

The command "bundle exec rake $(echo ${ACTION} | sed -e "s/CONFIG/${CONFIG}/g")" exited with 1.

Done. Your build exited with 1.

エラーが3つ返ってきていますが、いずれも非公開のAPIを使用していることについてのエラーです。 エラーのうちのこの部分に直接の原因が書いてあります。

The app references non-public selectors in Payload/Ubiregi2.app/Ubiregi2: _terminateWithStatus:

_terminateWithStatusUIApplicationで定義されているメソッドですが、iTunes Connectはあまり賢くないので、同じ名前のメソッドをうっかり他のクラスに定義してしまったとしても同様のエラーでバリデーションが失敗します。

このように「ついうっかり」プライベートAPIとして検出されるメソッドやプロパティを定義してしまっていたり、サードパーティのライブラリがエラーとして検出されてしまう、などという問題が、申請時になって発覚するのは大変なことなので、事前にわかるのはかなり便利ではないかと思います。

コマンドラインからiOSアプリケーションをiTunes Connectにアップロードする

参考
TL;DR

Xcode(厳密にはApplication Loader)に付属するiTMSTransporterまたはaltoolを使います。 上記のリンク先を見ればだいたいわかります。

altoolのほうが直感的なコマンドで簡単です。

iTMSTransporterはそもそもゲームのアイテムなど大量のIn-App Purchaseのメタデータを一括更新するためのソフトウェアで、POSTするデータはXMLで用意しなければならないなど、単に申請するアプリケーションをアップロードするという目的には少し面倒です。

altoolでアプリケーションをアップロードする

altoolはXcodeに付属しています。 --helpオプションを付けて実行すると表示される使い方を見ればだいたいわかると思います。

$ `/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool` --help
Copyright (c) 2009-2014, Apple Inc.  Version 1.0

Usage: altool -v -f file -u username -p password
       altool --upload-app -f file -u username -p password

 -f, --file                Filename.
 -u, --username            Username. Required to connect for validation and upload.
 -p, --password            Password. Required if username specified.

 -v, --validate-app        Validate an archived app. The username, password, and file path to app archive are required.
     --upload-app          Uploads the given app.  The username, password, and file path to app archive are required.

 -h, --help                Display this output.

 
ipaファイルをiTunes Connectにアップロードするには下記のようにオプションを構成します。

$ altool --upload-app -f build/AppName.ipa -u USERNAME -p PASSWORD

 
--upload-appオプションを--validate-appまたは-vにすると、検証(プライベートAPIの有無やLaunch ImageやアイコンがDeploymentTargetと矛盾しないかどうかなどのチェック)だけしてアップロードは行いません。

--validate-appはCIなどで自動的に実行するようにしておくと良いと思います。

$ altool -v -f build/AppName.ipa -u USERNAME -p PASSWORD

 

iTMSTransporterでアプリケーションをアップロードする

まずApp Store packageを作る必要があります。 App Store packageは.itmspという拡張子を持つディレクトリです。 App Store packageにはそのアプリケーションのメタデータをmetadata.xmlという名前のファイルで格納します。

上述したIn-App Purchaseのメタデータをアップロードするのは項目がたくさんあって大変なのですが、アプリケーションのアップロードに必要なのは下記のようにAppID、ファイル名とサイズ、およびチェックサムがあればいいようです。

<?xml version="1.0" encoding="UTF-8"?>
<package version="software4.7" xmlns="http://apple.com/itunes/importer">
    <software_assets apple_id="123456789">
        <asset type="bundle">
            <data_file>
                <file_name>AppName.ipa</file_name>
                <checksum type="md5">8ef4f4c855a4d16d5710075c2caa6c85</checksum>
                <size>1174126</size>
            </data_file>
        </asset>
    </software_assets>
</package>

 
ipaファイルはApp Store package、つまりmetadata.xmlと同じ階層に格納します。 App Store packageがmybundle.itmspという名前ならば、ファイル構成は次のようになります。

$ tree mybundle.itmsp/
mybundle.itmsp/
├── AppName.ipa
└── metadata.xml

0 directories, 2 files

 
あとは必要なオプションとともに実行するだけです。 パッケージをアップロードはするには、-mオプションを使って動作モードをアップロードモードに指定します。

'/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/bin/iTMSTransporter' -m upload -f mybundle.itmsp -u USERNAME -p PASSWORD -v detailed

 

以前の方法

以前のValidationコマンドを使う方法はXcode 6からはerror: Unknown application extension '.' - expected '.app' or '.ipa'というエラーが出てしまうので使えなくなったようです。

xcrun -sdk iphoneos Validation -online -upload -verbose build/AppName.ipa 

 

iTunes Connectで必要な手続き

以前のiTunes Connectではバイナリのアップロードするには、アプリケーションの状態をReady to Uploadの状態にしておく必要がありましたが、現在のiTunes Connectではバージョン番号とビルド番号(CFBundleShortVersionStringとCFBundleVersion)さえ異なっていればいつでも任意のビルドをアップロードできるようです(審査中でも)。

 
アップルのTestFlightは自動化できないからワークフローに載せにくいと思っていましたが、アップロードが自動化できるのならけっこう使いものになるんじゃないかと考えています。

Travis CIでiOSアプリのリリース作業を自動化する

この記事において利用している.travis.ymlRakefileの全体はGistにて公開しています。


↓ Rakefileの全体はこちら


↓ .travis.ymlはこちら

概要

ユビレジではiOS アプリを申請する際に発生する作業の大部分をCIで自動化しています。

申請の作業としてユビレジでは下記のワークフローを決めています。


1. リリースブランチを作る
2. リリースするバージョンのバイナリをビルドする
3. 2と同等のアプリケーションを社内に配布して最終チェックをする
4. クラッシュレポートのサービスとしてCrittercismを利用しているので、そこにデバッグシンボル(dSYM)をアップロードする
5. 2のバイナリをiTunes Connectにアップロードする


図にすると下記のようになります。
手順としては難しいものではないのですが、定期的に発生する作業としてはかなり面倒な部類のものです。

20141022124901


また、3のチェックで不具合が見つかった場合は修正して再度やり直しになるので、できるだけ簡単にしたいところです。
さらに、リリースビルドが正しい構成でビルドされているかの確認はパッと見ただけではわからないので、各自の環境でビルドしている場合、それがデバッグ構成になってないかどうかなどを気をつけるのがけっこうストレスだったりします。
そして、この作業は手作業でやるぶんには分割しにくいので(TestFlightの登録だけ手伝います、って言われても別に楽にならない)、1人の時間をけっこう取ってしまうというのも問題です。


そこで、ユビレジではこの一連の作業のうち、下記の点線で囲んだ部分、iTunes Connectにアップロードする以外の作業をすべてCIで自動化しています。

20141022130752


そうすると、実際の行われているワークフローは下記のように変わります。


1. リリースするブランチ(普通はmaster)からreleaseで始まる名前のブランチを派生する(例:release/3.0.0)
(Travis CIによって)
a. リリースビルドが作られる
b. デバッグシンボルドがCrittercismにアップロードされる
c. リリースビルドがAdHoc署名されてTestFlightで配信される
d. リリースビルドがGithub Releasesに登録される
2. 該当ののリリースをダウンロードしてXcodeから申請する


ここで実際に手作業で行う必要がある作業は太字で強調した2つだけです。

自動化の仕組み

リリースビルドの作成

ここから実現方法について説明します。
まず、リリースビルドを自動で作る仕組みは.travis.ymlとrakeのスクリプトで実現しています。

(略)
script:
  - '[ ! -z $(echo ${TRAVIS_BRANCH} | grep "^release.*$") ] && CONFIG=release || CONFIG=adhoc'
  - echo ${TRAVIS_BRANCH}
  - echo ${CONFIG}
  - bundle exec rake $(echo ${ACTION} | sed -e "s/CONFIG/${CONFIG}/g")
(略)
  matrix:
    - ACTION="profile:install certificate:add distribute:CONFIG certificate:remove"
    - ACTION=test


.travis.ymlは上記のようになっていて、ブランチ名が"release"で始まるかどうかでCONFIG変数の値が変わります。
CONFIGの値はrakeのタスクにそのまま渡されて、その値によってリリース版かベータ版のどちらの構成でビルドするかが決定されます。


Rakefileの該当部分は下記になります。

def archive(configuration: "Release")
  build_xcarchive(configuration: configuration)

  clean_ipa
  export_ipa(configuration: configuration)

  zip_artifacts

  if configuration == "Release"
    upload_artifacts
  end
end


内容を1つずつ見ていきます。
build_xcarchiveでは下記のコマンドが実行されます。
(見やすさのため絶対パスを相対パスに書き換えています。以下同様)

xcodebuild -sdk "iphoneos"
           -workspace "./Ubiregi2.xcworkspace"
           -scheme "Ubiregi2-Release"
           -configuration "Release"
           CONFIGURATION_BUILD_DIR="./build"
           CONFIGURATION_TEMP_DIR="./build/temp"
           CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (X123456YZ)"
           archive
           -archivePath "./build/Ubiregi2.xcarchive"


これでbuildディレクトリにUbiregi2.xcarchiveが生成されます。
このとき実行されたコマンドはTravis CIのログから後で確認できるので、デバッグ構成でビルドしてないだろうか?などという不安を抱えることはなくなります。

リリースビルドを元にAdHoc版を作成する

clean_ipaは以前のipaファイルがあったら次のコマンドが失敗するので消すメソッドです。手元で実行するときのためのものです。
export_ipaで今作った「Ubiregi2.xcarchive」をipaファイルに変換します。
このipaファイルを作るのは社内確認用にTestFlightにアップロードするためなので、ここで同時にAdHocプロビジョニングプロファイルで再署名します。


export_ipaで実行される実際のコマンドは下記になります。

xcodebuild -exportArchive
           -exportFormat "IPA"
           -archivePath "./build/Ubiregi2.xcarchive"
           -exportPath "./build/Ubiregi2.ipa"
           -exportProvisioningProfile "Ubiregi Ad Hoc"


これで先ほどビルドしたものと全く同じバイナリが、署名だけ変えて作成されます。
「Ubiregi2.xcworkspace」を単にipaに再パッケージしているだけなので、実行バイナリは完全に同一です。
こうすることによって、署名以外はストアで配布されるものと全く同じアプリケーションを社内で確認することができます。

成果物をzipにまとめる

xcarchiveはディレクトリなのでそのままでは不便なので次のzip_artifactsメソッドでzipファイルにまとめられます。
ここでdSYMディレクトリもついでにzipファイルにしてしまいます。

実際のコマンドは下記です。

(cd ./build; zip -ryq Ubiregi2.app.dSYM.zip Ubiregi2.app.dSYM)
mv ./build/Ubiregi2.app.dSYM ./build/Ubiregi2.xcarchive/dSYMs/Ubiregi2.app.dSYM
(cd ./build; zip -ryq Ubiregi2.xcarchive.zip Ubiregi2.xcarchive)
成果物を外部にアップロード

TestFlightとCrittercismへのアップロードについては以前に書いたものと同様なので、リリースビルドをGithubのReleasesにアップロードする部分を紹介します。

upload_artifactsメソッドは下記のコマンドになります。

curl -sSf
     -d "{\"tag_name\":\"v3.0.0-4321_2014-10-22T06-01-18Z\",
         \"target_commitish\":\"master\",
         \"name\":\"v3.0.0-4321_2014-10-22T06-01-18Z\",
         \"body\":\"Build: 3.0.0 (4321)\\nUploaded: 2014/10/22 15:01:18\\n\",
         \"draft\":false,
         \"prerelease\":false}"
         "https://api.github.com/repos/ubiregiinc/client-releases/releases?access_token=GITHUB_ACCESS_TOKEN"

curl -sSf
     -w "%{http_code} %{url_effective}\n"
     -o /dev/null
     -X POST https://uploads.github.com/repos/OWNER/REPO/releases/RELEASE_ID/assets?name=Ubiregi2-3.0.0-4321.xcarchive.zip
     -H "Accept: application/vnd.github.v3+json"
     -H "Authorization: token "
     -H "Content-Type: application/zip"
     --data-binary @"./build/Ubiregi2.xcarchive.zip"


Releasesへのアップロードは2段階で行います。
まず、最初のリクエストで該当のバージョンのReleaseを作成します。

そのリクエストの戻り値から、作成したReleaseのIDが取れるので、そこに向けてzipファイルをアップロードします。


ここまでで、以下の手順が自動的に完了します。

a. リリースビルドが作られる
b. デバッグシンボルドがCrittercismにアップロードされる
c. リリースビルドがAdHoc署名されてTestFlightで配信される
d. リリースビルドがGithub Releasesに登録される


TestFlightで配信されたアプリケーションを社内で検証して、問題があれば、修正して同じブランチにプッシュするだけで自動的にこの手順が再実行されます。

手戻りによる追加の手間はありません。

申請作業について

最終チェックが無事にすんだら申請です。

本当ははここが最も自動化したいところなのですが、iTunes ConnectはAPIなど提供されていないので、仕方なく手作業で行います。

Github Releaseからリリースする版のxcarchiveをダウンロードします。

20141022130752


zipファイルを展開すると、Xcodeで作成したのと同様のxcarchiveパッケージになるので、それをダブルクリックするとXcodeのオーガナイザに表示されます。

20141022130752


20141022130752


あとはSubmitボタンを押して申請すれば完了です。

おまけ(リリースブランチの作成)

ここまで自動化すると、トリガーとなるリリースブランチを作る作業がだんだん面倒に思えてきます。
(ブランチを派生させてInfo.plistのバージョンを書き換えて、など)
ただ、リリースブランチは再度修正が入ることもあるので、ガチガチにしてしまうとかえって面倒になるので、リリースブランチを簡単にプッシュできるrakeタスクを用意するくらいにとどめています。

リリースブランチを作るにはリリースしたいブランチ(普通はmaster)で

$ rake release

というタスクを実行します。
バージョンの更新についての選択肢が表示されるのでどれか1つを選ぶと、Info.plistの更新、リリースブランチの作成、リモートリポジトリにプッシュ、までをやってくれます。
(するとこれをトリガーとして自動的にCIが上述のビルドを開始します)

$ rake release
Select new version [current: 3.0.0 (4146)]:
1. 4.0.0 (4321)
2. 3.1.0 (4321)
3. 3.0.1 (4321)
4. 3.0.0 (4321)
?  

まとめ

リリースを自動化するのは様々なメリットがあります。
繰り返しの手作業が減ることや品質が一定に保てることももちろんですが、誰でもその作業ができるようになるとか、作業を分割できるようになるというのも大きなメリットです。
例えばリリースするコードをFixする作業(リリースブランチの作成)と社内への告知、iTunes Connectへのアップロードはそれぞれ別のひとがやることが可能になります。

自動化しなくても担当を分けることは可能ですが、手作業でやっていると互いに待ちの時間が発生したりバイナリの受け渡しの手間など、分業することで逆に面倒になります。

ということでユビレジで行っている自動化の仕組みを紹介しました。
参考になればいいなと思います。