その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

オランダの小さな島で開催されたカンファレンス「Swift Island」に参加した話 #swiftisland2018

今月の初旬、「Swift Island 2018」というSwiftおよびiOSをテーマとするカンファレンスに参加してきました。テッセル島というオランダにある小さな島が会場で、

f:id:shu223:20180723144557j:plain:w500

  • カンファレンス会場にバンガローがあり、基本的に全員がそこに泊まる
    • 部屋は別々
    • チケット代に宿代・食事代も含まれている
  • よくある「スピーカーの話を聴く」聴講型ではなく、「全員が手を動かす」ワークショップ型

というフォーマットで、今までに参加したどのカンファレンスとも違った良さがありました。

ワークショップ主体の少人数制・合宿型カンファレンス

冒頭にもちらっと書きましたが、カンファレンス名に"Island"とあり、一見「島での開催」が本カンファレンス最大の特徴っぽい感じがしますが、実際に参加してみての感想としては、

  • チケット販売枚数は最大50(ソールドアウトだったので、つまり参加者は50人)
  • すべてのセッションが参加者10人ちょっとの少人数ワークショップ形式で、全員が手を動かす

という点が本カンファレンス最大の個性かつ特長であると感じました。日本のSwift/iOSカンファレンスであるtry! SwiftやiOSDCは800人とかの規模であることを考えると、50人というのは非常にコンパクトです。トータル3日も一緒にいれば、全員の顔ぐらいはなんとなく覚えられそうなレベル。

そして、午前1コマ、午後2コマの1日3コマ × 2日間のワークショップがあり、同じ1コマの時間帯に4つのワークショップが平行開催されていて、1つあたりのワークショップの参加者は12〜13人ほど。全員が手を動かします。

そして斬新だったのは同じワークショップは繰り返し開催される点で、このおかげで人数的にあぶれたり、参加したいワークショップがかぶったりしてても別のコマで参加できるので、基本的には出たいワークショップにはすべて参加することができました。

こういうワークショップベースのカンファレンスは今まで経験したことなかったのですが、やはり聴いてなんとなくわかって気になるのと、自分で実際に一度書いてみるのとでは全然「自分のものにした感」が違う、と感じました。

「(都市から多少隔離された)島での合宿型」という形式も、50人というグループの結束が高まる/非日常感によりいつもと違う分野で手を動かすぞという気持ちを盛り上げてくれる、といった相乗効果があったと思います。

どのレイヤで手を動かすのか

参加者全員が手を動かすといっても、その方法、レベル感は講師によって様々で、例を挙げると、

  • Swiftの新機能

    • iOSのプロジェクトと、テストが用意されている
    • そのままだとテストがコケる
    • そのテストが通るように、iOS側のコードを書いていく
    • その際、Swiftの新機能Mirrorとかを使う
  • 機械学習

    • Turi Createを使ったモデルをトレーニングするコードがJupyter Notebook形式で用意されている
    • それを順次実行して学習を回してみる(ここで各ステップについてちょっと解説が入る)
    • できたCoreMLモデルをiOSに組み込む
  • Siri Shortcut

    • 前で講師が解説 → さぁみんなもやってみよう
    • を手頃なチャンクごとに繰り返す
    • 書籍みたいなテキストが用意されていて、ついていけなければ復習もできる
  • Network, Natural Language

    • フレームワーク群を使うiOSアプリケーションのスケルトンプロジェクトみたいなものが用意されている
    • 作業順とヒントが示されていて、各自で実装していく(例: Network FrameworkでUDPによるソケット通信をやるためのクラスとメソッドの定義だけ用意されていて、その中身を実装していく)

・・・と千差万別でした。どの講師の方のサンプル/教材も「どうやったらこのテーマをうまく学んでもらえるか」を考えて推敲してあるなぁと感心しました。

f:id:shu223:20180704161814j:plain:w500

(Siri Shortcutのワークショップ)

f:id:shu223:20180707041636j:plain:w500

機械学習のワークショップ)

参加者同士のコミュニケーション

全員が基本的にはワークショップ開始前日に到着するのですが、その「前夜」にもう既に参加者同士の輪が出来始めていて、自分はそこに飛び込めず、「やばい、このままずっとぼっちで過ごすのか・・・」と一時不安にもなりましたが、朝・昼・晩と食事がついていて、大きなティピーテント内にいくつか置かれたテーブルで基本的には相席になるので、そこで絶対に話す機会はあるし、

f:id:shu223:20180704124146j:plain:w600

ワークショップでも隣同士で話したり、Pub Quizといったチームで挑むイベントがあったりで、コミュニケーションのきっかけに困ることはありませんでした。

f:id:shu223:20180704220302j:plain:w600

ちなみに他のヨーロッパの国々から来てる人はいたものの、飛行機で10時間以上かかるところからわざわざ来たという人はいなくて、ヨーロッパ外からの参加は僕だけだったかもしれません。そのあたりで珍しがってもらえたというのもコミュニケーションの一助になった気がします。

テッセル島

会場のテッセル島は首都アムステルダムからものすごく遠いというわけではないのですが、電車と船と予約制バスを乗り継いで行くので、なんだかんだで半日かかります。

人より羊が多いらしく、とにかく何もありません。

f:id:shu223:20180703154846j:plain:w600

島での滞在中は自転車をレンタルして、朝とか夜とかにいろいろ周りました。灯台を目指してみたり、対岸の海の方に行ったり。

f:id:shu223:20180706094346j:plain:w600

f:id:shu223:20180723122525j:plain:w600

好みがあるとは思いますが、僕はこういうところをのんびりサイクリングするのは大好きだし、車もほとんど走ってないので会話もじっくりできて、最高でした。

