読者です 読者をやめる 読者になる 読者になる

リクルートテクノロジーズからGunosyに転職した

仕事

f:id:jeffsuke:20150114094735j:plain

退職

2014年12月末をもってリクルートテクノロジーズを退職しました。 糞プログラマだった私に、勉強期間とサポートを与えて頂いた。また、開発者として失敗をするチャンスや、最終的にはスクラムマスター的役割までやらせて頂いた。 1年3ヶ月と短い期間でしたが、とても良い経験を積めたと思っている。

自分にとって印象に残った点は、

  • 大企業の安定感と、リクルート的スピード感のバランス
  • ビジネスに対する嗅覚
  • 人を大切にする社風
  • 手を上げればやれる。
  • まずは、試してみようという文化。

逆に、慣れずに困った点は、

  • 意思を表示し、手を上げないと、エスカレーター式にやりたい事が降ってくる事はまずない
  • 良くも悪くも、やっぱり営業が強い会社
  • 機能会社としての立ち位置(別ポストで詳しく書きました)

より成果を出す人を見ていると、創業者江副さんの

自ら機会を創り出し、機会によって自らを変えよ。

という言葉を実現できているかどうかがキーだったと感じる。

開発者としてのみならず、起業家的な面でも良い影響を受けることが出来たと思う。特に、自分のビジネスやサービスに対して熱い想いを持つ人が多く、よい学びとなった。 人にも恵まれ、本当に良い時間を過ごせたと思っている。

*ちなみに、尖ったギークな人も多く活躍している。ユーザーを多く抱えるサービスを技術面で支えているのは彼等の努力による。例えばモバイルでは、Androidアプリ開発Push基盤などが参考になる事例だ。

その後

2015年1月からはGunosyというニュースアプリを作る会社で働いている。 開発者各個人の能力と、吸収のスピードがとても早く、腰が抜けそう。 どんなチームかというと、デザイナがiOSアプリを書き、iOSアプリ開発者がAPIも書く、そんなチーム。

新しい技術の採用にも積極的で、現在APIはGo言語で書かれている。この規模のプロダクションで、国内でGoを本格的に使っているサービスはおそらくGunosyくらいだろう。

また、オフィスも最近六本木ヒルズに移転し、快適な環境で働けて良い。

スタートアップという環境で揉まれながら、技術面を伸ばしていきたい。 また、Gunosyでは新しい仲間を募集しているので、興味がある方は是非。

800万DL突破!Gunosyと共に成長したいiOSエンジニア急募! - Gunosyの新卒・インターンシップ - Wantedly

■iOSエンジニア急募!■GunosyのiOS開発リーダー候補を大募集中! - Gunosyの求人 - Wantedly

iOSで動画ストリーミング。簡単で十分なやり方。

iOS Engineering

動画ストリーミングを実装する機会があったので、まとめる。

やり方は大きくわけて2つ

  • HTTP Live Streaming
  • Media Player framework

前者を用いた場合の再生方法は

  • AV Foundation framework
  • OpenAL framework

などがあります。

やりたいこと

UITableViewCell上でサーバー上にある動画を再生する。

f:id:jeffsuke:20150107163712g:plain

方針

今回の要件を満たすのには、Media Player frameworkで十分なので、これを使う。MPMoviePlayerControllerというクラスを用いる。これはiOS2.0からあるクラスなので、バージョン対応もバッチリ。

Media Player frameworkを使う上での注意点

  • Simulator上で動かすことが出来ない場合がある。
  • 複数の動画を同時に再生する事はできない。Media Player frameworkで新しい動画を再生し始めると、その直前に再生していた動画のキャッシュは削除される。
  • ビデオの長さが10分以上、5分間のデータ量が5MB以上であれば、HTTP Live Streamingを用いる必要がある。
  • アプリ側で動画をミュートできない

実装

まず、viewDidLoad にてMPMoviePlayerControllerを初期化する。