アムステルダム

今回テッセルには4泊したのですが、オランダははじめてだったので、その前後でアムステルダムに合計6泊しました。

マリファナが合法、ビザが取りやすい、ぐらいの前知識しかなくて、僕はそれらに興味がなかったので、実はとくにワクワクすることもなく現地に赴いたのですが、行ってみれば移住したいと思ってしまうほどに気に入りました。

  • アムステルダムは「運河が張り巡らされている港町」
    • もともと海が近くて川が流れてる街が好きな自分にはどストライク。
    • 運河をつなぐ橋の袂には大抵カフェがあってみんな屋外でビール飲んでる。

f:id:shu223:20180630125307j:plain:w600

  • オランダ全体が自転車社会

    • 自転車専用のレーンが整備されててあらゆる場所に自転車を止められる
    • アムステルダムも街が大きくなくて自転車で事足りる
    • (ペーパードライバーな自分は車社会のアメリカでは生きづらかった…)
  • 気候も良い

    • 暑すぎない、それでいてちゃんと暑い夏 1
    • 毎日快晴(※これはたまたまそうだったのかもしれない)

まぁ、しばらく滞在しているといくつか好きではない面も見えてきましたが。。いずれ1ヶ月とかアパート借りて執筆やリモートワーク、みたいな感じでもうちょっと長めに過ごせたらいいなと思います。

お金の話

  • チケット代+自転車レンタル+テッセル延泊+テッセル移動・・・合計€1000ぐらい 2
  • 東京 <-> アムステルダム往復航空券・・・$1340 3
  • アムステルダム6泊(3泊 + 1泊 + 2泊。全部Airbnb・・・合計$950ぐらい
    • 後で気付いたが、1泊とか2泊ならアムステルダムの場合はホテルの方がお得。チェックインも楽だし。
  • モバイルWi-Fiルータ、SIM・・・合計20,000円ぐらい
    • 空港でSIM買えたのでWiFiルータいらなかった。というかアムステルダムでもテッセルでもほとんど繋がらなくて、使えなかった。。
    • SIMは快適でした
  • 食費・・・数えてないが、それなりにかかってるはず

ユーロとドルと円が混じってますがざっくり35万〜40万円といった感じでしょうか。目を背けたくなる金額ですが、しかし、海外の知らない土地に行って、新しい人達と出会って・・・というのは自分にとって重要な人生の醍醐味で、ここをケチって何のために稼いでるのか、というところだし、新しい技術を学ぶきっかけというのは(腰が重い自分にとっては特に)いつでも価値あることなので、100%行ってよかったと思えます。4

まとめ

Swift Island 2018について書きました。要点をまとめると、

  • 少人数・ワークショップ主体のカンファレンス形式がとてもよかった
  • アムステルダムいいところだった
  • お金はかかるけど、たまにはこうして海外カンファレンスに参加するのはいいものだ

といったところです。またチャンスがあれば参加したいと思います。

f:id:shu223:20180707040820j:plain

こちらもどうぞ:これまでの海外カンファレンス参加記事

note.mu

d.hatena.ne.jp

d.hatena.ne.jp


  1. サンフランシスコみたいな「涼しい夏」は夏を過ごした感じがしない。。

  2. 海外のカンファレンスは高いのです。というか日本のが安い

  3. 夏の繁忙期なのでちょっと高い

  4. ちなみにオランダ滞在中にリモートワークも少ししたのですが、旅費の半分ぐらいは相殺できていると思います。

[iOS 12]Network FrameworkでUDPソケット通信

iOS 12で新規追加されたNetwork Frameworkを使って、UDPによるソケット通信を実装してみました。

以前だとCFSocketというCore FoundationのクラスでC言語ベースで実装する必要があったところが、Networkフレームワークの登場によりSwiftでSwiftyに書けるようになります。

受信側の実装

NWListenerというクラスを使って、UDPのListenerを実装します。

// 定数
let networkType = "_networkplayground._udp."
let networkDomain = "local"
private func startListener(name: String) {
    let udpParams = NWParameters.udp
    guard let listener = try! NWListener(parameters: udpParams) else { fatalError() }
    
    listener.service = NWListener.Service(name: name, type: networkType)

    let listnerQueue = DispatchQueue(label: "com.shu223.NetworkPlayground.listener")
    
    // 新しいコネクション受診時の処理
    listener.newConnectionHandler = { [unowned self] (connection: NWConnection) in
        connection.start(queue: listnerQueue)
        self.receive(on: connection)
    }
    
    // Listener開始
    listener.start(queue: listnerQueue)
    print("Start Listening as \(listener.service!.name)")
}

private func receive(on connection: NWConnection) {
    print("receive on connection: \(connection)")
    connection.receive { (data: Data?, contentContext: NWConnection.ContentContext?, aBool: Bool, error: NWError?) in
        
        if let data = data, let message = String(data: data, encoding: .utf8) {
            print("Received Message: \(message)")
        }

        if let error = error {
            print(error)
        } else {
            // エラーがなければこのメソッドを再帰的に呼ぶ
            self.receive(on: connection)
        }
    }   
}

送信側の実装

NWConnectionというクラスを利用して、UDPでデータ送信のための準備を行います。(Connectionとは言ってるものの、UDPなのでTCPとは違ってハンドシェイクを行っての接続の確立、みたいなことはしない)

private var connection: NWConnection!

private func startConnection(to name: String) {
    let udpParams = NWParameters.udp
    // 送信先エンドポイント
    let endpoint = NWEndpoint.service(name: name, type: networkType, domain: networkDomain, interface: nil)
    connection = NWConnection(to: endpoint, using: udpParams)
    
    connection.stateUpdateHandler = { (state: NWConnection.State) in
        guard state != .ready else { return }
        print("connection is ready")

        // do something
        ...
    }
    
    // コネクション開始
    let connectionQueue = DispatchQueue(label: "com.shu223.NetworkPlayground.sender")
    connection.start(queue: connectionQueue)
}

func send(message: String) {
    let data = message.data(using: .utf8)
    
    // 送信完了時の処理
    let completion = NWConnection.SendCompletion.contentProcessed { (error: NWError?) in
        print("送信完了")
    }

    // 送信
    connection.send(content: data, completion: completion)
}

サービスを探索する

接続相手を見つけるため、Listenerがアドバタイズしているであろうサービス(NWListener.Service)を探索します。

初期化
let netServiceBrowser = NetServiceBrowser()
NetServiceBrowserDelegateを実装
  • すべてoptional
  • とりいそぎ動作確認したいだけであれば、netServiceBrowserWillSearch(_:)(探索スタートする前に呼ばれるのでちゃんと動いてることを確認できる)と、netServiceBrowser(_:didFind:moreComing:)(サービス発見したときに呼ばれる)を最低限実装しておけばOK
extension ViewController: NetServiceBrowserDelegate {
    // 探索スタートする前に呼ばれる
    func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
    }

    // サービスを発見したら呼ばれる
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        // 自分以外であれば送信開始
        guard service.name != myName else { return }
        startConnection(to: service.name)
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didFindDomain domainString: String, moreComing: Bool) {
    }
    
    func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemoveDomain domainString: String, moreComing: Bool) {
    }
}
探索開始
netServiceBrowser.delegate = self
netServiceBrowser.searchForServices(ofType: networkType, inDomain: networkDomain)

その他

  • 受信・送信両方の機能を1つのアプリに持たせる

    • つまりどちらもがListenerになり、どちらもが送信側になりうる
  • アプリ起動時に受信を開始

startListener(name: myName)
  • 送信ボタン
@IBAction func sendBtnTapped(_ sender: UIButton) {
    send(message: "hoge")
}

実行

  • 2台のiOSバイスを用意する
  • 同じネットワークに接続する
  • 同アプリを実行

以上で両デバイスで受信準備が完了し、相手を見つけて送信準備も完了(NWConnection.State.ready)したら、送信ボタンを押すたびに相手にメッセージが飛ぶようになります。

Special Thanks

本実装はSwift Islandというカンファレンスでの Roy Marmelstein 氏のワークショップで学んだ実装を自分なりに咀嚼して(復習・覚書として)書き直したものです。

WWDC18の当該セッションもまだ見てなくて、たぶんワークショップに参加しなかったら自分では当面さわらなかったんじゃないかと思うフレームワークなのですが、こうして自分でやってみると(CF時代の不慣れなC言語による難しさ成分が取り除かれているため)意外と扱いやすいことがわかってよかったです。もうちょっとネットワークプロトコルの気持ちがわかりたいなと思ってた頃なので、引き続きさわっていきたい所存です。

アメリカ生活のふりかえり 2018年5月〜6月

サンフランシスコの会社で正社員として働きつつ1、日本でフリーランスとして働く二拠点生活を送っています。

今年からまた日本の住民票を戻したので、アメリカにいる期間の方が短くなります2。もうちょっとしたら家庭の事情によりさらに行く回数が減ることになりそうなので、自分にとって希少になりつつあるアメリカ滞在をなんとなく消化してしまわないよう、何をやったか/何を得たかを中心にふりかえりメモを書いておこうと思います。

やったこと

Fyusion勤務

  • とある画像処理ロジックを組んだ
    • 処理自体はCIFilterにあったので、自前でMetalシェーダを書くことはせず、MetalのパイプラインにCore Imageの処理を組み込むかたちで実装
    • このへんのMetalと他のフレームワークとの連携は本を書いたときにちゃんとドキュメントやWWDCセッションスライドを読んでおいた知見が活用できた
  • その他諸々、この数ヶ月の間に得たMetalの知見を活かした貢献ができた
  • 同僚の家に遊びに行って衝撃を受けた

f:id:shu223:20180626114308j:plain

(これまでタワーマンション的なものに興味がなかったが考えを改めた)

  • 朝から開いてる店が多く、ほぼ毎日早朝にカフェへ行き、1〜2時間仕事以外のコードを書き、それから出社していた
  • 退社後(18時ぐらいにオフィスを出る)はコードを書くウィルパワーは枯渇してるので、ブログを書いたり、メッセージやメールの返信等のタスクをこなしたり
  • サンフランシスコならではの生活とはいえないが、有意義に時間を使えたと思う。

Boston旅行 / MIT見学

  • ボストン/ケンブリッジ
    • アメリカは広いので知り合いがいるときに行っておきたい
    • 期限が切れそうなマイルを消化したかった
  • 連休+有給で5日間ほど
  • MIT見学させてもらった。最高。
    • 公開情報ではないおそれがあるので、いろいろと見せていただいた研究内容については書けない。。

f:id:shu223:20180626115114j:plain

  • シェアサイクルとUber水上タクシーで街を散策しつつ、手頃なカフェを見つけては勉強したりブログ書いたり。完全に有意義だった。

WWDC(の周辺に)参戦

  • 周辺のカフェで自主学習。いろいろブログ記事書いた(後述)
  • AltConfのセッション、おもしろいのがたくさんあった

滞在中に書いた記事

意識してたくさんアウトプットするようにした。

自分のブログ

shu223.hatenablog.com

shu223.hatenablog.com

note

note.mu

note.mu

note.mu

note.mu

note.mu

Qiita

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

滞在中に公開した/メンテしたOSS