@interface JSKMainTableViewController ()
@property (nonatomic) MPMoviePlayerController *moviePlayer;
@end

static NSString *const kURLString = @"https://ia802302.us.archive.org/27/items/Pbtestfilemp4videotestmp4/video_test.mp4";

@implementation JSKMainTableViewController
- (void)viewDidLoad {
    [super viewDidLoad];
   
    self.moviePlayer = [[MPMoviePlayerController alloc] init];
   
    // ストリーミングするため
    self.moviePlayer.movieSourceType = MPMovieSourceTypeStreaming;
    self.moviePlayer.contentURL = [NSURL URLWithString:kURLString];
   
    // UITableViewCell上に表示するため、動画操作パーツは非表示
    [self.moviePlayer setControlStyle:MPMovieControlStyleNone];
   
    // 動画をリピートする
    self.moviePlayer.repeatMode = MPMovieRepeatModeOne;
}

このmoviePlayerを、- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath にてCellに渡す。 CellForRowAtIndexPathで渡していないのは、Storyboard+Autolayoutを用い、UIViewの上に貼り付けているため、Viewが生成されるのを待つ必要があるため。

- (void)layoutWithUrl:(NSString *)url
{
    // 貼り付ける予定のUIViewのサイズを取得
    player.view.frame = _videoSuper.frame;
    [self.videoSuper addSubview: player.view];

    // 再生
    [player prepareToPlay];
    [player play];
}

以上で再生ができる。

Cellが表示している時のみ動画再生

今回はCellが出現するタイミングで再生を実施したかったので、UITableViewCell内の[player play];を削除し、UITableViewに以下を実装した。

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([cell.class isSubclassOfClass:JSKVideoTableViewCell.class]) {
        // 動画停止
        [self.moviePlayer pause];
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([cell.class isSubclassOfClass:JSKVideoTableViewCell.class]) {
             // 動画の貼り付け
        [(JSKVideoTableViewCell *)cell layoutWithPlayer:self.moviePlayer];

              // 動画再生
        [self.moviePlayer play];
    }
}

これにより、画面上にCellが表示されている時のみ動画が再生される。

リクルートテクノロジーズに愛をこめて

仕事

このエントリーは、株式会社リクルートテクノロジーズ Advent Calendar 2014の25日目です。

私の所属するリクルートテクノロジーズについて、真面目に書いてみる。 前提として、リクルートテクノロジーズに入社し 1年 3か月しかたっておらず見聞が狭い。また、この会社で多くのチャンスをもらい成長でき感謝している。そんな前提を踏まえて以下を読んで欲しい。

その立ち位置

リクルートグループは現在、IT企業としての変貌を遂げつつあり、その一角を担うのがリクルートテクノロジーズであり、ITによる競合優位性構築の中心を担っている。 主な事業内容は「リクルートグループのビジネスにおけるIT・ネットマーケティングテクノロジーの開発・提供」となっており、リクルートグループ全体に対し、コンサルティングファーム情報システム部門的な役割を担っている。一切の事業は持たず、技術提供という機能(≒SIerと私は解釈している)のみをを持っている。 IT企業への進化の過渡期ということもあり、リクルートらしい部分とSIerらしい部分が混ざり合った状態だ。

リクルートテクノロジーズのリクルートらしさ

スピード感、ボトムアップ、人材を大事にする等リクルートらしさは機能会社になっても残っている。

ボトムアップ

手を挙げた人がやれる。リクルートは社員皆経営者主義、つまり商売の勉強が出来る会社である。(江副浩正リクルートのDNA」)やりたい事を、上長や役員にプレゼンし、その効果をロジカルに示すことが出来れば、チャンスが貰え、それが成功すれば拍手喝采される。 例えば、2014年2月のiOSアプリXcode5ビルド必須化時に、入社一ヶ月ちょっとの私が取りまとめたいと手を挙げ、チャレンジさせてもらったりしている。

圧倒的当事者意識