github.com

  • サンプルいろいろ追加

github.com

  • WWDC18のセッションで知ったAVSpeechSynthesizerの発話の「読み」を自在にカスタムする方法についてサンプルに反映
  • Xcode 9.3以降でMetal Performance ShadersのMPSCNNConvolutionのイニシャライザで発生していたとあるめんどくさい問題について対処

github.com

github.com

github.com

  • マイナーアップデート

その他

  • 現地で働いている有志の人たちと、「2時間でIoTガジェットをつくる」プロジェクトをやった

    • 2時間でフルスクラッチでつくるわけじゃなく、顔を合わせて作業できる時間がそれだけしかなかったので、それまでにファーム/ハード(筐体&メカ)/アプリケーションのそれぞれのパートを準備してきて、会うときにがっちゃんこしたという話
    • 会ってすぐ(初対面)にアプリとハードの連結動作はうまくいき、細かい動きのチューニング含めて1時間もかからずに理想のかたちになった
    • ひさびさにBLEをさわる機会になってよかった。GATTの定義とか、ハードの方々と連携する勘所を思い出せた
  • このサンフランシスコ滞在中に連絡くれて会いに行った人の会社の仕事を最近リモートでやった

    • 上に書いたAltConfで再会した人の会社とはまた別
    • きっかけはGitHubのコード

  • Samplerシリーズの新作(iOS-12-Samplerではない)をつくり始めた
  • 次回技術書典に向けた執筆もほんのちょっとだけ手を付けた

まとめ

こうして振り返ってみると、1ヶ月の滞在としては濃密に過ごせたなと。二拠点生活移動のオーバーヘッドがあったり住居費が2倍かかったり 3 4 ともったいない点もありますが、気持ちが切り替わって時間を有意義に過ごせるというメリットを感じます。次は日本でのフリーランス生活をがんばります(がんばってます)。


  1. ビザはH-1Bです。

  2. この場合、確定申告は両方でやることになります。

  3. いま気付きましたが、ボストンにいる間は「東京の家賃+サンフランシスコの宿代+ボストンの宿代」、サンノゼにいる間は「東京の家賃+サンフランシスコの宿代+サンノゼの宿代」と、2倍どころか3倍かかってました

  4. 飛行機代は会社持ち。

[iOS 12]Siri Shortcutsの最小実装 - NSUserActivity編

先日のWWDC18で発表された、iOS 12の新機能 "Siri Shortcuts"。「よくやる手順を声で呼び出せる」というのはもちろん嬉しいことですが、それよりも、

「ロックスクリーンから呼び出せる」

というところに、プラットフォームの制約の中で取捨選択するしかないいちアプリ開発者としては圧倒的魅力を感じざるを得ません。

f:id:shu223:20180613205618p:plain

「何がどこまでできるのか、どう実装するのか」を掴むべく、まずはその最小実装を確認してみました。

ちなみに参考資料はWWDC 2018のセッション番号211「Introduction to Siri Shortcuts」とサンプルコード「SoupChef」です。1

実装方法は2通り

Siri Shortcutsの実装方法としては、以下の2種類があります。

  • NSUserActivityを利用する方法
  • Intentsを利用する方法

本記事では、タイトル通り、NSUserActivityを用いたSiri Shortcutsの最小実装だけを紹介します。

Appleのサンプル「SoupChef」は両方の実装が入っており、それはそれでありがたいのですが、初めて挑む人にはどのコードがどっち用なのか、たとえばIntentsを使わない場合はどれを省けるのかといったことがわかりづらいと思うので、そのあたりを本記事で紐解ければと。

3ステップ

NSUserActivityを用いたSiri Shortcutsの実装は、次の3つの手順で行います。

  1. ショートカットを定義する
  2. ショートカットを提供する(donate) 2
  3. ショートカットをハンドルする

「アプリの画面Bを開く」というショートカットをSiri Shortcutsを実現してみます。

1. ショートカットを定義する

Info.plistに次のようにNSUserActivityTypesを定義します。

<key>NSUserActivityTypes</key>
<array>
    <string>com.myapp.name.my-activity-type</string>
</array>

2. ショートカットを提供する

Intentsをインポートして、

import Intents

次のようにNSUserActivityを用意します。

extension NSUserActivity {
    
    public static let myActivityType = "com.myapp.name.my-activity-type"

    public static var myActivity: NSUserActivity {
        let userActivity = NSUserActivity(activityType: myActivityType)
        
        userActivity.isEligibleForSearch = true
        userActivity.isEligibleForPrediction = true
        userActivity.title = "My First Activity"
        userActivity.suggestedInvocationPhrase = "Let's do it"
                
        return userActivity
    }
}

一見複雑に見えますが、Siri Shortcutsに関係するポイントとしては、

  • isEligibleForPredictionプロパティにtrueをセットする
  • suggestedInvocationPhraseプロパティをセットする
    • ここにセットしたフレーズがショートカットの音声コマンドをユーザーに録音してもらう画面でサジェストされる

これぐらいです。

Appleのサンプルには、次のようにCSSearchableItemAttributeSetを作成してcontentAttributeSetにセットする実装も入っていますが、

// 省略可
let attributes = CSSearchableItemAttributeSet(itemContentType: "hoge")
attributes.thumbnailData = UIImage(named: "filename")!.pngData()
attributes.keywords = ["foo", "bar"]
attributes.displayName = "My First Activity"
attributes.contentDescription = "Subtitle"
userActivity.contentAttributeSet = attributes

私が試したところではSiri Shortcutsを行うだけであれば省略可能でした3。検索窓(Spotlight)からのテキストによる検索にも対応させたい場合に必要なのではないでしょうか。

作成したNSUserActivityオブジェクトを、ショートカットを提供するUIViewControllleruserActivityプロパティ(正確にはUIResponderのプロパティ)にセットします。

userActivity = NSUserActivity.myActivity

ここでもAppleのサンプルでは次のようにupdateUserActivityState(_)をオーバーライドしてNSUserActivityaddUserInfoEntries(from:)メソッドでuserInfoのデータを供給する実装が入っていましたが、これもなくても動作しました。

// 省略可
override func updateUserActivityState(_ activity: NSUserActivity) {
    let userInfo: [String: Any] =  [NSUserActivity.ActivityKeys.menuItems: menuItems.map { $0.itemNameKey },
                                         NSUserActivity.ActivityKeys.segueId: "Soup Menu"]

    activity.addUserInfoEntries(from: userInfo)
}

ショートカットでアプリが起動する際に、アプリの状態を復元するために必要なデータ(userInfo)がある場合に実装すべきものと思われます。

なお、ここに出てくるNSUserActivityCSSearchableItemAttributeSetが初見の方は、「iOS 9 の新機能のサンプルコード集」の「Search APIs」サンプルを実行してみつつコードを読んでみると非常にわかりやすいです。NSUserActivity を使うものと、Core Spotlight を使うものの2種類が実装してあり、コードがシンプルなのでどのプロパティが何に対応してるのかがわかりやすいです。

3. ショートカットをハンドルする

ショートカットが呼び出されてアプリが起動する際、UIApplicationDelegateapplication(_:continue:restorationHandler:)が呼び出されるので、そこで渡されてくるアクティビティ(NSUserActivity)をハンドルします。

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    
    if userActivity.activityType == NSUserActivity.myActivityType {
        // Restore state for userActivity and userInfo
        guard let window = window,
            let rootViewController = window.rootViewController as? UINavigationController,
            let vc = rootViewController.viewControllers.first as? ViewController else {
                os_log("Failed to access ViewController.")
                return false
        }
        vc.performSegue(withIdentifier: "B", sender: nil)
        return true
    }
    
    return false
}

ここではactivityTypeでどのアクティビティかを判定し、あとは愚直にperformSegueで画面Bに遷移させているだけです。

完成品の挙動

NDA期間中のため完成品の挙動をキャプチャしてアップすることができないのですが、

  • 設定から当該Siri Shortcutを登録
  • ロックスクリーンから登録したフレーズで呼び出す

これでアプリが起動し、画面Bまで自動的に遷移しました。

アプリはバックグラウンドで生きている必要があるのか?

気になったのが、UIViewControlleruserActivityプロパティに当該NSUserActivityオブジェクトをセットした点です。これってこのView Controllerがショートカットのdonatorであり、アプリが生きてないと呼び出せないのか?と。

というわけでアプリをkillしてもNSUserActivityベースのSiri Shortcutは動作するか試してみました。

結果 → 動作しました。

良かったです。

次回

  • Intentsを用いる場合の最小実装
  • 両者をどう使い分けるか
  • Siri Shortcutsで呼び出せない機能はたとえばどんなものがある?

といったあたりについて書きます。(書けたらいいなと思っています)


  1. 本記事はNDAに配慮し、Xcode 10やiOS 12のスクショは使わず、公開情報のみで構成しています。

  2. 「機能を提供する」ということをiOSで表現する場合にはよく"provide"という動詞が使われることが多い気がします。ここでAppleがあえて「寄付する」「寄贈する」といった意味の"donate"を利用しているのはどういう気持ちがこもっているのでしょう?

  3. このへん、キャッシュが残るような挙動があり、動作検証結果に100%確信がありません。

ドラッグ&ドロップで機械学習のモデルがつくれる「Create ML」の使い方

iOS 12の気になる新機能のAPIを見ていくシリーズ。昨日はARKit 2の永続化・共有機能や3D物体検出機能について書きました。

本記事ではCreate MLについて。1

Create ML

Create MLは、Core MLのモデルを作成するためのmacOSの新フレームワークです。

f:id:shu223:20180606070639p:plain

昨日のState of the Unionにてデモがありましたが、なんと、学習用データが入ったフォルダをドラッグ&ドロップするだけで作成できます。

ちなみにmacOS 10.14 Mojaveです。

MLImageClassifierBuilder

まだMojaveにアップデートしていないので試せていない2のですが、丁寧なチュートリアル記事が出ていて、作業手順を図付きで確認できます。

PlaygroundsでMLImageClassifierBuildeを初期化してshowInLiveViewを呼ぶコードを書き、

import CreateMLUI

let builder = MLImageClassifierBuilder()
builder.showInLiveView()

実行するとGUIが表れ、そこにデータフォルダをドラッグ&ドロップすると学習が開始されます。

f:id:shu223:20180606071137p:plain

f:id:shu223:20180606071232p:plain

テスト(モデルの評価)もドラッグ&ドロップ。

f:id:shu223:20180606071320p:plain

いかがでしょうか。コードは正味3行書いただけ機械学習ができてしまいました。

Create MLでつくった.mlmodelフォーマットのモデルファイルを使ったアプリの実装方法はこちらの記事をどうぞ。

Core ML+Visionを用いた物体認識の最小実装 - Qiita

学習用データについて

ドラッグ&ドロップでできます」とはいえ、そのためのデータセットを用意する必要はあります。

どういうデータを用意するかですが、同チュートリアル記事によると、

  • ラベルごとに最低でも10枚の画像
  • ラベルごとに枚数のバランスをとること(チーターは10枚、ゾウは1000枚、みたいなことをしない)