自らがコミットした事は、当事者意識をもってやりきるということ。ATIと略され、スタンスとか魂と言われる場合もある。「お前ならどうする?」と聞かれる事も多い。社内で活躍している人材は、この当事者意識が強く、どんな時でも働いているという印象があった。

フラットな社風

私が配属された部署の、グループマネージャーは20代。また、役員や社長とも会議依頼を送れば普通に話に行く事ができる。また、社長含めどんな人にも「さん」付けで呼ぶ文化であり、意見をぶつけることも多々ある。そして入社年月に関係なく筋の良い意見であれば採用される。

個人の能力開発

リクルートグループ全体に言えるが、個人の能力開発に強く力を入れている。2011年頃から導入されたWill Can Mustシートがその仕組の一つである。Willは何を実現したいか、Canは何ができる・できるようになりたいのか、Mustは達成すべきミッションを示している。このWillに関しては、会社内だけでなく、辞めた後の事も記載する。社外の活動を含めて、中長期的なキャリアを上長が一緒になり議論できる環境は他にはないのではないだろうか。

また、優秀な開発者もいるので、そういう人たちから手厚くサポートをしてもらえる環境はよかった。スタートアップに行っていたら、そんな余裕はなかっただろう。共通化とか標準化みたいな意識も生まれた。今アプリを作ってる上での基礎はここでほとんど学んだ。

ステップ休暇

以下リクルートテクノロジーズの公式ウェブサイトから引用。

最大連続28日間、勤続3年以上の社員なら誰でも3年ごとに取得できる長期休暇です。心身をリフレッシュしたり、自己の成長機会にするなど、目的は自由。加えて、それを応援する手当として一律30万円が支給されます。

パートナー文化

業務委託の人たちを、外注と呼ばず、パートナーと呼ぶ文化がリクルートにはあり、リクルートテクノロジーズも例外ではない。同じサービスを共に作っていく、パートナーだからとのこと。このパートナーと社員の境目が他社に比べると薄いのが特徴で、グループの戦略や、開発推進をパートナーが担っていたりする。このパートナーの大半は非常に優秀な人が多く、〜〜のコミッターみたいな人もいるとか。

自由な社風

これは開発者と相性の良い文化。服装自由でフレックス制。コアタイムがあるグループもある。

社内SIer的な要素

IT企業に生まれ変わる為にこれらの要素は徐々に変わってくるのだと思う。採用にも力を入れ、中途でユーザー企業で働きたいみたいな人(@hotchemiとか)を集めているので、ボトムアップリクルートの社風なら改善されていくのではと思っている。

事業を持っていない

機能会社なので、自分の担っているサービスの方針に入り込むには、(どこの会社でも同じだとは思うが)それなりの地位を築くか、大きな信頼を獲得するかしないと難しい。その人が圧倒的当事者意識を持っていない限りは、あくまで下請けSIerと代わらない仕事しか出来ない仕組みになってしまっている。特にリクルートらしい体育会系文化に馴染めない開発者はキツイのではと思っている。

開発よりも推進

コーディングは上記の通りパートナーが実施していることが多いため、まだまだ社員には推進能力が求められることの方が多い。数少ない開発が出来る社員もその背景知識を理由に、炎上プロジェクトに推進リーダーとして投入されている。人を動かして価値を生む方が、大きな価値を生むというのも分かるが、開発者でいたい私にとっては推進業務よりも開発がしたいと思ってしまう。

人月

独立した機能会社なので、費用計算は人月単位となる。これ自体悪だと思わないが、既に多く語られているように人月計算のビジネスモデル自体には大きな問題点があるのは昔から語られてきたこと。

一括納品のビジネスモデル、多重請負構造、ディフェンシブな開発体制

SIerを退職しましたより

本当のSIerと違い、このモデルは事業会社との関係性、ホールディングスの一声で大きく変わる可能性があり、私は開発と事業が一体になる兆しがあるのではと思っている。

その他伝えておきたい事