Use at least 10 images per label for the training set, but more is always better. Also, balance the number of images for each label. For example, don’t use 10 images for Cheetah and 1000 images for Elephant.

  • JPEGPNG等、画像フォーマットはなんでもいい(UTIがpublic.imageに適合していれば)
  • サイズは揃ってなくてもOK
  • サイズもなんでもいいが、最低でも299x299ピクセルはあった方が良い

The images can be in any format whose uniform type identifer conforms to public.image. This includes common formats like JPEG and PNG. The images don’t have to be the same size as each other, nor do they have to be any particular size, although it’s best to use images that are at least 299x299 pixels.

あとは実際に推論を行うのと同じ状況で学習データも収集した方がいいとか、いろんな角度、ライティングの状況のデータがあった方がいい、ということが書かれています。

If possible, train with images collected in a way that’s similar to how images will be collected for prediction.

Provide images with variety. For example, use images that show animals from many different angles and in different lighting conditions. A classifier trained on nearly identical images for a given label tends to have poorer performance than one trained on a more diverse image set.

ラベルをフォルダ名にし、その配下にトレーニングデータ、テストデータを配置します。

Next, create a folder called Training Data, and another called Testing Data. In each folder, create subfolders using your labels as names. Then sort the images into the appropriate subfolders for each data set.

f:id:shu223:20180606071547p:plain

オープンなデータセット

ちなみに独自のデータセットを作成するのは非常に大変ですが、公開されているものもたくさんあります。

Mojaveを入れたら、このあたりのデータからいろいろなモデルをつくって試してみたいなと思っています。

こちらもどうぞ

shu223.hatenablog.com


  1. なお、本記事はNDAに配慮し、Xcode 10やiOS 12のスクショは使わず、公開情報のみで構成しています。

  2. High SierraXcode 10を入れて試してみましたが、import CreateMLUIでエラーになりました。

API Diffsから見るiOS 12の新機能 - ARKit 2 #WWDC18

今年のWWDCの基調講演は、エンドユーザ向けの新機能の話に終止した感が強く、デベロッパ的にはあまりピンと来ない2時間だったように思います。が、State of the Unionやドキュメントを見ると、試してみたい新APIが目白押しです。例年通り、気になったものを列挙していきます 1

全部書いてると時間かかりそうなので、まずは本記事では ARKit 2 について。

Multiuser and Persistent AR

自分が見ているARの世界を共有・永続化できるようになったというのは本当に大きいです。Appleのデモは「ゲームで対決できます」みたいな派手なものでしたが、たとえば奥さんの机に花を置いておくとかみたいなささやかなものでもいいですし、ソーシャルコミュニケーションな何かでもいいですし、教育的な何かでも使えそうですし、とにかく「自分しか見れない」という従来の制約がなくなるだけでARを利用したアプリのアイデアはめちゃくちゃ広がると思います。

f:id:shu223:20180605114618p:plain

API的には、ARWorldMapというクラスが追加されていて、これが「世界」の情報で、これを保存したりシェアしたりするようです。

ARMultiuserというサンプルが公開されていて、送受信のあたりのコードはこんな感じです。

(送る側)

sceneView.session.getCurrentWorldMap { worldMap, error in
    guard let map = worldMap
        else { print("Error: \(error!.localizedDescription)"); return }
    guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
        else { fatalError("can't encode map") }
    self.multipeerSession.sendToAllPeers(data)
}

(受け取り側)

if let unarchived = try? NSKeyedUnarchiver.unarchivedObject(of: ARWorldMap.classForKeyedUnarchiver(), from: data),
    let worldMap = unarchived as? ARWorldMap {
    
    // Run the session with the received world map.
    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = .horizontal
    configuration.initialWorldMap = worldMap
    sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    
    // Remember who provided the map for showing UI feedback.
    mapProvider = peer
}

ここで面白いのは、このサンプルでは送受信に Multipeer Connectivity を使っていて、データとしては普通にNSKeyedArchiverアーカイブ/アンアーカイブしたデータを送り合っている点です。

つまり、送受信や保存の方法はARKitに依存しておらず、なんでもOKということです。データをサーバに送ってもいいわけですが、バックエンドを用意せずとも、サクッとUserDefaultsに保存してもいいし、Realmに保存してもいいし、Core Bluetooth(BLE)を利用しても良いわけです。([PR]その際、こういう本が役に立つかもしれません)

iOS×BLE Core Bluetoothプログラミング
堤 修一 松村 礼央
ソシム
売り上げランキング: 92,732

3D Object Detection

「3D物体検出」は従来の(2D)物体検出と何が違うかというと、「正面から見た状態」だけでなく、横からでも、後ろからでも、上からでも、その物体を認識できるということです。この機能により、現実空間の物体(銅像とか)をマーカーとして仮想コンテンツを置く、といったことが可能になります。

ARKit 2には、3D物体を「スキャン」して「リファレンスオブジェクト」を生成する機能と、そのリファレンスオブジェクトと同じ特徴を持つ物体を現実空間から検出する機能とが追加されています。

f:id:shu223:20180605140338p:plain

(サンプルに同梱されている画像。スキャン〜検出の流れが示されている)

このわりと複雑なスキャンフロー、出来合いのやつがあるのかなと思いきや、ソースを見てみるとがっつりアプリ側で実装されています。3Dバウンディングボックスを順番に埋めていくのとかも全部。まぁ、ここはUIUXとして工夫のしがいがあるところなので、この方が良いですよね。

スキャンする際は、コンフィギュレーションとしてARObjectScanningConfigurationを使用します。

let configuration = ARObjectScanningConfiguration()

で、スキャン中にどういうことをするかですが・・・非常に泥臭い実装がされています。ここでは省略しますが、ARSessionDelegateの各デリゲートメソッドから実装を追ってみてください。