国際的人材がまだまだ少ない

最低限の英語すら喋れる人材が少ない。全社員が集まる会で「英語は喋れなくても、海外出張大丈夫でした」って聞いた時には、頭が痛くなった。逆に、国際人材にとってはチャンスをつかみやすい環境ではある。現に、私は一年間に二回海外出張に活かせて貰っている行っている。ただし、国際人が力を存分に発揮出来る環境かで言うと怪しい。海外の売上を増やしているとは言え、リクルートグループの主な売上は国内のメディアであり、特にリクルートテクノロジーズが係る領域はドメスティックな領域が多い。今後、よりグループ全体の技術力を支える企業として、国際的な技術者の採用は必須だと考えている。

定年まで働ける

リクルートテクノロジーズ入社時に「定年まで働ける環境を提示したい」と言われびっくりした。リクルートは三年で辞めて独立するための会社かと思っていたからだ。リクルートテクノロジーズでは、独立支援の制度よりも、長く働くことを前提にした退職金制度となっていた。ここが、リクルートらしさとSIerらしさの混ざり合った不思議な感じを生んでいるのだろうか。。。

メディアテクノロジーラボとアドバンスドテクノロジーラボ

ウェブ系の新規事業はホールディングス管轄のメディアテクノロジーラボが担っているため、リクルートテクノロジーズは既存メディアのエンハンス再構築がアプリ領域はメインになる。そう、会社として新規事業に取り組むのが難しい構造になっている。これ自体は悪ではない、なぜなら社員各自はNew Ringという社内起業プログラムに参加し、採択されればメディアテクノロジーラボに移籍できるからだ。またしても、主体性が問われている。

また、本当のギークが集まっているらしいのがアドバンスドテクノロジーラボ(ATL)である。自然言語処理を用いてうんぬんとからしい。自分が直接ATLに関わっていないのでよく分かっていない事実。

まとめ

リクルートテクノロジーズに対する愛を語った。 IT企業として生まれ変わる過渡期の話であり、徐々に変化していくのだと思う。 リクルートグループIT化の中核として働いてみたい人がいたら是非中途採用に申し込んで欲しい。

Qiita API v2 Hackathonに参加してきた:写真撮ってQiita Teamにアップロード

iOS Engineering

このエントリーは、株式会社リクルートテクノロジーズ Advent Calendar 2014の17日目です。

2014/12/13、リクルート本社サウスタワーで行われた Qiita API v2 Hackathon で、QiitaSnapなるアプリを作り発表させていただきました。

商品はとても豪華。以下のとおり。

素敵なサウスタワー41階の景色も相まって、とても楽しい会でした。

リクルートが出てくるのはここまでです。

Hackathonのテーマ

今回のHackathonのテーマは、Qiita APIv2を利用して毎日の開発が楽しくなるツールの開発でした。 私のチームではQiita Teamを使っており、UIUX設計時のホワイトボード上のメモを議事録として投稿することがよくありました。 これが意外と面倒なので、三秒でQiita Team上にアップロードできるiOSアプリを作りました。

つくったもの

UIImagePickerControllerを用いて写真を撮影し、Parseに画像をアップロードし、Qiita API v2で画像をアップロードするシンプルな仕組みです。 カメラに関してはUIImagePickerControllerを用いないでもAVFoundation Frameworkを用いればカスタマイズ可能なカメラを実装できるのですが、短時間での実装だったためより簡単に実装できるUIImagePickerControllerを用いました。

まず、起動直後にカメラを起動し、その画面上で投稿するQiita Teamを選択するボタンを表示。

f:id:jeffsuke:20141217154217p:plain

カメラの部分は、UIViewControllerの上に以下のコードを書いている。

- (void)viewDidLoad
{
    [super viewDidLoad];
    
//    UIImagePickerControllerの初期設定
    self.picker = [[UIImagePickerController alloc] init];
    self.picker.delegate = self;
    self.picker.allowsEditing = NO;

//    起動するのはカメラ
    self.picker.sourceType = UIImagePickerControllerSourceTypeCamera;
    
//    ボタンの設置
    UIButton *button = [UIButton buttonWithType:100];
    [button setBackgroundImage:[UIImage imageNamed:@"team"] forState:UIControlStateNormal];
    [button setFrame:CGRectMake(0, [UIScreen mainScreen].bounds.size.height - 200, 100, 42)];
    [button addTarget:self action:@selector(teamAction:) forControlEvents:UIControlEventTouchUpInside];
    self.picker.cameraOverlayView = button;
    
    [self presentViewController:_picker animated:YES completion:NULL];
}

これでカメラの実装は完了。

カメラで撮影後は、UIImagePickerViewControllerDelegate- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)infodelegateメソッドから撮影画像を取得し、画像サイズをUIGraphicsBeginImageContextにて変更している。

// Resize image
    UIGraphicsBeginImageContext(CGSizeMake(640, 960));
    [image drawInRect: CGRectMake(0, 0, 640, 960)];
    UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

これにより生成された画像をParseにアップロード。 その画像をリクエストするURLを取得するためにPFFileオブジェクトを取得する。

//PFFileオブジェクトを取得する
    PFFile *fileData = [objectData objectForKey:@"imageData"];
            
//画像データURLを取得する
    imageURL = fileData.url;

最終的にこのURLを元にPOSTする内容を生成。 今回は、時間がなく平文で、一つのStringで以下のようにした。

NSString *json = [NSString stringWithFormat:@"{\"body\": \"![](%@)\",\"tags\": [{\"name\": \"QiitaSnap\"}],\"title\": \"議事録 %@\",\"tweet\": true}", imageURL, stringFromDate];

これをQiita API v2にて、/api/v2/itemsエンドポイントにアップロードしすると以下のように投稿投稿される。

f:id:jeffsuke:20141217155723p:plain

これから

外に見せれる形にして、リリースしたい。

まとめ

  • Qiita API v2 Hackaton出た。
  • Qiita APIは使いやすい。
  • ハッカソンで体力使ってカメラアプリ作った。

忘れないでGCD(復習しよう)

iOS Engineering

このエントリーは、iOS Advent Calendar 2014 の 2日目です。 2日目なので、Swiftとかではない送りバンドな記事で行きます。

非同期処理が多く求められるモバイルアプリ開発の現場では、ReactiveCocoaやRxJava等のFrameworkが注目を浴びている。 しかし、意外と基本となるGCD(Grand Central Dispatch)のことを忘れがち。

FacebookTwitterからタイムラインを取得しいい具合に表示する案件をやっていた時、非同期処理とNSNotificationを多様した難解な実装となっていた。 これもdispatch_groupやdispatch_barrier_asyncを使えば解決できるんだよね。 いい機会だし、復習してみよう。

GCDとは

Dispatch queueにBlocksとして実行したいタスクを渡し実行できる。このqueueには2種類ある。

  • Serial dispatch queue:タスクを逐次的に実行
  • Concurrent dispatch queue:他のタスクを待たずに実行

実際に使うときは、描画を行うメインスレッドであるMain dispatch queue(Serial dispatch queue)と、iOSがいい具合に判断しスレッドを作り実行してくれるGlobal dispatch queueのどちらかを選択して使う事になる。Global dispatch queueでは、優先度が選べるがこれはあくまで目安なので注意。

Serial dispatch queue

    dispatch_queue_t queue = dispatch_queue_create("com.jsk.test", NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(queue, ^{NSLog(@"%d", i); });
    }

Output

0
1
2
3
4

Concurrent dispatch queue

    dispatch_queue_t queue = dispatch_queue_create("com.jsk.test", NULL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(queue, ^{NSLog(@"%d", i); });
    }

Output(順番は実行時によって異なる)

4
0
2
1
3

Dispatch Groupでちょっと待つ