リファレンスオブジェクトを表すARReferenceObjectを作成するには、ARSessioncreateReferenceObject(transform:center:extent:completionHandler:)メソッドを呼びます。この引数に、スキャン対象のバウンディングボックスのtransform, center, extentを渡します。

sceneView.session.createReferenceObject(
    transform: boundingBox.simdWorldTransform,
    center: float3(), extent: boundingBox.extent,
    completionHandler: { object, error in
        if let referenceObject = object {
            // Adjust the object's origin with the user-provided transform.
            ...
        } else {
            print("Error: Failed to create reference object. \(error!.localizedDescription)")
            ...
        }
})

ARKitの新規追加API

APIを眺めるだけでもいろいろと察することはできるので、ざーっと載せておきます。

  • ARWorldMap

The space-mapping state and set of anchors from a world-tracking AR session.

  • AREnvironmentProbeAnchor

An object that provides environmental lighting information for a specific area of space in a world-tracking AR session.

  • ARReferenceObject

A 3D object to be recognized in the real-world environment during a world-tracking AR session.

  • ARObjectAnchor

Information about the position and orientation of a real-world 3D object detected in a world-tracking AR session.

  • ARObjectScanningConfiguration

A configuration that uses the back-facing camera to collect high-fidelity spatial data for use in scanning 3D objects for later detection.

  • ARFaceAnchor - 目の動きのトラッキング

    • var leftEyeTransform: simd_float4x4

      A transform matrix indicating the position and orientation of the face's left eye.

    • var rightEyeTransform: simd_float4x4

      A transform matrix indicating the position and orientation of the face's right eye.

    • var lookAtPoint: simd_float3

      A position in face coordinate space estimating the direction of the face's gaze.

  • ARImageTrackingConfiguration

A configuration that uses the back-facing camera to detect and track known images.

こちらもどうぞ

shu223.hatenablog.com


  1. なお、本記事はNDAに配慮し、Xcode 10やiOS 12のスクショは使わず、公開情報のみで構成しています。

フリーランスを再開して2ヶ月のお仕事まとめ

アメリカでのパートタイム会社員と日本でのフリーランスの2足のわらじを履き始めてから約2ヶ月が経ちました。今月からまた1ヶ月ほど会社員に戻るので、この2ヶ月にやったこと・つくったものをまとめておきます。1

f:id:shu223:20180506214619j:plain
Alexaを持ち出して外で遊ぶ

Core NFCを使ったゲームアプリ

これはまだ未公開なのですが、Core NFCを使ったゲームアプリをつくりました。ブルーパドル社でのお仕事。既に何種類かのゲームが遊べるのですが、手前味噌ながらかなり楽しいです。早く公開したいですがもうちょっと先になりそうです。

Core MLを使った写真の自動分類機能

とあるIoTスタートアップにて。モデル自体は社内の機械学習エンジニアの方がつくったものが既にあり、それをCore MLで使えるように変換するところからスタート。精度やパフォーマンスの検証、どういうUXに落とし込むのがいいかの検討を経て、当該企業のiOSアプリのいち機能として実装しました。社内βテストののち、近いうちにリリースされる見込みです。

ARKit、Metalを使った機能

GraffityというAR落書きSNSアプリ(最近出た2.0ではARビデオ通話アプリに)の開発をお手伝いしました。ARKitをプロダクトとしてがっつりやっている会社というのは(ゲームを除いて)たぶんまだ多くはなく、しかもMetalシェーダを書くという今の興味にがっちりはまるお仕事でした。2

技術顧問

最近立ち上がったとある会社に技術顧問として関わらせていただいてます。優秀な方々が開発していて、正直なところ技術を顧問できてるかは疑問ですが、年齢的にボードメンバーと開発メンバーのちょうど中間だったりするので、開発陣と経営陣のコミュニケーションの橋渡し役をしたり、色んな現場を見てきたならではの視点からプロダクト全体の方向性について意見を言ったりといったところではお役に立ててるのかなと。立ち上がったばかりのスタートアップで状況は常に動き続けているので、自分がどこで役立てるのか、慎重に見定めて行動していきたい所存です。

その他

Alexaスキル

すごく簡単なものですが、Alexaスキルをつくり、ブルーパドルメンバーで遊んでみたりしました。これ自体はストアで公開するほどのものではないのですが、「音声インターフェースアプリケーションはこんな感じでつくる」というのが掴めてよかったです。

ウェブのマークアップ

ブルーパドル社ではマークアップも経験しました。htmlとcssの概念ぐらいはもちろん理解してましたが、gulp、pug、scssというような最近の書き方はまったく知らなかったし、レスポンシブ/スマホ対応というのはそういうことなのねというのを知れたのも良かったです。残念ながらJSまでは時間的に手が回らず。

AV Foundationを使うiPadアプリ

GitHubの僕のとあるリポジトリを見て、海外の会社より依頼をいただきました。AV Foundationの最近さわってないAPIが関わるところだったので、自分のローカルコードベースを再整備しておく意味でもこれはやっておきたかったのですが、タイミング的に一番忙しい時期で、「次の休日に1日だけやってみて、間に合いそうならやる」と先方に伝え、1日やってみた感触では保ち時間では到底終わりそうにないためゴメンナサイしました。

Fyusion社のお仕事

アメリカでの雇い主であるFyusionより、ちょくちょくタスクが来てました。フリーの仕事で手一杯で機能開発までは手が回りませんでしたが、僕が実装した部分について質問に答えたり、日本のクライアント関連の対応をしたり。

個人活動

はじめて技術書を個人で出版

iOS/macOSGPUインターフェースであるMetalの入門書を書き、「技術書典4」にて販売しました。

shu223.hatenablog.com

総括

実績がぼやけている?

この2ヶ月、夜も休日も仕事をしていてものすごく忙しかった気がするのですが、こうやって列挙してみると、不思議なもので「もうちょっとできたのでは」という気がしてきます。

大小のプロジェクトが順不同で入り混じっているので、工数順に並べ替えてみます。

Metal入門執筆(9人日) > ARKit+Metalを使った機能開発 > Core MLを使った機能開発 > 技術顧問 > Webマークアップ > Core NFCを使ったアプリ開発 > その他(それぞれ1日かそれ未満)

左端と右端以外はあまり大差なく、のべ5〜8人日ぐらい。こうして個々の工数を意識してみると、要は分散してしまってるのだなと。

これはなかなか悩ましい問題です。いまのところは、AlexaやWebをやってみるといった「寄り道」は、これまでiOSにフォーカスしすぎた自分には必要なもので、逆に「プロフィールに書いたときにわかりやすく目立つ実績」(例: 有名なプロダクトをほぼまるっと実装、みたいなの)は、いま仕事の依頼が増えても手が増えるわけでもないので、こだわりすぎるところではないかなと考えています。

iOSエンジニアとしてのリハビリ

1年半にわたってひとつの会社にフルコミットしてきたのち、ひさびさにフリーランスとして色々な現場に行ってみるとまず気付いたのは、iOSエンジニアとしての総合力が落ちているということでした。Fyusion社では機械学習・3Dプログラミング・GPU制御といった新しいフィールドを開拓できた一方で、そういった技術を利用する機能の開発に関わることにフォーカスしていたためか、UIKitの細かい挙動とか、DBまわりとか、Swiftの最近のデファクト系ライブラリとか、そういう今までは普通にキャッチアップできていたところが疎くなっていて、浦島太郎になったような心持ちでした。

専業iOSエンジニアが書いたさまざまな現場のコードに触れ、ひさびさに日々の仕事でSwiftを書き、フルスクラッチでアプリもつくり・・・という中で、だいぶ勘を取り戻せた(キャッチアップできた)実感のある2ヶ月でした。

興味のある技術分野のお仕事

MetalやARKitといった最近興味を持っている分野のお仕事や、Core MLやCore NFCといった新しいフレームワークを実プロダクトで使う機会にも恵まれ、ただただ楽しかったです。

心残りは得られた技術ノウハウ3をアウトプットできてない点。たとえばCore NFCとか、ARKitとか、実プロダクトで使ってみてこそ得られる細かい知見がいろいろとありますが、今の所ローカルメモの中で埋もれています。

技術書の個人出版という新しい可能性

前身ブログ、QiitaGitHubと、たくさん「技術情報発信」を行ってきましたが、基本的にそれら自体は無収益でした。書籍を書くこともありましたが、フルコミットで2ヶ月とかかかって、印税は「3日受託開発した方が稼げる」ぐらい。

自分に学びがあるし、発信した内容からお仕事に繋がることもあるのでここでご飯を食べられる必要はないのですが、問題は、フリーとしての仕事(しかもおもしろい)がたくさんあるとき、それらを断ってまで(=自分の時間を1日n万円で買ってまで)新しい技術を勉強する選択がなかなかできない、というところにありました。

そんな折、今回「Metal入門」というニッチな技術書を執筆し、出版社を通すことなく個人で販売してみたところ、なんと簡単に収益が商業本の印税を超えてしまいました。

執筆に使った時間をカウントしてみると、のべ9人日ぐらい。この内訳としても、半分ぐらいは本としての体裁を整えるためにRe:Viewと格闘する時間だったり、印刷所を選んだり入稿方法について調べたりする時間だったりしたので、2回目、3回目とやっていくうちにもっと短縮できるはずです。そうすると、いずれ(下地となるブログ記事等が既にある前提で)技術情報発信の時間単価が受託開発の時間単価に並ぶというのも可能性としては考えられ、これは個人的には非常にエポックメイキングな出来事でした。

ブルーパドル社での非常勤メンバーとしての関わり

ブルーパドル社では毎週「ハッカソン」と称し、なにか簡単なものをつくってそれをベースにいろいろ遊んでみつつブレストする、ということをやってます。これがめっちゃ良くて、自分でアイデアを出す脳みそをまた久々に使うようになったし、家で眠ってるガジェットを掘り起こして触ってみる機会にもなってます。(上述したAlexaスキルはそのうちのひとつ)

また僕が普段フリーランスとしてお手伝いしているスタートアップの世界と、ブルーパドル社の主な事業領域である広告の世界、すぐ隣にあるようでまったく別の世界である点もおもしろいです。同じようなプログラミング言語フレームワークを使っていても、優先するポイントや考え方が違う。それに付随してスピード感とかも違ってくるし、それぞれの業界にいる人達の興味やトレンドも違う。

いろんな面でとにかく新鮮で、毎日多くの学びがあります。

今後の展望

冒頭に書きましたが、フリーランス活動は一区切りし、5月はひさびさにサンフランシスコに戻ってFyusion社にフルコミットします。6月頭はWWDCで、中旬ぐらいからまたフリーランスを再開する予定です。


  1. 許可をいただいた会社だけ社名を書いています。自分が関わったプロダクトや機能がまだ出ていない会社については、出たら聞こう、と思ってたのでまだ伏せています。

  2. ちなみにアメリカで所属しているFyusion社と事業領域がかぶるところがあるといえなくもない(AR)ので、そこらへんはあらかじめあちらのボードメンバーに了承をとりました。

  3. もちろんその会社固有の技術情報ではなくAPIの使い方とかハマりどころとか、あくまで一般的な話