Dispatch Groupを使うと、queueに追加する処理をグループ化し、全ての処理が完了した事を受け取る事ができる。 これを使えば、非同期通信が複数走っている場合等の処理をまとめることができて便利。 使い方はとっても簡単。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    for (int i = 0; i < 3; i++) {
        dispatch_group_async(group, queue, ^{
            NSLog(@"%d", i);
        });
    }
    dispatch_group_notify(group, queue, ^{
        NSLog(@"Done");
    });

Output

2
0
1
Done

また、dispatch_group_waitを用いる事で処理をその箇所で止める事もできる。

Dispatch Barrier Async

上記Dispatch Groupと少し似ているが、Dispatch barrier asyncでは、Concurrent dispatch queueに追加された処理が実行完了されるまで待ち、Serial Dispatch Queueに新たなタスクを追加し、そのタスクが実行完了されるまで待つことが可能だ。 例えば以下のように使うことができる。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{NSLog(@"1");});
    dispatch_async(queue, ^{NSLog(@"2");});
    dispatch_async(queue, ^{NSLog(@"3");});
    dispatch_barrier_async(queue, ^{NSLog(@"wait");});
    dispatch_async(queue, ^{NSLog(@"4");});
    dispatch_async(queue, ^{NSLog(@"5");});

dispatch_barrier_asyncメソッドを使うだけで、前の処理を待ってくれる。なんて便利なんだ。

まとめ

GCDを復習した。 dispatch_groupやdispatch_barrier_asyncを用いて他の非同期処理を待つ事ができる。 これでsemaphoreで無理やり処理していた箇所が書き換える事ができそうだ。

おまけ

Rebuild.fmで紹介されていたHBOのSilicon Valleyを見ている。あの近辺でのスタートアップがリアルに描かれていてかなり面白く、おすすめだ。

iOSの、画面遷移時のメモリリークが止まらなかった話

iOS Engineering

先日、画面遷移時にメモリが開放されず、徐々にメモリ利用率が上昇する現象に苦しまされた。 Instrumentsで調べてみても、リークは見られなかった。何が問題だったのか。それはdispatch_afterを用いたループするアニメーションだった。

f:id:jeffsuke:20141002085323p:plain

dispatch_afterや、NSRunLoopNSTimer等を用いてループ処理を実行していると、ownerとなるオブジェクトが解放されようとしても、これらのオブジェクトが強参照するために、解放されないようだ。

今回実装していた物

UIImageViewのカスタムクラスの上に、UIImageが乗っており、animationImagesNSTimerによって、フェードイン・アウトするアニメーションの挙動を実装した。 参考:iphone fading of images

f:id:jeffsuke:20141002085957g:plain

元々の実装

これだと、ループが回り続け、ownerのオブジェクトは永遠に開放されない

NSTimer *timer = [NSTimer timerWithTimeInterval:4.0 
                                              target:self 
                                            selector:@selector(onTimer) 
                                            userInfo:nil 
                                             repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [timer fire];

対策後

NSTimerをpropertyとして持ち、Viewが消える時に、invalidateし、解放する必要がある。

@interface JSKSwipeViewController ()
@property (nonatomic) NSTimer *timer;
@end

@implementation JSKSwipeViewController
- (void)startAnimation
{
    _timer = [NSTimer timerWithTimeInterval:4.0
                                     target:self
                                   selector:@selector(onTimer)
                                   userInfo:nil
                                    repeats:YES];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    
    // stop animation and release
    [self.timer invalidate];
    self.timer = nil;
    
}

UINavigationControllerの戻るを消し、別のボタンで戻る

iOS Engineering

ViewDidLoadにて以下のメソッドを呼ぶ。 UINavigationController上の戻るボタンが消える。

[self.navigationItem setHidesBackButton:YES];

対象となるIBAction内で以下を呼ぶ。

[self.navigationController popViewControllerAnimated:YES];

結果、以下のように。

f:id:jeffsuke:20140923231854p:plain