株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

こんにちは。iQONのバックエンドエンジニアを担当しているjoeと申します。 最近、iQONのお知らせ機能のDBをMySQLからDynamoDBへ移行しました。 移行する際に発生した問題点である並列処理によるデータ欠損とProvisioning超過の対策を書きます。 間違っているところや改善点があればご指摘ください。よろしくお願い致します。 お知らせ機能とは お知らせ機能とは、facebookで言うところの「◯◯さんがあなたの投稿に「いいね!」といっています」のような、ユーザーに対するアクションがあったことを通知したり、アイテムが値引きされた、アイテムの在庫が少なくなった等のlikeしたアイテム情報をユーザーに通知する機能です。 既存のお知らせ機能の問題点 既存の構成における問題点は以下の二点です。 データの肥大化 レスポンスが遅い お知らせのデータ構造は、下記のようになっています。 MySQL : マスターデータ redis : キャッシュ 図 お知らせのデータ保持の構成図 redisにはユーザーごとのMySQLのお知らせidを直近100件分保持しています。 お知らせはユーザーの行動やアイテムのステータスの変化の分レコード数が存在するので、MySQLのレコード数が尋常ではない量になります。また、ユーザー分のindexをredisに保持しているので、これではユーザーの人数が増えれば増えるほどredisの容量を食う事になります。MySQLのデータが大きくなればなるほどレスポンスが遅くなっていきます。 この問題を解決するために、awsの DynamoDB に移行することにしました。 なぜDynamoDB? DynamoDBは スキーマレスなkey, valueストレージ 10ms未満のレイテンシー 大規模なデータにも柔軟に対応 自動で冗長化してくれるのでメンテが楽 のような特徴があり、今回解決したい問題の2つである速度改善とデータの肥大化への対策が期待できます。 また、DynamoDBのデメリットである柔軟な検索や集計処理が不得意という点に関しては、今回のケースで影響は少ないと考えられます。 DynamoDBでのデータ保持 ユーザーごとにkeyを持たせて、お知らせはJSONを要素とする配列を格納するようにしました。元々redisで100件分しかデータを保持していなかったので、配列の長さは100件までとしました。 update_countという項の存在意義は後ほど説明します。 event_typeというのは発生したeventごとに割り振られているユニークな数値です。これによってお知らせの種類を見分けています。 # ユーザー1人分のお知らせデータ feedback_user_id(primary_key): "feedback:user:40" feedbacks: [ {"event_type":11,"set_id":1079944,"create_time":"2016-04-30 12:43:56 +0900","feedback_id":500000566}, {"event_type":11,"set_id":1080144,"create_time":"2016-04-30 12:43:58 +0900","feedback_id":500000568}, {"event_type":11,"set_id":1079456,"create_time":"2016-04-30 12:44:00 +0900","feedback_id":500000570}, {"event_type":11,"set_id":1073230,"create_time":"2016-04-30 12:44:01 +0900","feedback_id":500000572}, {"event_type":11,"set_id":1068334,"create_time":"2016-04-30 12:44:03 +0900","feedback_id":500000574}, {"event_type":11,"set_id":1064505,"create_time":"2016-04-30 12:44:05 +0900","feedback_id":500000576}, {"event_type":11,"set_id":1065469,"create_time":"2016-04-30 12:44:06 +0900","feedback_id":500000578}, {"event_type":11,"set_id":1055427,"create_time":"2016-04-30 12:44:07 +0900","feedback_id":500000580} ], update_count: 766 お知らせ機能のdynamo移行における問題点 お知らせ機能のdynamo移行における問題点は2つです。 provisioning量超過 並列処理実行によるデータの欠損 お知らせの発行において予測できない書き込みの増加が発生することがあるので、provisioning量超過時のエラーの対処をする必要がありました。 また、iQONのお知らせデータの発行はworkerによる遅延処理のため、複数のworkerが同時にdbにアクセスしてデータを書き換えるという事例が発生し、データが欠損する恐れがありました。 それぞれの問題についての今回の対応を書いていきます。 provisioning量の調節 いきなりですが、provisioning量を自動調節する方法は弊社の別のブログで公開しているのでそちらを参照してください! DynamoDBの導入とDynamic DynamoDBを用いたプロビジョニング量自動調整 ただ、上記の場合だと急激なthroughput(読み込み・書き込み量)の変化に追いつけずに書き込みに失敗することがあるので、今回はprovisioningを超過して失敗した場合、お知らせの発行処理をenqueueしてworkerに処理させるようにしました。 実装例 お知らせをDynamoDBにinsertする処理をwith_retryメソッドで囲み、insertが失敗した場合にworkerにenqueueしています。 # retry処理 def with_retry begin yield rescue Aws :: DynamoDB :: Errors :: ProvisionedThroughputExceededException , Aws :: DynamoDB :: Errors :: ConditionalCheckFailedException => e if get_event_value[ :retry_enqueue_limit ] != 0 @retry_enqueue_limit -= 1 Sidekiq :: Client .enqueue( FeedbackDynamoFailedInsertWorker , get_event_value) else :: Rails .logger.info " [FEEDBACK ENQUEUE RETRY MAX] #{ e.message }" raise e end end end # 失敗のqueueを処理するworker class FeedbackDynamoFailedInsertWorker < Worker sidekiq_options :queue => :feedback_failed_insert , :retry => false , :backtrace => true def perform (params) sleep( 0.005 ) begin FeedbackUserIndexDynamo .new(params[ " user_id " ]).update_feedback!(params[ " event_value " ], params[ " retry_enqueue_limit " ]) rescue => e :: Rails .logger.info " [ERROR][FeedbackDynamoInsertWorker] " + e.message end end end 上記を実装することで完全にthroughputによるエラーをなくせました! 並列処理実行によるデータの欠損 iQONのお知らせデータの発行はworkerによる遅延処理のため、複数のworkerが同時にdbにアクセスしてデータを書き換えるという事例が発生します。そこで起こりえるのが、同時データ取得・書き込みによるデータの欠損です。 例えばworker1がDynamoDBにアクセスしてお知らせのデータを取得し、お知らせの配列にデータを追加してupdateをする。その間にworker2がDynamoDBにアクセスして値を追加、worker1の動作を上書いてしまうといった不整合が発生します。 そこで、 DynamoDBの機能としてある"Conditional Update(条件付き書き込み)"という楽観的ロックを用いてデータの欠損を防ぎました。 "Conditional Update"とは、DynamoDBのupdate処理のoptionの一つであり、こちら側が指定した条件を満たす時のみデータをupdateするようにする処理です。 実際の構成 Conditional Updateの条件にするため、上記でも説明したupdate_countというcounterをお知らせの更新のたびにincrementします。 Conditional Updateの条件を下記のように設定して同時書き込みを防ぎました。 条件:  update_count == previous_update_count 図 お知らせ更新の構成図 ※ iQONのお知らせは、過去のお知らせ情報を元に丸め処理(同じコーデに対するLikeのお知らせは1個に丸める等)をするため、DynamoDBからお知らせの配列ごと取得し、お知らせを書き換えて配列をDynamoDBに上書きするというフローになっています。 実装例 insert def add! (feedbacks, update_count, previous_update_count) options = { table_name : " feedbacks " , key : " feedback:user:1 " , update_expression : " SET update_count = :update_count, feedback = :feedbacks " , condition_expression : " update_count <= :previous_update_count " , expression_attribute_values : { " :feedbacks " => feedbacks, " :update_count " => update_count, " :previous_update_count " => previous_update_count } } @@dynamodb .update_item(options) end update処理を行うときは、 update_item を使用しますが、 update_item のオプションは AttributeUpdates でなく、 UpdateExpression を使用することが推奨されています。 また、update_expressionでは下記の4つのupdateのオプションがあります。 SET REMOVE ADD DELETE 今回は ADD より柔軟な SET を使用しています。(ADDは数値とset型の配列しか受け付けない) また、 SET を使えば、数値をincrement/decrementすることもできるので、update_countのincrementをDynamoDB側で行う事もできます。詳しくは参考資料を御覧ください。 (今回は自前でincrementしています。) update_expressionの参考資料 Modifying Items and Attributes with Update Expressions その他の注意点としては、 数値の型がintではなくBigDecimal Time型がないので時間は文字列で値を入れる 等があります。 Conditional Updateの失敗数の監視と失敗時の対応 Conditional Updateのfail、つまり、同時書き込みによる失敗数は下記の図のようになっています。 このように、DynamoDBではthroughtputの量やConditional Updateのfail等、awsコンソールで表示してくれます。 Conditional Updateの失敗をした場合は、上記で説明した用にworkerでenqueueして処理を再度行うようにしています。 結論 各対処法によってデータ欠損、Provisioning量超過によるデータの書き込みエラーで飛んでくるsentryのエラーを0にできました! DynamoDBを触ってみて、配列の上限がx個に達したら自動的に古い要素を削除してくれるような機能があればいいなと思いました。 このような機能がある、又はDynamoDBの◯◯を使えばコレが実現できそう等がありましたら教えて下さい! 終わりに VASILYではDynamoDBをごりごりに使ってみたい仲間を募集しています。 新規事業の開発も始まりましたし、ご興味がある方はぜひこちらからご応募ください。
iOSアプリを開発しているエンジニアの庄司です。 今回は、iPhoneでのテザリング中や通話中に、ステータスバーの高さが変わることによる表示崩れの対応について紹介します。 TL;DR iPhoneでテザリング中、 UITabBar が画面からはみ出したりすることへの対応方法です。 RootViewControllerのviewに UITabBarController のviewを addSubview: するときは、親viewの中に収まるようにAutoLayoutを設定します。 scrollView.contentInset の調整には topLayoutGuide.top を使います。 サンプルアプリをGitHubにあげています。[ GitHub ] 何が起きていたか テザリング中や通話中などにレイアウトが崩れる UITabBar が20pts下がって、画面からはみ出しまう ViewController構成
 UIViewController // RootViewController |- UITabBarController |- UINavigationController | |- UITableViewController |- UINavigationController |- UITableViewController RootViewController内の viewDidLoad で UITabBarController をコードで追加しています UITabBarController をRootViewControllerとするXcode Projectでは、この問題は発生しません Viewデバッガで見てみる 下記のような位置関係になっているため、 UITabBar がはみ出して見えます。 
 // UIWindowからの相対的なframe UIWindow : ( 0 , 0 , 375 , 667 ) RootViewController : ( 0 , 20 , 375 , 647 ) UITabBarController : ( 0 , 40 , 375 , 647 ) UITabBar がはみ出してしまう問題の対応 UITabBarController のviewを addSubview した後、AutoLayoutを設定して UITabBarController のviewがsuperviewの中に収まるようにします Before // RootViewController.swift override func viewDidLoad () { super .viewDidLoad() let tc : UITabBarController = createTabBarController() addChildViewController(tc) view.addSubview(tc.view) tc.didMoveToParentViewController( self ) } After // RootViewController.swift override func viewDidLoad () { super .viewDidLoad() let tc : UITabBarController = createTabBarController() addChildViewController(tc) view.addSubview(tc.view) // view の中に収まるように、tabBarController.view に constraintを設定 view.addFittingConstraintsFor(tc.view) tabBarController.didMoveToParentViewController( self ) } extension UIView { /** childViewが同じサイズに収まるように、constraintsを設定する - parameter childView : 子View */ func addFittingConstraintsFor (childView : UIView ) { let constraints = [.Top, .Leading, .Bottom, .Trailing].map { NSLayoutConstraint( item : childView , attribute : $0 , relatedBy : .Equal, toItem : self , attribute : $0 , multiplier : 1.0 , constant : 0.0 ) } childView.translatesAutoresizingMaskIntoConstraints = false addConstraints(constraints) } } 修正結果 UITabBarController のviewはRootViewControllerのviewと同じ位置、サイズになりました。 コンテンツ開始位置のズレ このサンプルでは特に問題はありませんが、テザリング中に UITableView のコンテンツ開始位置がズレる現象もよく見かけます。 ステータスバーのサイズ UIApplication の statusBarFrame が変わります。 テザリング中は見た目通り、高さが40で返ってきます。 // 通常時 statusBarFrame : ( 0 , 0 , 375 , 20 ) // テザリング中 statusBarFrame : ( 0 , 0 , 375 , 40 ) しかし、上記のViewデバッガで見てわかるように、RootViewControllerが 20 だけ下がります。 
下記のようなコードを書くと、通常時と比べてコンテンツ開始位置が 20 だけ下がって見えてしまうでしょう。 // 通常時: 20 / テザリング時: 40 scrollView.contentInset.top = statusBarFrame.height topLayoutGuide を使う UIViewController の topLayoutGuide はテザリング中でも値が変わりません。 topLayotGuide.top はステータスバーやナビゲーションバーの高さも考慮した値を返します。 ランドスケープ時にステータスバーが消えた場合は、ナビゲーションバーの高さだけ返してくれます。 // 通常時: 20 / テザリング時: 20 scrollView.contentInset.top = topLayoutGuide.top 所感 特に情報が見つからなかったので、独自の解決策です。 もっと良い方法や、Appleの公式なドキュメントがあれば教えて下さい。 UITabBarController をRootViewControllerとしてStoryboardで実装した場合は、今回の問題は発生しませんでした。 国内外・有名無名問わず、多くのアプリで同じようなレイアウトの崩れがいくつか見られます。 開発者が意識することなく、うまいことiOS側で管理してほしいものです。
今回はHTMLメールのテスト工数を短縮するEmail Testing Serviceを紹介したいと思います。 はじめに HTMLメールと聞くと、気が重くなるエンジニアも多いのではないでしょうか。テーブルレイアウトとインラインCSSという普段と違う開発が求められますし、各メーラー、Webメールの表示対応が必須となります。 2016年になっても何かが解決するどころか、確認するデバイスだけが増え続けています。 コーディング → 配信 → サポートクライアントで表示確認 上記をひたすら繰り返し、多くの時間を消費するのを何とかしたい、そんな思いからEmail Testing Serviceを調査し、サービスの運営に取り入れました。 Email Service メールに関するサービスはたくさんあります。 作成を簡単にするためにエディターやテンプレートを提供するサービス 配信を効率よく、簡単にできるようにするサービス 開封率など効果測定、分析を提供するサービス etc 人によって求めている機能は違いますが、現在のプロダクトでは「様々なデバイスでの表示確認に時間がかかっている」という問題が大きいです。そのため今回は HTMLメールのプレビュー機能 について提供しているサービスを紹介します。 Litmus まず最初に Litmus の紹介です。 HTMLのプレビュー機能に特化しているサービスです。 簡単な会員登録を行い、無料で使用することができます。 無料で提供されるクライアントは3種類で「Outlook 2013」「Gmail (Chrome)」「iPhone 6S」でのプレビューが可能です。 HTMLを書きながら各クライアントの見え方が確認できます。 若干反映が遅い気がしますが、それでも十分開発工数の短縮が望めると思います。 個人の感想ですが、使用した中で一番UIが分かりやすく、登録が簡単だったサービスです。 MailChimp 次に MailChimp の紹介です。 HTML作成から配信、効果測定まで提供されており、もちろんプレビュー機能も存在します。プレビュー機能は課金制となっており、tokenという物を購入しないといけません。1つのクライアントを1回テストするのに1token使用し、会員登録の時点では10token付与されます。最小単位の購入で25token/$3と価格自体は安いのですが、どれくらいの頻度でテストをするかによってコストが変わってきます。 とても評判の良いサービスですが、今回はプレビュー機能のみの利便性を求めたため導入は見送りました。「Powered by litmus」とありますが、サポートクライアント数は41ほどでした。 Email on Acid 最後に紹介するのが Email on Acid です。 Email on Acidについては登録しなくても こちら からサンプルが確認できます。 サポートクライアント数が70以上あり、プレビューの切り替えも楽に出来る印象を持ちました。1週間無料のトライアル期間もあり、価格も月額$45からと比較的安い印象を受けました。 まとめ HTMLメールの実装は昔からあまり変わっていませんが、プロバイダー、マーケティング、プレビューといった形でリッチになっている印象を受けました。紹介した3社の優劣は、必要としている機能、規模などによりますので、ぜひご自身にあったサービスを選択してみてください。 今回は海外サービスの紹介となりましたので、日本語でのHTMLメールをテストする際は注意が必要ですが、これらのサービスを使用して少しでもHTMLメールのテストが幸せな物になればと思います。 さいごに VASILYではエンジニアを募集しています。ご興味のある方は是非ご応募よろしくお願いいたします。 https://www.wantedly.com/projects/45354 www.wantedly.com https://www.wantedly.com/projects/42989 www.wantedly.com
こんにちは、エンジニアの遠藤です。 最近iQONアプリのホーム画面のデザインをリニューアルしました。 タブを使ったデザインにすることで、iQON内にある多くのコンテンツが見やすくなりました。 今回はこのタブ機能の実装についてざっくりと紹介しようと思います。 実装したものはライブラリーとしてGitHubに公開しているので、ぜひ使ってみてください! github.com 機能 今回実装した機能は下記の3つです 1. スワイプでページを無限に表示切り替え 2. タブは無限スクロール 3. タブをタップしたらタップした項目のページを表示 実装について 1. スワイプでページを無限に表示切り替え スワイプしたらページの表示を切り替えたいので UIPageViewController を継承した TabPageViewController というクラスを実装しました。 今回は無限にページの表示切り替えをしたいので UIPageViewControllerDataSource のViewControllerを返すメソッドで調整をします。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] private var beforeIndex : Int = 0 private var currentIndex : Int ? { guard let viewController = viewControllers?.first else { return nil } return pageViewControllers.indexOf(viewController) } override func viewDidLoad () { super .viewDidLoad() // 初期化処理など dataSource = self delegate = self setViewControllers( [pageViewControllers[ 0 ]], direction : .Forward, animated : false , completion : nil ) } } // MARK: - UIPageViewControllerDataSource extension InfinityTabPageViewController : UIPageViewControllerDataSource { private func nextViewController (viewController : UIViewController , isAfter : Bool ) -> UIViewController ? { guard var index = pageViewControllers.indexOf(viewController) else { return nil } if isAfter { index ++ } else { index -- } if index < 0 { index = pageViewControllers.count - 1 } else if index == pageViewControllers.count { index = 0 } if index >= 0 && index < pageViewControllers.count { return pageViewControllers[index] } return nil } func pageViewController (pageViewController : UIPageViewController , viewControllerAfterViewController viewController : UIViewController ) -> UIViewController ? { return nextViewController(viewController, isAfter : true ) } func pageViewController (pageViewController : UIPageViewController , viewControllerBeforeViewController viewController : UIViewController ) -> UIViewController ? { return nextViewController(viewController, isAfter : false ) } } 2. タブは無限スクロール タブは無限でスクロールできるように、表示したい要素数の3倍を用意してスクロール位置がしきい値を超えたら中央に戻します。 今回は UICollectionView をつかって TabView として実装しています。 UICollectionView ではなく UIScrollView でも実装はできるのですが、表示したい要素が増えた時にメモリを圧迫してしまうので採用しませんでした。 // TabView.swift class InfinityTabView : UIView { private var pageTabItemsWidth : CGFloat = 0.0 // MARK: - UICollectionViewDataSource func collectionView (collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int { return pageTabItemsCount * 3 // 表示したい要素数の3倍を返す } func collectionView (collectionView : UICollectionView , cellForItemAtIndexPath indexPath : NSIndexPath ) -> UICollectionViewCell { // Cellを返す処理 } // MARK: - UIScrollViewDelegate func scrollViewDidScroll (scrollView : UIScrollView ) { if pageTabItemsWidth == 0.0 { pageTabItemsWidth = floor(scrollView.contentSize.width / 3.0 ) // 表示したい要素群のwidthを計算 } if (scrollView.contentOffset.x <= 0.0 ) || (scrollView.contentOffset.x > pageTabItemsWidth * 2.0 ) { // スクロールした位置がしきい値を超えたら中央に戻す scrollView.contentOffset.x = pageTabItemsWidth } } } 作成した TabView を TabPageViewController に表示します。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] var pageTabItems : [String] = [] override func viewDidLoad () { super .viewDidLoad() // 初期化処理など let tabView = TabView() tabView.translatesAutoresizingMaskIntoConstraints = false let height = NSLayoutConstraint(item : tabView , attribute : .Height, relatedBy : .Equal, toItem : nil , attribute : .Height, multiplier : 1.0 , constant : TabView.tabViewHeight ) tabView.addConstraint(height) view.addSubview(tabView) let top = NSLayoutConstraint(item : tabView , attribute : .Top, relatedBy : .Equal, toItem : topLayoutGuide , attribute : .Bottom, multiplier : 1.0 , constant : 0.0 ) let left = NSLayoutConstraint(item : tabView , attribute : .Leading, relatedBy : .Equal, toItem : view , attribute : .Leading, multiplier : 1.0 , constant : 0.0 ) let right = NSLayoutConstraint(item : view , attribute : .Trailing, relatedBy : .Equal, toItem : tabView , attribute : .Trailing, multiplier : 1.0 , constant : 0.0 ) view.addConstraints([top, left, right]) tabView.pageTabItems = pageTabItems } } 3. タブをタップしたらタップした項目のページを表示 iQONでは UICollectionViewCell に UIButton をのせた TabCollectionCell を実装しています。 // TabCollectionCell.swift class InfinityTabCollectionCell : UICollectionViewCell { var pageItemPressedBlock : ((index : Int , direction : UIPageViewControllerNavigationDirection ) -> Void )? var pageTabItemButtonPressedBlock : (Void -> Void )? override func awakeFromNib () { super .awakeFromNib() // 初期化処理など } // MARK: - IBAction @IBAction private func pageItemButtonTouchUpInside (button : UIButton ) { pageTabItemButtonPressedBlock?() } } 作成したCellを TabView の UICollectionView に表示していきます。 // TabView.swift class TabView : UIView { var pageItemPressedBlock : ((index : Int , direction : UIPageViewControllerNavigationDirection ) -> Void )? var pageTabItems : [String] = [] } // MARK: - UICollectionViewDataSource extension TabView : UICollectionViewDataSource { func collectionView (collectionView : UICollectionView , cellForItemAtIndexPath indexPath : NSIndexPath ) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier(InfinityTabCollectionCell.cellIdentifier(), forIndexPath : indexPath ) as ! InfinityTabCollectionCell configureCell(cell, indexPath : indexPath ) return cell } private func configureCell (cell : InfinityTabCollectionCell , indexPath : NSIndexPath ) { // 無限スクロールのために要素数を3倍用意しているので要素群のindexを計算します let fixedIndex = indexPath.item % pageTabItemsCount cell.title = pageTabItems[fixedIndex] cell.isCurrent = fixedIndex == (currentIndex % pageTabItemsCount) cell.pageTabItemButtonPressedBlock = { [weak self , weak cell] in self ?.pagingViewController(fixedIndex : fixedIndex , nextIndex : indexPath.item ) } } private func pagingViewController (fixedIndex fixedIndex : Int , nextIndex : Int ) { // 遷移する先のページのindexをみてページ送りの方向を決めてからページの表示を切り替えます var direction : UIPageViewControllerNavigationDirection = .Forward if (nextIndex < pageTabItemsCount) || (nextIndex < currentIndex) { direction = .Reverse } pageItemPressedBlock?(index : fixedIndex , direction : direction ) } } TabPageViewController にcellのタップ時の処理を書きます。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { var pageViewControllers : [UIViewController] = [] var pageTabItems : [String] = [] override func viewDidLoad () { super .viewDidLoad() // TabViewの初期化処理など      tabView.pageItemPressedBlock = { [weak self ] (index : Int , direction : UIPageViewControllerNavigationDirection ) in self ?.displayControllerWithIndex(index, direction : direction , animated : true ) } } func displayControllerWithIndex (index : Int , direction : UIPageViewControllerNavigationDirection , animated : Bool ) { let nextViewControllers : [UIViewController] = [pageViewControllers[index]] setViewControllers( nextViewControllers, direction : direction , animated : animated , completion : completion ) } } そのほか 機能については上記3つで実装できます。 上記実装のほかにページの表示を切り替え時のタブの位置を移動させるように実装しています。 大まかな実装としては、 UIPageViewController のsubviewsの scrollView を取得してdelegateをselfに設定します。 スワイプした時のscrollViewのdelegate内で処理を書いていきます。 // TabPageViewController.swift class TabPageViewController : UIPageViewController { override func viewDidLoad () { super .viewDidLoad() // 初期化処理など let scrollView = view.subviews.flatMap { $0 as ? UIScrollView }.first scrollView?.delegate = self } } // MARK: - UIScrollViewDelegate extension TabPageViewController : UIScrollViewDelegate { func scrollViewDidScroll (scrollView : UIScrollView ) { // ページを切り替えながらタブの位置を移動させる } } 詳しい実装については説明が長くなってしまうので公開しているコードを見てください。 まとめ 今回はUIPageViewControllerをつかって無限スクロールできるタブUIの実装について紹介しました。 ページの表示を切り替えるだけなどの機能だけならば、比較的シンプルに実装をすることができました。 しかし、ページの表示を切り替えながらタブの位置を移動させようとすると、かなりコード量が増えて苦労しました。 今後同じようなUIを実装しようと考えている方はぜひライブラリーを使ってみてください。 改善要望については、PRやissueをお待ちしています。 GitHub - EndouMari/TabPageViewController: Paging view controller and scroll tab view 最後に VASILYでは、一緒にiQONのアプリを開発してくれる仲間を募集しています。少しでもご興味のある方はぜひご応募よろしくお願いいたします。 https://www.wantedly.com/projects/27397 www.wantedly.com
Making Sequences work for you こんにちは! VASILYのiOSエンジニア にこらす です。 SwiftのコントリビューターとSwift Evolution SE-0053 の作者です。 他のOSSプロジェクトにも貢献してるので興味があれば Github でフォローしてください。 今回のトピックはSwift2.0以降の SequenceType というプロトコルと、その内部的な動きについて紹介します。 class や struct を SequenceType プロトコルに準拠させると、 for in ループや map , filter などを使えるようになります。 さあ、始めましょう! struct Unique <T: Comparable> { private var backingStore : [T] = [] var count : Int { return backingStore.count } func setHas (element : T ) -> Bool { return backingStore.filter { $0 == element }.count != 0 } mutating func append (element : T ) { guard ! setHas(element) else { return } backingStore.append(element) } } この Unique という構造体はとてもシンプルです。 Comparable に準拠した型の要素を append すると内部的な配列に要素を追加します。配列中に同じ要素が存在する場合は追加しません。 それでは Unique をテストしてみましょう! var mySet = Unique < String > () // Our set can mySet.setHas( "A Careless Cat" ) //false mySet.append( "A Careless Cat" ) mySet.setHas( "A Careless Cat" ) //true mySet.append( "A Careless Cat" )  //すでにある文字列 mySet.count //まだ1です! //もうちょっと項目追加しましょう! mySet.append( "A Dangerous Dog" ) mySet.append( "A Diamond Dog" ) mySet.append( "Petty Parrot" ) mySet.append( "North American Reckless Raccoon" ) mySet.append( "A Monadic Mole" ) 動物は十分入ったので、名前を print してみよう! for animal in mySet { println(animal) } あれ?うまくいきませんね。。。: なぜうまくいかなかったかというと、 for in を使うために必要な実装が足りないからです。 Stringの配列で for in を書くのは、下記の書き方と同じです。 let vowels = [ "A" , "E" , "I" , "O" , "U" ] var gen = vowels.generate() while let letter = gen.next() { print(letter) } generate() は SequenceType プロトコルのメソッドです。 Unique は SequenceType プロトコルに準拠してないので for in が動かないのです。 それでは、 SequenceType プロトコルに必要な条件を見て実装してみましょう! public protocol SequenceType { associatedtype Generator : GeneratorType public func generate () -> Self.Generator } generate() の戻り値の型は型推論が効くので、 typealias Generator = ... を書く必要はありません。 Unique を SequenceType プロトコルに準拠させて、 generate() メソッドを実装するべきですが、まだ GeneratorType のことをよく知りません。 それでは GeneratorType をチェックしましょう! Generator とは? public protocol GeneratorType { associatedtype Element public mutating func next () -> Self.Element ? } SequenceType と同じように一つのメソッドしかありません。 next() の戻り値の型は、 Element 型となっていますが型推論が効きます。 [String] であれば、実際には String になります。 Generator は next() メソッドで、保持しているデータを順番に返します。最後のデータを返したあと再度 next() を呼ぶと nil を返します。 GeneratorType を使い終わると再利用できません。同じデータセットをもう一度 GeneratorType で読みたければ、新しく GeneratorType を生成する必要があります。 そして、 generate() を実装する時に戻り値の GeneratorType と他の GeneratorType 変数の状態を共有しないようにしなければいけません。 基本的な上限がある GeneratorType : struct CountToGenerator : GeneratorType { private var limit : Int private var currentCount = 0 init (limit : Int ) { self .limit = limit } mutating func next () -> Int ? { guard currentCount < limit else { return nil } defer { currentCount += 1 } return currentCount } } var goTillTen = CountToGenerator(limit : 10 ) while let num = goTillTen.next() { print(num) } SequenceType と GeneratorType を学んだので、 Unique 専用の GeneratorType を作りましょう! class UniqueGenerator <T>: GeneratorType { private var _generationMethod : () -> T ? init (_generationMethod : () -> T ?) { self ._generationMethod = _generationMethod } func next () -> T ? { return _generationMethod() } } extension Unique : SequenceType { func generate () -> UniqueGenerator <T> { var iteration = 0 return UniqueGenerator { if iteration < self .backingStore.count { let result = self .backingStore[iteration] iteration += 1 return result } return nil } } } ここまで学んだことで、 Unique の GeneratorType の実装ができましたが、もう少し短く書けます。 AnyGenerator というGenericタイプを使うと UniqueGenerator を作る必要がありません。 extension Unique : SequenceType { func generate () -> AnyGenerator <T> { var iteration = 0 return AnyGenerator { if iteration < self .backingStore.count { let result = self .backingStore[iteration] iteration += 1 return result } return nil } } } もう一度 for in ループを書きましょう! for item in mySet { print(item) } やっと動きました! map と filter も動きます!やった! let cnt = mySet.map { Int( $0 .characters.count) } //[14, 15, 13, 12, 31, 14] mySet.filter { $0 .characters.first != "A" } //["Petty Parrot", "North American Reckless Raccoon"] Controlling Sequences with Sequences SequenceType の実装次第で、とても大きいリストや無限リストを作ることができます。 そういったリストから先頭から n 個取り出そうとすると、そのまま使うと無限ループになってしまいます。 例えば下記の ThePattern は無限リストです。 for in で使うと、 Generator が nil を返さないため、永遠に 0 か 1 を返します。 class ThePattern : SequenceType { func generate () -> AnyGenerator <Int> { var isOne = true return AnyGenerator { isOne = ! isOne return isOne ? 1 : 0 } } } // 無限ループ for i in ThePattern() { print(i) } この無限リストから class First <S: SequenceType>: SequenceType { private let limit : Int private var counter : Int = 0 private var generator : S.Generator init (_ limit : Int , sequence : S ) { self .limit = limit self .generator = sequence.generate() } func generate () -> AnyGenerator <S.Generator.Element> { return AnyGenerator { defer { self .counter += 1 } guard self .counter < self .limit else { return nil } return self .generator.next() } } } for item in First( 5 , sequence : ThePattern ()) { print(item) // 0 1 0 1 0 } いいですね! First(n, sequence: s) を呼び出すと、 s の先頭 n 個の要素を取り出せます。 s が無限リストだとしても最初の n 個しかチェックしないので効率的です。 まとめ 配列のように扱うクラスやコンテナクラスであれば SequenceType プロトコルを採用しましょう。 コードが読みやすくなりますし、自然に for in や map , filter を使えるようになります。 ぜひ気軽に SequenceType のプロトコルを使ってみてください。 最後に VASILYではSwift好きなiOSエンジニアを募集しています!興味があればぜひ応募してみてください。 https://www.wantedly.com/projects/27397 www.wantedly.com
2016年3月22日、第二回目となる Fashion Tech meetup を開催しました。前回は MERY を運営する株式会社peroli様との開催でしたが、今回は FRIL を運営する株式会社Fablic様が加わり、VASILYを含め3社での開催となりました。 イベント公開開始時、参加枠70席のところ120名を超える申し込みがあり、増枠を設けるほどの大盛況となりました。 最終的に180人を超える申し込みを頂き、FashionxTechnologyに少しずつ関心が持たれているのではないかと感じています。 弊社からも3名が登壇し、Fashion x Technologyを題材とした発表を行ってきました。 今回はその資料を公開します。残念ながら参加出来なかった方、Fashion Tech meetupを初めて知った方、是非ご一読ください。 メインセッション 「ディープラーニングを使って商品カテゴリの分類をしてみました」 弊社の新卒エンジニアの塩崎と、内定者インターンの後藤が発表しました。 塩崎は原子核物理学、後藤は観測的宇宙論・銀河形成論というファッションとはかけ離れた研究をしていた2人が、ディープラーニングでファッションのカテゴリを分類していくという発表になっています。 LT ファッションアイテムの類似画像検索を実装してみました こちらは2016年3月より弊社に入社したデータサイエンティストの中村が類似画像検索についてLTを行いました。まだ入社して3週間ですが、さっそく自身の知識と探究心で、技術的なアプローチをしています。 まとめ VASILYの資料を公開しましたが、いかがでしたでしょうか。 ファッションやWeb業界と大きく異なる経歴を持つ若いメンバーが、FashionxTechnologyに対して様々なアプローチをして活躍していることは伝わったのではないかと思います。 今回弊社の資料を公開しましたが、peroli様、Fablic様の発表も面白く、各社の個性がでたFashion Tech meetupでした。来場された方もファッションに関わる方、技術に興味を持つ方の集まりなので懇親会も活発なものでした。 Fashion Tech meetupを通して、少しずつ業界を盛り上げていけるのではないかと思います。 本記事で興味を持ち、Fashion Tech meetup #3が開催される際に足を運んでくださる方が増えれば幸いです。 おわりに 弊社の資料を通して興味を持たれた方、是非一度オフィスまでお越しください。 一緒にFashionxTechnology盛り上げていきましょう! https://www.wantedly.com/projects/45354 www.wantedly.com https://www.wantedly.com/projects/42989 www.wantedly.com
こんにちは。デザイナーの権です。 先日、何かと話題のAdobeのプロトタイピングツール 「Adobe XD(Adobe Experience Design)」 のプレビュー版がリリースされました。 早速いろいろと触ってみたところ、プレビュー版ということもありまだシンプルな機能のみという感じでしたが、さすがAdobeだけあっての直感的なUIで、初めて使うツールでありながら扱いやすさは抜群に良いと感じています。 今回は、まだ1~2日使っての感想ではありますが、実際に自分たちのアプリ開発の現場に取り入れられそうかを前提に「Adobe XD」を使っての操作感や印象をご紹介します。 ※使い方などは、Adobeの こちら のブログでの詳しい解説を参考にしていただければと思います。 全体的な操作感 類似の機能を兼ね備えていることから、 Sketch との比較を余儀なくしてしまいますが、レイヤー管理ができない、グラデの扱いがない、縦に長いページのプロトタイプが作成できないなどできないことがまだまだ多く、現段階だとSketch+ InVision の組み合わせの方が勝っている印象です。( Craft のPrototype機能がリリースされると尚更敵わなそう。) しかし、GUI画面はシンプル&直感的で扱いやすく、ショートカットもAdobeの他ソフトとほぼ共通化されてるので、操作しやすく軽いです。 そして、「UIデザインする上でプロト作りはデザインの一環」と言わんばかりに「Design」と「Prototype」がタブ切り替えで反映できるため、スイッチングコスト無しに確認できるフローは素晴らしい体験だと思いました。 また、個人的に特にいいと思ったのは、数あるプロトタイプツールに比べてダントツでわかりやすさを感じる理由として考えられる、チュートリアルのクオリティの高さ。 デモ動画でももちろんわかりやすく紹介されてましたが、起動画面にはサンプルファイルが用意されていて、ステップに沿って操作することにより体験しながら覚えていけます。こういったしっかりしたチュートリアルが用意されていることがAdobeツールの扱いやすさにつながっているのだと感じます。 他Adobeソフトとの互換性 デザインツールやプロトタイピングツールといえば、Sketchや Prott など今では様々なツールが世に溢れていますが、なんだかんだでAdobeソフトが一番扱いやすいし手に馴染むといったデザイナーの方は多いのではないのでしょうか。 かくいう私もその一人で、個人制作ではSketchも使用していますが、様々な業務が交錯する社内での制作ではAdobeソフトをメインで使用しているため、「Adobe XD」に関してはPhotoshopやIllustratorとの互換性に優れていて使いやすいのでは?と熱く期待していました。 新規から作成するユーザーに向けてを想定しているとは思いますが、それぞれのソフトでデータをインポートしてみて、どの程度連携できそうかを以下にまとめてみました。 Photoshop⇄XD PSDデータをインポートすることはできない Photoshopからは画像のインポートのみできる スマートフィルターで再編集などもできない テキスト情報をコピーすると、テキストはペーストされるがスタイルは解除される Illustrator⇄XD Photoshopと同じく、Aiデータをインポートすることはできない 図形などのベクターオブジェクト、テキスト情報はインポートできる。しかし、テキストを編集しようとするとブツ切りになり、特に日本語の文字組みは崩れてしまう 画像、シンボルデータ、シャドウグローなどの効果はインポートできない 今まで作成したデータをインポートできてプロト制作まで完了できたら、さぞ便利だろうと思っていましたが、 正直なところ期待には及ばずといったところで、上記のような現象が起こってしまいました。 現場で使うにはまだちょっと難しいかなといった印象ですが、これからのアップデートに期待していきたいです。 「Adobe XD」に今後期待するもの 上記であげたような現象もありましたが、「Adobe XD」を触ってみて最大の魅力と感じたのは、「Design」と「Prototype」がワンクリックでタブ移動できてシームレスに制作できる部分ではないかと思います。 デザインに限らずどんな事象でも、「オブジェクト間の距離や時間」は体験の向上に関与するものであり、近くなればなるほど新しい体験と感動が提供できるはずなので、「Adobe XD」のように作業間の距離や時間を縮めることは、開発効率のアップに確実につながると思います。 そして、Adobeユーザーとしては、さらに詳細な作り込みを経ての制作となればPhotoshopやIllustratorなどが必要となるため、他Adobeソフトとの互換性が高くなれば、さらにストレスなく制作が進められるので、ツール名である「Experience Design」のごとく、体験をデザインするにふさわしいソフトとして将来欠かせないツールになるのではと心から期待しています。 さいごに VASILYでは、デザイナーも様々なツールを試して、常に試行錯誤しながら、最適な制作&開発方法を模索しております。 そんな環境にご興味のある方は、以下ページからまずは話だけでも聞きに来てみてください。 学生、インターンも大歓迎です。素敵なポートフォリオとともに遊びに来てください。 お待ちしております。 https://www.wantedly.com/projects/41710 www.wantedly.com https://www.wantedly.com/projects/42672 www.wantedly.com
この記事は2016年3月15日時点の情報です。 最新の仕様とは異なるかもしれないので、ご注意ください。 概要 Facebookのモバイルページ向けネイティブアドがオープンβで提供を開始していたようなので、 埋め込んでみました。 基本的にドキュメントが英語しかなかったので、この記事では、Facebookが提供しているドキュメントの意訳と実際に試してみて得られた知見を合わせて書きます。 Audience Network for Mobile Web - Open Beta 埋め込むまでの手順 Facebook developer登録(登録済みならば省略可) Facebookアプリ登録(登録済みならば省略可) placement発行 コードの埋め込み Facebook developer登録 Web上に多くのドキュメントがあるので、詳しい手順は省略します。 Facebookアプリ登録 こちらも、Web上に多くのドキュメントがあるので、詳しい手順は省略します。 placement発行 Facebook developerページにアクセスし、左メニューのAudience Networkを選択します。 ページ内のPlacementsタブ内の「Create Ad Placement」ボタンを選択します。 表示されたモーダル内の項目を埋めます。 「名前」は自分がわかりやすいものを入力しましょう。 「広告を表示するステップ」はどのような手順をとった際に、広告が表示されるかを記述します。 場合によっては、この情報を元に、facebookの広告チームが提案を行ってくれるそうです。 「表示フォーマット」は、「ネイティブ」を選択します(他の形式として「バナー」がありますが、この記事では省略します)。 ステータスはオンを選択します(オフは配信を行わず統計データを見るだけの場合に使います)。 最適化はとりあえず、フィルレート最適化を選んだほうが良さそうです。 最後に保存するを選択して、placementを発行しましょう。 コードの埋め込み Placementsタブ下のAcive placementsに先ほど発行したplacementが追加されていると思いますので、 そこにある「Get HTML」のボタンを押します。 基本は、ダイレクトを選択します(DFPを利用する場合は、DFPを選択してください)。 セレクトボックスのほうは、ネイティブを選択します。 表示されたHTMLコードをコピーし、自分のサイトの表示させたい箇所に貼り付けます。 以下のようなコードが表示されます。 < style > #ad_root { display : none ; font-size : 14px ; height : 250px ; line-height : 16px ; position : relative ; width : 300px ; } .thirdPartyMediaClass { height : 157px ; width : 300px ; } .thirdPartyTitleClass { font-weight : 600 ; font-size : 16px ; margin : 8px 0 4px 0 ; overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ; } .thirdPartyBodyClass { display : -webkit- box; height : 32px ; -webkit- line-clamp: 2 ; overflow : hidden ; } .thirdPartyCallToActionClass { color : #326891 ; font-family : sans-serif ; font-weight : 600 ; margin-top : 8px ; } </ style > < script > window .fbAsyncInit = function () { FB. Event .subscribe ( 'ad.loaded' , function ( placementId ) { console.log ( 'Audience Network ad loaded' ) ; document .getElementById ( 'ad_root' ) .style.display = 'block' ; } ) ; FB. Event .subscribe ( 'ad.error' , function ( errorCode, errorMessage, placementId ) { console.log ( 'Audience Network error (' + errorCode + ') ' + errorMessage ) ; } ) ; } ; ( function ( d, s, id ) { var js, fjs = d.getElementsByTagName ( s ) [ 0 ] ; if ( d.getElementById ( id )) return ; js = d.createElement ( s ) ; js.id = id; js.src = "//connect.facebook.net/en_US/sdk/xfbml.ad.js#xfbml=1&version=v2.5&appId=999999999999999" ; fjs.parentNode.insertBefore ( js, fjs ) ; } ( document , 'script' , 'facebook-jssdk' )) ; </ script > < fb :ad placementid= "111111111111111_1111111111111111" format= "native" nativeadid= "ad_root" testmode= "false" ></ fb :ad> < div id = "ad_root" > < a class = "fbAdLink" > < div class = "fbAdMedia thirdPartyMediaClass" ></ div > < div class = "fbAdTitle thirdPartyTitleClass" ></ div > < div class = "fbAdBody thirdPartyBodyClass" ></ div > < div class = "fbAdCallToAction thirdPartyCallToActionClass" ></ div > </ a > </ div > これでとりあえず表示はできたかと思います。 もし、表示できない場合には、Facebook developerページで広告を設置したサイトのドメインを許可しているかどうかを確認してください。 また、開発環境などで確認をしたい場合には、fb:adタグのtestmode属性の値を true にし、 Facebook developerページの設定で、Developer以上の権限が付与されたアカウントでFacebookにログイン した状態でアクセスしてみてください。 testmodeがtrueの場合には、テストデータしか配信されず、クリックなどの挙動も確認できないため、注意が必要です。 Getting Started with the Audience Network 一つのページに複数のADを埋め込む方法 今回は、コンテンツ中とコンテンツ下の2箇所にアドを設置しましたが、 2箇所に設置する場合にどうするかのドキュメントがなかったので、以下のように実装しました。 placementを表示するADの数だけ発行する コールバックをplacementIDに応じて処理する サンプルコード < script > window .fbAsyncInit = function () { FB. Event .subscribe ( 'ad.loaded' , function ( placementId ) { if ( placementId == '111111111111111_1111111111111111' ) { document .getElementById ( 'ad_root1' ) .style.display = 'block' ; } if ( placementId == '222222222222222_2222222222222222' ) { document .getElementById ( 'ad_root2' ) .style.display = 'block' ; } } ) ; FB. Event .subscribe ( 'ad.error' , function ( errorCode, errorMessage, placementId ) { if ( placementId == '111111111111111_1111111111111111' ) { document .getElementById ( 'ad_root1' ) .classList.add ( 'error' ) ; } if ( placementId == '222222222222222_2222222222222222' ) { document .getElementById ( 'ad_root2' ) .classList.add ( 'error' ) ; } } ) ; } ; ( function ( d, s, id ) { var js, fjs = d.getElementsByTagName ( s ) [ 0 ] ; if ( d.getElementById ( id )) return ; js = d.createElement ( s ) ; js.id = id; js.src = "//connect.facebook.net/en_US/sdk/xfbml.ad.js#xfbml=1&version=v2.5&appId=999999999999999" ; fjs.parentNode.insertBefore ( js, fjs ) ; } ( document , 'script' , 'facebook-jssdk' )) ; </ script > ネイティブアドのデザイン・コーディング 実際に表示してみるとわかると思いますが、サンプルそのままではレイアウトや文字色などサイトに合っていません。 ここからは、ネイティブアドらしく、各サイトに合った形で広告を埋め込むための情報を説明していきます。 Native Ads in Mobile Web ネイティブアドの構成要素 広告の配信ネットワークから広告が配信されてくる際に、 自分のコンテンツに合うように表示する要素を取捨選択することができます。 以下に利用可能な要素と簡単な説明を載せておきます。 要素名 クラス名 説明 省略してもよいか Icon Image fbAdIcon 正方形(128x128)のアイコン。広告主のアイコン(企業ロゴ)などが表示されます 省略可 Ad Image fbAdMedia 長方形(600x314と書かれていますが、実際は796x415などのサイズ)の広告のメインビジュアル 省略可 Title fbAdTitle 広告のタイトル 省略不可 Subtitle fbAdSubtitle 広告のサブタイトル 省略可 Body fbAdBody 広告の本文 省略可 Call-to-action fbAdCallToAction いわゆるCTA。「ゲームであそぶ」「詳しくはこちら」などのクリックを誘う文言が入ります 省略不可 Ad Link fbAdLink 広告へのリンク。広告のクリック領域を決めるに使います 省略可 Social Context fbAdSocialContext アプリの評価や追加情報(200万ダウンロード)などを表示できるらしいです(今回は未使用) 省略可 Native Mobile Web Components デザインガイドライン コンテンツに合うようにデザインを調整できるネイティブアドですが、 facebookが規定する最低限のガイドラインを守る必要があります。 Design Guidelines for Audience Network Ads 最低限表示する必要のある要素に関するガイドライン 次の3つの要素を表示する必要があります。 表示しなかった場合には、Facebook側から広告の配信が停止されてしまいます。 広告のタイトル CTA AdChoicesと広告だと見分けがつく要素(例:SponseredやAdといった表記) また、それぞれの要素は表示の上でいくつか規定があります。 広告のタイトル 広告のタイトルは少なくとも20文字までは表示する必要があります。 タイトルが20文字より多い場合には、ellipsisなどを使って省略することが可能です。 CTA CTAの文言は省略してはいけませんが、最大でも25文字なので、とても長くなることはありません。 また、配信される広告を見たところ、10文字以内が多そうです(25文字の場合を考慮したデザインにしたほうがよいと思います)。 CTAはテキストのまま表示してもよいですが、ボタンのようにデザインすることも許されています。 自分のコンテンツとの親和性やクリック率を考えて、使い分けるとよいと思います。 AdChoicesと広告だと見分けがつく要素 まず、広告とコンテンツの台(背景やカードを指す?)とを明確に線引きし、 誤ってクリックすることを避けるためにコンテンツとの間にスペース空ける必要があります。 この辺の感覚は、 ガイドライン に載っている例を参照したほうがよいかと思います。 ユーザが表示される広告について選ぶためにAdChoiceは必須になっています。 AdChoiceについては次の3つの方法の内どれか 一つ を行う必要があります。 (元々、アプリ向けのガイドラインなので、正しいかどうかは怪しいです) Audience Network SDKによって提供されるAdChoiceの制御機構を用いて、クリック可能な「AdChices」というテキストを表示する コンテンツと区別するために明確に「Sponsored」「Promoted」「Ad」といった文言を表示し、かつ、広げて表示可能なAdChoices iconを表示する もし、1.と2.のどちらもできない場合には、「Sponsored」「Promoted」「Ad」といった文言を表示し、app storeページとプライバシーポリシーで閲覧可能なリンクとして https://m.facebook.com/ads/ad_choices へのリンクを設置する 実際に、モバイルページに広告を読み込むと自動でAdChoices icon(緑色の三角のようなアイコン)が読み込まれます。 それをタップすると広がって、AdChoicesのテキストが表示されますので、 モバイルページのネイティブアドでは、これを用いて2.の要件を満たすのがよさそうです。 (AdChoice iconの動作については、ドキュメントに書かれていない仕様なので、突然変わるかもしれません) その他の構成要素に関するガイドライン メインビジュアル メインビジュアルは拡大縮小や切り抜きを行ってもよいですが、描き変えたり歪めてはいけません。 最小限の文字と画像はメインビジュアルの下部20-25%の上に重ねて表示できますが、 メインビジュアルの上に表示されるテキストは読みやすくする必要があります。 本文 本文の表示は必須ではありませんが、表示する場合には少なくとも72文字は表示する必要があります。 72文字表示できない場合は、非表示にするか、アニメーションで全体を表示するほうがよいです。 実際に導入したデザイン 今回は次のようなデザインを採用しました。 実際の広告画面を表示するのは問題がありそうなので、あくまでイメージですが、参考にしてみてください。 まとめ 今回はFacebookのネイティブアドをとりえあず導入してみました。 導入したばかりなので、収益という観点でのパフォーマンスはまだわかりませんが、 融通の効くデザインガイドラインである点やドキュメントがしっかりしている点で好印象でした。 モバイルページ版はまだオープンβということもあり、情報が部分的ではあるので、 間違っている箇所もあるかもしれませんが、参考になれば幸いです。 VASILYでは、新しい技術に積極的に取り組めるようなメンバーを募集しています。 興味のある方は こちら からご応募ください。
こんにちは、VASILYのバックエンドエンジニアの塩崎です。 iQONの中ではクローラーと検索サーバーを担当しています。 iQONのクローラーには提携ECサイトさんからクロールした商品を商品カテゴリー(Tシャツ、ワンピース、etc.)に自動的に分類する機能があり、商品タイトルや商品説明文などのテキスト情報を元に分類を行っています。 しかし、一部のカテゴリー(セーター・ニット帽)の商品はテキスト情報だけからでは精度の良い分類を行うことができません。 そのため、これらのカテゴリーの商品については画像を用いたカテゴリー分類を導入しました。 これらの機能を実現するために、当社のデータサイエンスチームとも協力を行い、ディープラーニングを用いたカテゴリー判定器を開発しました。 また、この機能は既存のクローラーの機能からの独立性が高いので、クローラーに組み込むときにはマイクロサービス化をして組み込みました。 その結果、カテゴリ判定の精度として、セーターカテゴリーでは99.7%、ニット帽カテゴリーでは99.8%という結果を得ることができました。 背景 iQONのクローラーは提携ECサイトさんから毎日数百万商品のクロールを行っており、毎日数千〜数万商品がiQONに新たに追加されます。 これらの数の商品のカテゴリー分類を人間が行うことは大変な労力を要するため、iQONのクローラーには商品カテゴリーの分類を自動的に行う機能があります。 商品タイトルや商品説明文といったテキスト情報に対して形態素解析を行い、文脈情報を考慮した上で候補となる単語を1つに絞り込み、その単語に対して辞書マッチを行うことでカテゴリー分類を行います。 その結果、精度98%での商品カテゴリー分類を実現しました。 しかし、セーター(iQONでのカテゴリ分類ではトップス・ニットと表記されるが、紛らわしいのでこの記事ではセーターと表記)とニット帽の判定をテキストだけから精度良く行うことは困難です。 これらの商品のタイトル、説明文には「ニット」という表記しかされていない場合が多いからです。 なぜこのような曖昧な表記になってしまっているかというと、人間が商品情報を見るときには画像とテキストを同時に見て判断をするために、このような表記でも通常のECサイトには十分なためだと思います。 この問題を解決するためには、商品画像を用いたカテゴリー判定が必要になります。 画像からカテゴリー判定する手法の検討 近年、画像判定の分野では畳み込みニューラルネットワーク(CNN)が注目されていますが、この問題に対してもCNNが有効であるかを確認するために、以下に列挙された3つの手法との比較を行いました。 多層パーセプトロン (MLP) HOG特徴量 + k近傍法 (HOG + kNN) HOG特徴量 + サポートベクターマシン (HOG + SVM) これら合計4つの手法を用いて、セーターとニット帽の分類を行った結果を以下の表に示します。 なお、この検証では速度を優先してRGB画像をグレースケール画像に変換して分析を行いました。 手法 accuracy (%) CNN 97.84 MLP 93.08 HOG + kNN 96.74 HOG + SVM 97.47 この結果からCNNでも他の手法に比肩する精度が出ることがわかりました。 この後、約100のカテゴリー分類するための分類器を作る必要があることや、商品に対して複数のタグ(トップス + ニット)を付与する必要があることなどを考慮し、CNNを採用しました。 ディープラーニングを用いた商品カテゴリーの判定 画像からのカテゴリー判定を行うために、以下のようなCNNのモデルをChainerを用いて作成しました。 import chainer.functions as F import chainer.links as L from chainer import Variable, FunctionSet model = FunctionSet( conv1 = F.Convolution2D( 3 , 64 , 4 , stride = 2 , pad = 1 , wscale = 0.02 *math.sqrt( 4 * 4 * 3 )), conv2 = F.Convolution2D( 64 , 128 , 4 , stride = 2 , pad = 1 , wscale = 0.02 *math.sqrt( 4 * 4 * 64 )), conv3 = F.Convolution2D( 128 , 256 , 4 , stride = 2 , pad = 1 , wscale = 0.02 *math.sqrt( 4 * 4 * 128 )), conv4 = F.Convolution2D( 256 , 512 , 4 , stride = 2 , pad = 1 , wscale = 0.02 *math.sqrt( 4 * 4 * 256 )), fl = L.Linear( 6 * 6 * 512 , 2 , wscale = 0.02 *math.sqrt( 6 * 6 * 512 )), bn1 = F.BatchNormalization( 64 ), bn2 = F.BatchNormalization( 128 ), bn3 = F.BatchNormalization( 256 ), bn4 = F.BatchNormalization( 512 )) def forward (x_data, train = True ): x = Variable(x_data, volatile = not train) h = F.relu(model.bn1(model.conv1(x))) h = F.relu(model.bn2(model.conv2(h))) h = F.relu(model.bn3(model.conv3(h))) h = F.relu(model.bn4(model.conv4(h))) y = model.fl(h) return F.softmax(y) CNNの層数は4層で全ての層が畳み込み層です。 出力層にはソフトマックス関数を用いているために、各カテゴリに属している確率が出力されます。 入力は96 x 96 pixelのカラー画像(RGB)です。 ECサイトから取得する画像はこのサイズではないことがほとんどですので、CNNに入力する前にリサイズ処理を行います。 アスペクト比1:1ではない画像については、正方形のエリアに貼り付けを行います。 プーリング層も組み合わせたモデルも作成し検証を行いましたが、全てが畳み込み層であるようなモデルの精度が最も高かったたために、このような構成にしています。 ECサイトから取得する画像の中にあるアイテムの位置は大体固定されているため、このような結果になったのではないのかと思います。 学習にはセーター・ニット帽の各カテゴリーの画像を1.5万枚ずつ使用しました。 テキストからのカテゴリー判定の結果を補助的に用いながら、すべての画像を目視でチェックしデータクレンジングを行いました。 また、学習の際にはGoogleが2015年に発表したBatch Normalization( http://arxiv.org/abs/1502.03167 )という手法も用いて、学習の高速化を図っています。 マイクロサービス化した理由 この機能をiQONのクローラーに組み込む際にはマイクロサービスとして組み込みを行いました。 使用している言語・ライブラリなどの違いにより同一ホスト上で稼働させるよりも、独立した機能として切り出した方がクローラーシステム全体の構成がシンプルになると判断したためです。 また、画像からカテゴリーを判定するという機能それそのものの独立性が高いことも理由の1つです。 現在クローラーはCentOS 6.5 + rubyで稼働しているのに対して、画像判定処理はUbuntu 14.04 + pythonで稼働しています。 画像判定サーバーの構成 画像判定用のサーバーの構成は以下のようにしました。 リバースプロキシ: nginx( http://nginx.org/ ) アプリケーションサーバー: uWSGI( https://uwsgi-docs.readthedocs.org/en/latest/# ) webアプリケーションフレームワーク: flask( http://flask.pocoo.org/ ) 現在の負荷状況ではインスタンスタイプc4.largeが一台あれば十分です。 負荷が増えた時にはサーバーのスケールアップ・スケールアウトも検討に入れる必要があります。 VASILYではデプロイツールとしてcapistranoを使用していますが、今回はPythonで製作をしたのでデプロイツールもPython製のfabric( http://www.fabfile.org/ )を使用しました。 通常のWEBアプリケーションのデプロイに要求されるようなアプリケーションコードの変更をするデプロイの他に、モデルのパラメーターを変更した時にモデルファイルをAmazon S3から取得し更新を行うデプロイ方法もサポートしています。 Teslaを搭載した学習用サーバーで生成したモデルファイルをAmazon S3にアップロードした後に、そのファイルをAmazon S3から取得し、サーバーの再起動を行います。 このサーバーに対して以下のようにリクエストを投げることで、画像判定の結果を得ることができます。 サーバーに対するリクエスト・レスポンスのフォーマットはGoogle Cloud Vision APIを参考にしました。 $ curl -X POST -H "Content-Type: application/json" -d '\ { \ "requests":[ \ { \ "features":[ "CATEGORY_DETECTION"], \ "image":{ \ "uri":"http://www.example.com/image.jpg" \ } \ } \ ] \ }' \ "http://image-determination:8080/annotate" { "responses": [ { "categoryAnnotations": [ { "score": 80.0, "description": "sweater" }, { "score": 20.0, "description": "knit_caps" } ] } ] } ここで返されるscoreはそのカテゴリーに属する確率を%で表現したものです。 確率からカテゴリーへのマッピング カテゴリー判定器の出力は、各カテゴリに属している確率ですので、それを元にカテゴリーにマッピングする必要があります。 今回は以下のような関数を用いて各カテゴリーにマッピングを行いました。 def map_to_category : threshold = 90 if score_sweater > threshold return SWEATER elif score_knit_caps > threshold return KNIT_CAP else return UNKNOWN 単純にscoreの高い方のみを採用するということはせずに、scoreの高い方でありなおかつ score > threshold を満たすものを判定結果としています。 これは、カテゴリ判定ミスのFalse PositiveとFalse Negativeがユーザーに与える影響が大きく異なるための処理です。 簡単に言いますと、判定結果が曖昧な商品を間違ったカテゴリーに表示するよりも、その商品を出さない方がマシという考えです。 結果 このカテゴリー判定器を用いてセーターとニット帽の画像1万枚ずつを分類した結果を以下に示します。 予測 実測 セーター ニット帽 不明 セーター 9839 15 146 ニット帽 32 9636 332 これを元に、セーターカテゴリーの精度(precision)を求めると、9839 / (9839 + 32) = 99.7% という結果が得られました。 同様にニット帽カテゴリーの精度を求めると、99.8%という結果が得られました。 以下のiQONの画面からわかる通り、平置きの商品画像・人間が着用している商品の画像・マネキンが着用している画像など、様々な種類の画像を問題なく分類できています。 セーターの判定結果 ニット帽の判定結果 これからの展望 このカテゴリー判定器をさらに発展させたものとして、iQONで使用している約100のカテゴリに分類を行う判定器も開発しています。 それを実現するためには画像情報だけではなく、テキスト情報やブランド情報などの画像以外の情報も統合的に用いて判定を行います。 VASILYでは、最新の研究論文にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 https://www.wantedly.com/projects/42989
概要 畳み込みニューラルネットワークによる画像生成モデル(DCGAN)に弊社のワンピース画像10万枚を学習させました。 得られた生成モデルを使って、乱数で作った100次元ベクトルからワンピース画像を生成しました。 逆に、一枚のワンピース画像を100次元ベクトルに圧縮し、可視化しました。 可視化したことで、モデルがワンピースの【色】【形】【柄】【モデルやマネキンの有無】など、基本的な特徴を捉えられていることがわかります。 この技術は、自動タグ付けや類似画像検索に応用することができます。 はじめに  はじめまして。データサイエンスチームの後藤と申します。現在、アイテム画像のカテゴリ判定モデルを作ったり、各部門のKPIの日々の変動やシステムの異常を知らせるダッシュボードを作る仕事をしています。  また、最新の研究論文にもアンテナを張り、提案手法の検証にも取り組んでいます。今回は、去年の暮れあたりから話題になっている、ディープラーニングによる画像生成モデル(DCGAN)を弊社の所有するワンピースの画像に適用して、その利用価値を検討してみたいと思います。ちなみに、私はディープラーニングに入門したばかりで、今回の記事には間違いが含まれている可能性があります。その点を注意してお読み頂けば幸いです。 使用するモデル  今回用いるモデルは、DCGAN( http://arxiv.org/abs/1511.06434 )と呼ばれるものです。Deep Convolutional Generative Adversarial Networksの略で、名の通り、畳み込みニューラルネットワークによる生成モデルの一種です。大雑把に言えば、学習した特徴量を組み合わせて本物らしい画像を生成するGeneratorと、本物の画像と生成された画像を見破るDiscriminatorの二つのニューラルネットワークが互いに切磋琢磨して学習を進めていくというモデルになっています。論文中では「人の顔」や「ベッドルーム」の画像を学習し、見事に生成できています。この研究をうけて、アルゴリズムをchainerで実装し、アニメキャラなどのイラストを描かせた記事もあります。 Chainerで顔イラストの自動生成 Chainerを使ってコンピュータにイラストを描かせる 前者はアルゴリズムの定性的な解説が、後者は歴史的経緯が詳しいです。  調べた範囲では、ファッションアイテムの画像に適応した例はなかったので、アルゴリズムの適用範囲を探るという意味でやる価値はあると思いますし、特徴量を自動で抽出するというディープラーニングの特性を使ってこれまでにないサービスを作れるんじゃないか、という期待もしながら分析を進めます。  実装は、以下の二つを参考にしています。ネットワークの構成は、論文と同じものを採用します。学習を安定させるBatch normalizationを組み込んだことが肝のようです。 https://github.com/mattya/chainer-DCGAN https://github.com/Hi-king/chainer-DCGAN 学習データ   今回は、弊社のサービスで使われているワンピースの画像、約10万枚を学習に用います。前処理として、画像を96×96ピクセルのRGBに変換しておきます。数あるカテゴリの中からワンピースを選んだのは、個人的な趣味です。 ワンピースの生成  学習をある程度進めると、100個の乱数で与えることで、以下の様なワンピースを作ることができました。特徴を組み合わせて新しく生み出したものなので、この世にないワンピース、と言うことができます。生成モデル特有の不気味さがありますが、ご興味のある方は是非拡大して見てみてください。  遠目でみると自然な画像が生成されています。一枚一枚をじっくりみると、アイテムが大きく写っている画像は違和感なく綺麗に生成できていることがわかります。一方で、モデルの顔や腕の位置などバリエーションが多いパーツが含まれている場合は、若干不自然で、本物の画像でないことがすぐにわかります。まれに、マネキンの体に、首元がハンガーになっているようなものもあり、このモデルは現実を理解してワンピースを生成しているわけではない、ということも伺えます。  全体的に見て、それなりに自然で、個々の要素の特徴をある程度、捉えられていることがわかりました。ここから様々なことができるようになります。  例えば、さらなる検証が必要ですが、100次元ベクトルと得られた画像の対応関係を調べることで、モデル特有のベクトルを抽出したりできそうです。モデル着用のワンピースの画像から、モデルだけを取り除く演算をして、服だけの画像を生成することも可能になるのでは、と想像しています。 画像のベクトル化でアイテムを整理する  今度は逆に、一枚のワンピース画像を100次元ベクトルに変換して情報を圧縮します。100次元でもまだ、人が理解するには難しいので、多少の正確さを犠牲にして、2次元にマッピングします。ここでは、t-SNEというアルゴリズムを利用しています。  上図では、2000点程度のアイテムをマッピングしてみました。画像の分布を見てみると、領域ごとになんらかの特徴を持つことがわかります。具体的に4箇所拡大してみました。左から【形】、【柄】、【色】、【モデル】という特徴を持ったアイテムが集まっています。  今回のモデルは、画像の特徴を上手く100次元ベクトルで表現できているようです。このように画像データの特徴を数値で表現できると、サービスに組み込みやすくなります。例えば、どの数値が【柄】と対応しているのかを把握しておくことで、花柄やボーダー柄といったタグを自動で付与することが可能になります。また、手元にある写真の類似画像を検索する、といったタスクも少ない計算でできるようになりそうです。ほかにも様々な切り口でアイテムを捉えることできそうなので、アイテム検索がより便利な機能になるかもしれません。今回得られた知見をより深めて、サービスをより使いやすいものにしていきたいと思います。 最後に VASILYでは、技術的チャレンジをしながらiQONを開発していただける仲間を募集しています。ご興味のあるかたは こちら からご応募よろしくお願い致します。
こんにちは。iOSエンジニアの庄司です。 普段のアプリ開発において、バックエンドチームから「○○のページで△△のデータ取得するためにリクエストしているURLってどんなの?」と聞かれることがよくあります。 その都度、APIリクエストとリクエスト結果をprintで表示するフラグをONにしてアプリをビルドするということをしていたため、かなり手間がかかっていました。 こういった作業を楽にするためにネットワークデバッグライブラリをいくつか比較してみました。 ライブラリ ResponseDetective https://github.com/netguru/ResponseDetective NSURLSessionのリクエストやレスポンスをデバッガのログに流してくれるライブラリです。 特徴・利点 フルSwiftで書かれており、NSURLProtocolのメソッドに割り込んでログを落とす事ができます。 また、HTML, JSON, imageなどのレスポンスのタイプによって処理を分けることができます。 欠点 通信のデリゲートを受け取ることはできますが、デフォルトでは何もログを落とさないため、printも自分で書きます。 その結果、そこそこのコード量になってしまいますし、printを書くということは、ログを見るためにその都度ビルドが必要になってしまいます。 Wireshark https://www.wireshark.org/ Windows, OSXどちらでも使えるパケットキャプチャツールです。 特徴・利点 Remote Virtual Interface(RVI)を使って、UUIDごとの端末の通信をキャプチャできます。 あくまでパケットキャプチャツールなので、アプリにコーディングは不要。 Androidにも使えますし、他社製アプリの通信もキャプチャすることができます。 欠点 httpsの通信はドメインを見ることはできますが、エントリーポイントやレスポンスは暗号化されたままで、見ることはできません。 自分のアプリであれば、httpsの通信も 見ることができる ようです。 Charles https://www.charlesproxy.com/ OSX上で動くHTTPプロキシツールです。 多くのiOSエンジニアが使っているのではないでしょうか。 インストール手順はこちらの Qiitaの記事 が参考になります。 特徴・利点 Wiresharkと同様、アプリに対する実装は不要です。 通信を内容を見るだけではなく、ブレークポイントを仕掛けたり、レスポンスをローカルファイルの別の値に変更したりすることができるので、APIの異常系のテストにも使えます。 またWiresharkと違って、httpsの通信の中身を見ることができます。 欠点 30日は無料で使えますが、それ以降はライセンスの購入が必要になります。 1ライセンス最大$50ですが、豊富な機能を考えるとその価値があると思います。 PonyDebugger https://github.com/square/PonyDebugger square社製のネットワークデバッガです。 特徴・利点 iOSアプリ内部にライブラリを実装し、OSX側で立ち上げたサーバを介してGoogle ChromeのDeveloper Tool通信やView階層を見ることができます。 ChromeのDeveloper Toolを使い慣れている人にはとても親しみやすく見やすいです。 欠点 OSX側にpythonのサーバを立ち上げるのですが、これの環境構築に手間取りました。(OSX側のサーバインストールのエラーについてのissueが何種類もありました。今回参考になった issue ) 2013年から運用されているObjective-Cのライブラリで、もはや積極的にメンテナンスされていません。 netfox https://github.com/kasketis/netfox Swiftで書かれたの通信監視ライブラリ、通信結果をアプリ上で確認することができます。 GitHubのREADMEから 特徴・利点 セットアップは、AppDelegateに一行書くだけです。 アプリをシェイクするとリクエストしたURLリストがモーダル表示されます。 また、ジェスチャーが既存機能と衝突する場合は、カスタマイズすることもできます。 欠点 モーダル表示で確認するため、リアルタイムでURLを確認できません。 AlamofireやAPIKitなど、利用しているライブラリによっては ひと手間 かけないと通信ログを取れません。 mitmproxy 【2016/02/12 追記】 https://github.com/mitmproxy/mitmproxy OS X, Linux 上で動作する、CUIのHTTPプロキシです。Pythonでできています。 特徴・利点 Charles同様SSLのリクエストの中身を確認することができます。 リクエストのインターセプトや書き換えはもちろんのこと、Pythonスクリプトで動的にレスポンスを書き換えることができます。 ターミナルからキーボード操作で扱えるのも嬉しいですが、一番嬉しいのはここまで使えて無料であることです。 また、 公式ドキュメント も充実しています。 欠点 ちょっと触って見る限り、Wireshark, Charles でできることはだいたいできる上に無料なので、欠点は見当たりません。 まとめ ここまでのライブラリのメリット/デメリットを簡単にまとめました。 ライブラリ メリット デメリット ResponseDetective カスタマイズ性が高い アプリ実装多め Wireshark アプリ実装不要 https不可 Charles アプリ実装不要・設定が簡単・高機能 有料 PonyDebugger ChromeのDeveloper Toolが見やすい 古い・メンテ少ない netfox アプリ実装は一行だけ リアルタイムに通信を見れない mitmproxy アプリ実装不要・CUI・無料でSSLも確認できる なし 僕の場合は最終的に Charlesにお金を払うことに決めました。 mitmroxy を使うことに決めました。 冒頭であったように、「○○のページで△△のデータ取得するためにリクエストしているURLってどんなの?」となった場合でも、アプリをビルドすることなく、リクエストしているURLをすぐに確認することができます。 また、Xcodeとは別のツールであることで、APIリクエストとそのレスポンスがXcodeの狭いデバッグエリアを占領することがなくなり、ログがかなり見やすくなりました。 最後に VASILYでは、一緒にiQONのアプリを開発してくれる 仲間を募集 しています。少しでもご興味のある方は是非こちらからご応募よろしくお願いいたします。 参考URL Technical Q&A QA1176 - Getting a Packet Trace - Mac Developer Library 通信系のデバッグには Charles が便利 - Qiita
こんにちは。デザイナーの権です。 アプリ開発に関わるデザイナーの方々は、仕様とデザインが決まって実装に移る際、どのようにチームでやりとりしていますか? それぞれの開発体制や状況に合わせて進めていくので、会社によって様々だと思います。弊社でも効率的な開発のために、デザインデータや仕様書、指示書、プロトタイプの準備等、日々試行錯誤しながら最適な方法を探しています。 数ある開発手段の中で今回は、弊社が運営するサービス「 iQON 」の機能改善やデザインリニューアルに効率的に対応できるよう新たに作成した、デザインガイドラインについてご紹介したいと思います。 デザインガイドライン作成の背景 弊社では1年ほど前からデザイナーが5人に増えたことにより、プラットフォームごとに一人づつオーナーシップをとって制作を進める体制に変更されました。 私も以前は、アプリデザインや広告のデザインなどジャンル関係なく携わっていましたが、現在はiOSアプリのデザインをメインで制作しています。 しかし、体制の変化だけではまだ効率的な開発とは言えず、たとえば、UI改善する度にデザインの統一性が取れてない箇所があることで実装時にコミュニケーションコストが高くなってしまう問題などが浮き彫りになり、改めてアプリ全体でのデザインルールを定義することが必要不可欠になってきました。 ユーザビリティを考慮し、デザインの統一性が取れたクオリティの高いアプリを効率的に開発するためにも、デザインガイドラインを作成することになったのです。   デザインガイドライン作成のポイント いざ、「デザインガイドラインを作ろう」としても、正解がないため試行錯誤しながら見つけていくしかありません。 それぞれのアプリやプロジェクトごとに必要な項目など違うので、マッチしないものもあるかもしれませんが、弊社での作成ポイントをご紹介します。   -サービスやアプリのブランドを理解し、あるべき姿をデザインする ガイドラインは、これからアプリのビジュアルの基準になるものなので、どんな機能や画面にも適用できるように設計しなければなりません。 iQONはすでに成長しているサービスであり、アプリを利用している既存ユーザーもたくさんいるので、新しい機能や画面を制作する時もブランドイメージを壊さないようなデザインに仕上がるように、まずはホームやリスト、詳細などの主要画面からコンセプトデザインを作成し、あるべき姿を探ります。 そして、どのような場合でも適用できるような共通パーツを用意できるようにデザインし、ガイドライン作成に入ります。   -目的を設定し、セオリーから最適な方法を選択し作成する やみくもにモジュールを書き出して指定していくことはしないように、作成する目的を以下のように設定。 新しい画面もルールに沿って作成していくようなフローに変える   共通パーツやテキスト指定などをガイドラインから選んで指定する(または、エンジニアがガイドラインを基に実装できる)形にする。   常に更新&運用していき、チームで共有できる形にする 目的を設定することによって、本当にこのガイドラインが実用的なのかどうかを確認できたので、方向性がブレそうになっても立ち返って軌道修正することができます。 そして、全く同じ方法で展開できるものはないのですが、ガイドラインのセオリーや特性を理解するため、一般的にはどのように作られて活用されているのかを Google Material Design や 海外のUIスタイルガイドなど を見て参考にしました。 その中から自身のアプリ開発に最適だと思われる方法を選択して作成していきます。 iQONではどのようなものを作成したのか、一部ご紹介したいと思います。   1.オブジェクト別スタイル アプリ内で使用するカラー、フォント、テキストなど、オブジェクトのスタイルを一覧で表記。 乱立してしまいがちなテキストのスタイルも用途やルールを定義することで、 新しい画面作成の場合でも、用意された候補の中から適用することができます。   2.コンポーネント別スタイル 画面からコンポーネント別に抽出して、使われるテキストや画像のスタイルやマージンなどを表記。 コンポーネントで分解して表記しているので、細かい指定もまとめて確認できるようにしています。 例えば、グリッド表示のコンポーネントを、別の画面でも同じスタイルで実装して欲しい時などにも便利。   3.ステータス別スタイル ボタンのnormal時、hover時などのステータス別のスタイルを表記。 通常時やhover時など、アプリ内で基本このルールを共有しておけば、 毎回の指定が必要なくなります。   -メンバーに共有し、フィードバックをもらいながら改善する 弊社の場合だと、仕様や遷移の確認手段として InVision でプロトタイプを作成しているので、それとともにデザインガイドラインをエンジニアへお渡しして実装してもらいます。 実際に役立ててもらわないと意味がないので、はじめてガイドラインを導入してもらう時は利用の仕方などをお互いに確認しながらメリットを理解してもらい、問題や修正点があれば都度フィードバックをもらいながら修正していきました。 この際重要なのは、メンバーに「デザインガイドライン利用のメリットを実感してもらう」こと。 「同じようなモジュールを実装する時は、このガイドを参照できます」、「テキストの指定はこのリストの中から用途に当てはまるものを指定してください」などの便利な使い方を共有することで、スムーズに開発フローへの導入をすすめることができました。   -チーム内アンケートで効果測定する メンバーに「デザインガイドライン利用のメリットを実感してもらう」ために行ったことがもう一つ。 実際にデザインガイドラインを導入することで、そもそも問題を解決できているのかを測定したくても、プロジェクトによって開発期間や難易度は変わるので、定量的な数字を測ることはなかなか難しいのが現実です。 そこで、デザインガイドライン作成の目的にある、"ガイドラインは常に更新&運用"のためにも必ず行おうと思っていたのが、チーム内アンケートでした。 「デザインガイドラインを導入して開発効率が上がりましたか?」という質問に対して、メンバー全員から「上がった」との回答をもらい、以下のような具体的な効果も答えていただけました。 fromエンジニア " デザイナーに実装途中の画面を見せた時に、見てもらいたいところと関係ない箇所で「○○の色が違います」とか指摘されることが減った。 " " デザインがまだできていなくても、おおまかにガイドを見て実装ができるのでいい。 " fromデザイナー " 基準にすべきデザインデータが明確なので、都度デザインデータの場所を確認する必要がなくなった。 " " ルールが決まったことで微調整がなくなった。(作成時間を比較すると、3h〜5hくらい早くなった。) " 上記回答は、定性的ではありますがメリットとして感じるには十分な結果になったのではと思います。 個人的にも、今まで曖昧だった部分が明確になり、ルールを元にデザインができるので、「悩む時間」を「作業時間」へと変換できていたことが、効率化を実感できた瞬間でした。   改善点 効率化を成し遂げられた部分はあるものの、改善すべき点はまだたくさんあります。 アンケートではメンバーから改善点や意見も書いてもらい、その中でも多かったのが、「更新と管理」の問題でした。 ガイドライン自体の作成にはやはり時間はかかる 今の仕様のままだと少しずつボリュームが増えていくので、肥大化してしまう 人力が必要な場面が多いので、ヒューマンエラーが起きかねない GitHubで変更履歴も追えるようにするといいのでは このような問題はこれからも出てくると考えていますので、さらに改善してデザインガイドラインを日々アップデートしていく次第です。   まとめ 今回は、「効率的なアプリ開発のためにデザインガイドラインを作成した話」をご紹介しましたが、いかがでしたでしょうか。 デザインガイドライン自体は、ユーザーの目に直接触れることはありませんが、最高のユーザー体験を提供していくためには効率のいい開発環境があってこそ実現するので、デザインガイドラインの作成はデザイナーにとって大事なミッションでした。 はじめは手間がかかる作業ではありますが、チームメンバー達に、わかりやすく効率的に仕様を伝えるための手段の一つになると思います。 皆さんも、効率的なアプリ開発のために独自のデザインガイドラインを作成していきませんか。 今回の記事を少しでも参考にしていただき、さらに良い方法やご意見など、是非教えていただければ幸いです。   さいごに 「iQON」は、Webサービス開始から約5年半、アプリリリースからは3年半以上が経とうとしています。 もっと使いやすいサービスになれるよう、機能改善/追加やリニューアルしてみたいというデザイナーの方々、ただいま絶賛募集中です。 ご興味のある方は、以下ページからまずは話だけでも聞きに来てみてください。お待ちしております。 https://www.wantedly.com/projects/41710 www.wantedly.com https://www.wantedly.com/projects/42672 www.wantedly.com  
RecyclerViewが発表されて1年半ほど経ちましたが、みなさんRecyclerViewは活用していますか? これまではListView・GridViewを頑張って使っていたiQONも、直近のリリースから少しずつRecyclerViewに置き換えはじめました。 RecyclerViewはListView・GridViewよりも柔軟になり拡張しやすくなった代わりに、必要なものは自分で実装しないといけなくなりました。 そのため、ListView・GridViewにはあったけどRecyclerViewではなくなった機能が存在します。 今回はRecyclerViewの GridLayoutManager を使う際、データロード中フッターにProgressBarを出す方法を紹介したいと思います。 LinearLayoutManagerに関しては今回触れませんが 『ProgressBarを表示する』 の項を参考にしてもらえれば実装できると思います。 サンプルコード 今回の内容のサンプルコードはこちらになります。 https://github.com/nissiy/GridLayoutSample 参照していただけると理解が深まると思います。 興味がある方はビルドもしてみてください。 実装 ProgressBarを表示する RecyclerViewには ListView#addFooterView のような仕組みがないためフッターを自分で実装しないといけません。 フッターを作成してそこにProgressBarを表示させるには、以下のことを行う必要があります。 データロード前と後でデータセットに細工をする データセットの中身を見て RecyclerView.Adapter#getItemViewType の返す値を変える データロード前と後でデータセットに細工をする 以下のように、通信処理の前後でデータセットにStubをセットしたり、取り除いたりします。 // MainActivity.java private void loadData( final int page) { // ProgressBarを表示させるためにStubをセット if (page > 1 ) { adapter.add( new ProgressStub()); } // postDelayedして通信処理を仮想しています handler.postDelayed( new Runnable() { @Override public void run() { // 通信処理が終わったのでセットしたStubを取り除く if (page > 1 ) { adapter.remove(adapter.getItemCount() - 1 ); } // 通信して取得したデータを処理 ... } }, 2000 ); } データセットの中身を見て RecyclerView.Adapter#getItemViewType の返す値を変える RecyclerView.Adapter#getItemViewType(int position) をOverrideして、返す値を変えることで RecyclerView.Adapter#onCreateViewHolder 側で、ViewTypeによってViewHolderを分けることができます。 今回もデータセット内のStubの有無をチェックして、ViewTypeを返し分けて、ViewHolderを分けることでProgressBarを表示させるようにしています。 ヘッダーなどを実装する場合にも同様のアプローチを取ることで実装できます。 // PhotoGridAdapter.java @Override public int getItemViewType( int position) { Object object = objects.get(position); if (object instanceof ProgressStub) { // データがProgressStubの場合は通常とは違う値を返す return TYPE_PROG; } else { return TYPE_ITEM; } } // PhotoGridAdapter.java @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder viewHolder; if (viewType == TYPE_ITEM) { FrameLayout view = (FrameLayout) inflater.inflate(R.layout.photo_layout, parent, false ); AppCompatImageView photoImageView = (AppCompatImageView) view.findViewById(R.id.photo_image_view); photoImageView.setLayoutParams( new FrameLayout.LayoutParams(imageSize, imageSize)); viewHolder = new PhotoLayoutHolder(view); } else { // ViewTypeがTYPE_PROGの場合はProgressBarのViewHolderを返す FrameLayout view = (FrameLayout) inflater.inflate(R.layout.progress_bar_layout, parent, false ); viewHolder = new ProgressBarLayoutHolder(view); } return viewHolder; } カラム数をpositionごとに変える GridLayoutManagerを使う際には、SpanCountを 2 や 3 などに設定してカラム数を決めると思います。 GridLayoutManagerは拡張することでカラム数をpositionごとに変更することができるため、ヘッダー・フッターを作りたい時や、グリッドの途中でぶち抜きのコンテンツを出したいときに細工を行います。 今回もフッターに出すProgressBarはキレイに中央寄りになってほしいので、ProgressBarを表示するpositionではカラム数が変わるようにGridLayoutManagerを拡張しました。 public class GridWithProgressLayoutManager extends GridLayoutManager { public GridWithProgressLayoutManager(Context context, final int spanCount, final RecyclerBaseAdapter adapter) { super (context, spanCount); setSpanSizeLookup( new GridLayoutManager.SpanSizeLookup() { // 今回はここを細工しています @Override public int getSpanSize( int position) { // ProgressBarを表示するpositionではSpanSizeをいっぱいに広げる if (adapter != null && adapter.getItemViewType(position) == RecyclerBaseAdapter.TYPE_PROG) { return spanCount; } // 1を返すと通常通りのSpanSizeになる return 1 ; } // 今回は触れませんが高速化のためにOverrideしています。詳しくは下記のURLを参照してください。 // http://developer.android.com/intl/ja/reference/android/support/v7/widget/GridLayoutManager.SpanSizeLookup.html @Override public int getSpanIndex( int position, int spanCount) { if (adapter != null && adapter.getItemViewType(position) == RecyclerBaseAdapter.TYPE_PROG) { return 0 ; } return position % spanCount; } }); } } ItemDecorationを使っている場合は注意が必要 ItemDecorationを使っている場合、処理が複数回呼ばれてProgressBarがカクついてしまいます。 そのため、ProgressBarの場合は処理をスキップしてあげる必要があります。 GridLayoutManager特有の問題のためLinearLayoutManagerに関しては気にしなくて大丈夫です。 // GridSpacingItemDecoration.java @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { // ProgressBarのViewHolderの場合は処理をスキップする RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view); if (viewHolder instanceof ProgressBarLayoutHolder) { return ; } int position = parent.getChildAdapterPosition(view); int column = position % spanCount; outRect.left = column * spacing / spanCount; outRect.right = spacing - (column + 1 ) * spacing / spanCount; outRect.bottom = spacing; } まとめ 長年、ListView・GridViewを使い続けているプロジェクトの場合、RecyclerViewへ移行するとなると自分で実装しないといけないものが多くかなりハードであると思います。 iQONの場合も最適化のための独自の仕組みや、広告表示処理などが複雑に絡まっているためRecyclerViewへの移行には時間がかかっています。 ただ、RecyclerViewへ置き換えが完了したページを見ると、もともと巨大で手を入れにくかった処理がモジュールごとに分散できているのでメンテナンスがしやすくなっています。 シンプルなリスト表示・グリッド表示の場合には今まで通りListView・GridViewを使った方が良いと思いますが、positionごとにコンテンツを変えたり、アニメーションを駆使したりしたい場合は、長期的考えてRecyclerViewを使ったほうが良いと思います。 最後に VASILYでは、一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は是非 こちら からご応募よろしくお願いいたします。
  あけましておめでとうございます。データサイエンティストの金田です。現在 iQON では、データ分析の基盤として BigQuery を利用しており、データ分析や計算負荷の高いバッチ処理等に活用しています。しかしながら、通常のデータベースとは若干異なる点があり、効率的な運用ができるまでに様々な試行錯誤がありました。今回はそれらの試行錯誤によって得られた知見をベストプラクティスとして紹介したいと思います。   ログデータのテーブル名に日付を入れる BigQuery でログを保存する場合は、テーブル名の最後に yyyymmdd 形式で日付を入れることをお勧めします。理由は二つあり、1つ目はスキャン対象となるデータ量を抑えられるため、2つ目が Web UI でのテーブル管理が容易になるためです。 処理データ量の抑制 まず、スキャンするデータ量を抑えられるという点ですが、BigQuery ではクエリを発行した際、スキャンしたデータ量に応じて課金が決まってくるため、扱うデータ量が大量になってくると、いかに無駄なデータへのクエリの発行を減らせるかが重要になってきます。 その点、BigQuery では テーブルワイルドカード関数 という関数が容易されており、テーブル名の最後に yyyymmdd 形式で日付を入れておくと、指定した期間のテーブルだけを参照するといったことが可能です。 具体的には、「nginx」というデータセットに、「access_yyyymmdd」といった形式でアクセスログのデータが保存されているとします。その際、11月分のログを分析したいといった場合は、TABLE_DATE_RANGE 関数を使って、下記のようにクエリを書くことで、簡単に指定した期間のログだけを処理対象とすることが可能です。 [sql] SELECT * FROM (TABLE_DATE_RANGE(nginx.access_, TIMESTAMP('2015-11-01'), TIMESTAMP('2015-11-30'))) [/sql] これにより、必要な期間のログだけをクエリの対象とできるため、スキャンするデータ量を抑えることが可能になります。 Web UI でのテーブル管理 また、ログデータを日別で管理することで、Web UI でのテーブル管理も容易になります。 通常は、データセットに日別に多くのテーブルを作成してしまうと、すべてのテーブルが表示されてしまうため、Web UI での管理が煩雑になってしまいますが、テーブル名の最後に日付を入れておくことで、下記のようにまとめて表示をしてくれます。 個別の日付のテーブルに対して操作を行いたいときは、右側のプルダウンで日付を選択すると、特定の日付データだけ操作することが可能です。 \   ビューを活用する 他のデータベースと同様に、BigQuery では仮想テーブルを作成するビューの機能を持っていますので、この機能を活用することで効率的に運用をすることができます。 通常、ログやデータベースのデータを分析に使う際は、JOIN 操作を行って、トランザクションテーブルとマスタデータを結合したり、SQLの関数を使って加工を行ったりしますが、そのような操作はビューとして保存することができます。具体的には、Web UI のクエリ作成画面の下に、「Save View」というボタンがありますので、こちらを押すことでクエリで作成したテーブルを仮想テーブルとして保存することができます。 保存されたテーブルは、下記のように緑色のアイコンと共に表示されるので、ビューであることがするわかります。 実際の運用においては、 データを利用するユーザーは、分析用に加工されたビューに対してクエリを発行するようにすることで、煩雑なSQL文が量産されるという事態を防ぐことができます。 また、例えば上記で説明した TABLE_DATA_RANGE 関数と CURRENT_TIMESTAMP という関数を組み合わせて、任意の期間のデータだけを参照対象とするといったことも可能です。例えば、下記のクエリでは過去60日のログだけを計算対象にしていますが、このように期間を絞ったユーザー参照用のビューを用意することで、無駄なデータに対するクエリ発行を防ぐことができると同時に、ユーザーが意図せず大量のデータに対してクエリを発行してしまうというリスクを抑えることもできます。 [sql] SELECT * FROM (TABLE_DATE_RANGE(nginx.access_, DATE_ADD(CURRENT_TIMESTAMP(), -60, "day"), CURRENT_TIMESTAMP())) [/sql] ただし、大量の JOIN が発生したりするビューを作成する場合は、クエリの結果が返ってくるのに時間がかかることがありますので、レスポンスタイムが気になる場合は、必要に応じてビューではなく、バッチで実体のテーブルを作成することも考慮に入れてください。 非構造化データをBigQueryで扱う BigQuery には、 JSON 関数 というものが用意されており、文字列で保存されているJSONから、キーを指定して必要な値だけを抜き出すということが可能なため、非構造化データを扱うことも可能です。 特に、アクセスログ等は、ビジネス要件によって取得項目が変更になったりすることもありますので、BigQuery で非構造化データも扱うことができると知っておくと、設計の幅が広がります。 具体的には、下記のような形式で文字列としてJSONのデータが入っているとした場合、 スキーマ name type timestamp TIMESTAMP log_json STRING データ例 timestamp log_json 2015-12-27 12:07:49 UTC {"where":"item_detail","item_id":"3030910","model":"iPhone 6"} テーブルから item_id だけを抜き出したいときは、下記のように記述することで JSON 形式で保存してあるデータを取り出すことが可能です。 [sql] SELECT * FROM (TABLE_DATE_RANGE(nginx.access_, TIMESTAMP('2015-11-01'), TIMESTAMP('2015-11-30'))) [/sql] ただし、この方法を使う場合は、JSON文字列として保存されている全てのデータをスキャンする形になりますので、データ量が大きく、頻繁にクエリを発行する必要があるものに関しては、予め必要な項目だけを抜き出して、構造化したデータを別テーブルとして保存しておいた方がよいと思います。 ストリーミングインサートは必要に応じて BigQuery ではストリーミングインサートという機能があり、例えば Fluentd から直接ストリーミングで BigQuery へデータをインポートすることが可能です。 ただし、便利なストリーミングインサートですが、通常のバッチでのインポートに比べて、料金がかかるということと、データに欠損が生じる可能性があるという事があるため、特にリアルタイムに結果を把握必要のないものに関しては、日次バッチでのインポートも考慮した方がよいと思います。 また、ストリーミングインサートを使う場合でも、データに欠損が生じる場合がありますので、バックアップとしてS3やGCSにも保存しておくようにしておきましょう。   バッチ処理にAirflow を活用する 上記でも記載した通り、データ分析基盤として BigQuery を活用するためには、日次でデータを BigQuery へインポートしたり、分析用のテーブルを日次で作成したりする必要がでてきます。 通常、このようなバッチの処理には、cron を使ってバッチを起動する場合が多いと思いますが、cron だと、ジョブに依存関係をもたせたり、ジョブが失敗した場合に、どこで失敗したのか特定することが難しいということがありました。 そこで、VASILYでは、バッチの処理に AirBnB 社が開発した、Airflow というジョブ管理ツールを使うことで、バッチ処理にまつわる業務を効率化しています。このツールを使うことで、例えば下記のように複数のジョブに依存関係がある場合も、その依存関係を考慮して処理を実行してくれ、万が一失敗した場合でも、リトライ処理を自動で行ってくれたり、問題の特定がすぐにできるようになったため、データの管理に関する業務が大幅に改善されました。 最後に 今回は、データ分析基盤として BigQuery を活用するにあたり、どのような点に気をつけて設計や運用を行なったらよいかについて説明を行いました。通常のデータベースとは若干異なるところがありますので、その点は注意が必要ですが、使いこなせば従来では考えられなかった低コストでデータ分析の環境を整えることが可能です。 また、今回ご紹介した BigQuery 以外にも、VASILY では、Tableau を使ってビジネスユーザーがクエリを書くことなく自由にデータを取得できるような仕組みを構築したりと、様々な取り組みを行っていますので、次回はその辺りの仕組みも説明できればと思っています。 最後に、VASILY では、データサイエンティスト を募集しています。データの分析だけでなく、データエンジニアの役割も担う必要がありますので、幅広い経験を積むことができます。 ご興味のある方は是非 こちら からご応募よろしくお願いいたします。
  Merry Christmas! フロントエンド開発の荒井です。今回はフロントエンド開発陣が3ヶ月で行ったサイト最適化を紹介したいと思います。短期間で多くの変更を施したため、今回は取り組みやすく、特に大きなインパクトがあったと思われる内容2つを紹介します。   はじめに VASILYでは提供したい価値を再定義し、ユーザーにとって価値のあるコンテンツを届けるため、2015年8月にiQONのPCサイトのリニューアルを行いました。このリニューアルでは「サイトマップの見直し」「文書構造(h1, title, description etc)」の見直し等を行い、サイトの中身をガラッと変えています。 リニューアルを行った後、Google Search ConsoleやNewRelicで日々サイトの状態を確認していたのですが、日が経つにつれ、細かな問題が出てきました。これから挙げる事例は、日々のチェックにより発見された問題の紹介と解決策です。   Google Search Consoleで発見されたエラー対応 フロントエンドエンジニアの日課として Google Search Console の確認があります。 iQONではECサイトをクロールしてアイテムを集め、ユーザーがそのアイテムを使用してコーディネートを作るというサービスの特性上、出来上がったページに以下の問題が多数報告されていました。 タイトルタグの重複 重複するメタデータ(descriptions) 「Google Search Consoleのエラーを徹底的になくそう」 当たり前のことですが、膨大なコンテンツを保持するサイトですと、数が少ないエラーは見過ごしがちです。エンジニアはまずGoogle Search Consoleに現れたエラーを無くすことに着手しました。例えそれが1件でも改修対象として手を動かしています。   タイトルタグの重複 こちらは主にユーザーが投稿したコーディネートに発生していた問題です。 タイトルタグにはユーザーが入力したコーディネートのタイトルを採用していたのですが、「冬コーデ」のような重複したタイトルのページが多数存在してしまいました。   ユーザーが作成したページのタイトルやメタデータはユニークであるべきです。 iQONでは以下のようにしてユニーク化を行っています。 [bash] 投稿されたタイトル + ユーザー名 + つくったコーディネート(◯◯件目) ex)「ワントーンコーデ♪メイクは派手めだけどコーデはシンプルに☆…」XXXさんがつくったコーディネート(335件目) [/bash] 上記は一例で、様々なところをユニークにする見直しを行っています。   重複するメタデータ こちらはアイテムページに出ていたエラーです。同じ商品が異なるECサイトで販売されていた場合などに説明文が重複してしまい、エラーが起きていました。 そこで、商品の「商品名」「ブランド」「説明文」などの条件から同一商品を判断し、自動でcanonicalを貼るように改修を続けています。   その他の対応 blockquote見直し、no indexの設定、リダイレクトの見直し..etc.. 数えていませんが、数十項目の改善をしたと思います。 大事なのはCGMでコントール不可能だったところをコントロール可能な構成に直していったということです。サイト最適化はこうした塵を積もらせる改善が必要です。   HTTPSの対応 次にHTTPSの話をします。昨今、セキュリティを優先事項とする企業が増えました。VASILYではフロントエンドサーバーのHTTPS対応が遅れていたため、iOS 9のApp Transport Securityが話題になったタイミングでHTTPS化に踏み切りました。   Google HTML/CSS Style Guide まず、100を超えるファイルにベタ書きされたhttpを書き換えるところから始めました。その際のコーディングはGoogle HTML/CSS StyleのProtocolに準拠しました。 httpとhttps両方のプロトコルに対応するところはhttp/httpsを省略し//から始めるようにします。   body { background: url( http://iqon-img.s3.amazonaws.com/static/images/top/iqon_bg_gray.png ) 0 0 repeat; }   body { background: url(//iqon-img.s3.amazonaws.com/static/images/top/iqon_bg_gray.png) 0 0 repeat; }    詳細は Google HTML/CSS Style Guide に記載があります。   HTTPSを標準にする 上記書き換えがすべて終わった上で、サイトの標準をhttpsにします。 リダイレクトを設定すれば良いのですが、外部に提供しているウィジェットなどもあるので、単純に一律切り替えることは出来ず、確認の時間が非常に取られました。 また、CDNがhttps対応をしてないため画像まわりで問題が発生したりするなども起きますので、踏み切る際は事前に確認しておくことをおすすめします。   環境整備 フロントエンドでは社内確認用のステージングの環境にAmazonの Elastic Load Balancing を使用しておらず、その役割を古いバージョンの Varnish に頼っていたため、プロダクトの確認環境を整えるのにも体力を使いました。HTTPS化を進めるにあたり、規模が大きくなるほどコード修正以外にも作業が発生します。   HTTPS化をしてみて サイトの健全さの意味でも対応するべきでしたし、 HTTPS ページが優先的にインデックスに登録されるようになる といった記事にもある通り、インデックスにも効果があるようです。DAUを倍にした要因の一つだと考えています。   まとめ 今回紹介した2つは取り組みやすい2つの事例を紹介しました。 他にも App Indexing 対応や パフォーマンス最適化 などといったことも行っています。Google Search Consoleで確認する限り、クロール数は日に日に増え、3ヶ月で約5倍程度の伸びがありました。それに伴い、DAUは3ヶ月で倍になり、検索からの流入が増えている傾向にあります。 今後は AMP や Service Worker の導入を検討しているので、次回は結果を報告出来ればと思います。   さいごに VASILYではエンジニアを募集しています。 ご興味のある方は是非 こちら からご応募よろしくお願いいたします。それでは良いお年を。
こんにちは。VASILYデザイナーの文山です。 入社して1年が経つタイミングで、弊社のコーポレートサイトリニューアルを経験しました。今日はそれを例に、コンセプトをキーワードまで分解しデザインに落とし込むまでの過程や、デザイナーとして大切だったことをご紹介します。   コンセプトをキーワードまで分解するために社内アンケートを実施 弊社では採用に力を入れていることもありターゲットは採用面接応募者とし、コンセプトは「VASILYらしさを伝える」ことに決まりました。 「VASILYらしさ」をデザインに落とし込むためには、より具体的に言語化する必要があります。そこで社内アンケートを実施し、VASILYらしさをキーワードとしてあげてもらいました。 社内アンケートを実施した理由は、 ・社員が思っているVASILYらしさが把握できる ・自分にない視点が見える ・コンセプトが具体化できる というメリットがあったためです。   アンケートを集計した結果、 先進的 ・ シャープ  ・ エネルギー という3つのキーワードが抽出できました。    どのようにデザインに落とし込んだか より良いデザインにするためにも、チームで共通のコンセプトを持ち、同じ方向性を持つことがとても重要です。 デザインに取り掛かる前に、様々な参考サイトをピックアップし、キーワードと一致するものをまとめてチーム内で共有しました。 そうすることで、デザインの方向性が具体化し、チーム内で共通認識を持つことができます。 参考サイトをピックアップする際は、以下のようなWEBデザインリンク集から情報を集めました。優れたデザインのリンクがカテゴリー別にたくさん紹介されているので、キーワード別に検索することもできておすすめです。 ー参考にしたWEBデザインリンク集ー AWWWARDS I/O 3000 siteinspire straightline bookmark 上記でのWEBデザインを参考に、チームでの方向性をすり合わせた後、社内アンケートから抽出したキーワードをもとに、デザインに取り掛かりました。 ここからは、TOPページ / リクルートページ / 各職種の詳細ページの3つを例に、キーワードからどのようにデザインに落とし込んだのか、ご説明していきたいと思います。  TOPページ TOPページ は第一印象となる重要なページです。 社内アンケートから抽出したキーワードの「先進的」や「エネルギー」というところが伝わるよう、会社のミッションの英字テキストを大胆にレイアウトし、フォントはDINを使用しました。 少し縦長でスタンダードなフォントではありますが、大きく使用することで、スタイリッシュな印象になります。 しかし、スタイリッシュな印象を持たせることができた反面、インパクトが強すぎてしまい、男性的なデザインになってしまいました。 社内アンケートからピックアップしたキーワードには女性的という要素は入っていませんでしたが、弊社は女性向けサービスを運営していることから、男性的なデザインに寄りすぎても会社のイメージと合いません。 そこで英字テキストに透過をかけ抜け感を出すことで、女性的な表現に近づけるよう調整をしています。 リクルートページ リクルートページ はVASILYの行動指標である「HIPSTER」がメインとなるページにしました。 弊社では「HIPSTER」という7つの項目から構成されている、VASILY社員としての行動指標があります。判断に迷った際や、何かを始める時、悩んでいる時など、この7つの項目のもと行動しています。 入社を検討されている方々にこの「HIPSTER」を理解してもらい、同じマインドのもと一緒に働きたいという意図があったため、HIPSTERの各項目の説明を入れ、その後に各職種のエントリーページに繋がるという構成にしました。 また、縦に並べてHIPSTERを説明すると項目が多く、下までスクロールしてもらえない可能性があったため、目線を自然に誘導できるよう斜めのラインを使用したレイアウトにし、「シャープ」「エネルギー」という2つのキーワードが伝わる表現にしました。  各職種の詳細ページ   各職種の詳細ページ では、働くイメージを持ってエントリーボタンへ誘導できるよう、以下の構成にしました。  ・各職種のミッション ・業務内容 ・取り組み ・よく関わる職種 しかし、これらの後にエントリー項目が続くため、コンバージョンして欲しいエントリーボタンがページ下部に下がってしまい目立たなくなってしまいます。そのため、こちらでもキーワードである「シャープ」を取り入れ、ページの背景の色を斜めで区切ったレイアウトにし、メリハリをつけることでエントリー項目がきちんと目立つよう工夫をしました。 大切だったこと   より良いデザインにするためにも、一緒に制作するチーム内で方向性を合わせることは重要です。 そのためには、コンセプトをキーワードまで分解して言語化することがとても大切なことでした。そうすることで、コンセプトをより具体的にすることができ、チームでデザインの方向性を話し合う際にも、そこを軸として考えることができます。 また、コンセプト通りに制作するだけでなく、自身で考え+αしてデザインに落とし込むということもデザイナーとして大切なことだとリニューアルを通して、改めて感じました。 最後に   2015年も残りわずかですね。皆様良い年末をお過ごしください。 現在VASILYではコンセプトをキーワードまで分解して考え、デザインに落とし込めるデザイナーを絶賛募集中です! たくさんのご応募お待ちしております。 https://www.wantedly.com/projects/5485 連絡先:info[at]vasily.jp
こんにちは。iOSエンジニアの遠藤です。 最近のiQONはコンテンツ量が増えてユーザーの詳細ページに表示する情報が多くなってきました。今のデザインでは情報量が多すぎて詳細ページが見づらい状況になっています。そこで以下のようなコンテンツをタブで管理できるかつユーザー情報を表示してスクロールするデザインを実装しました。 実装について色々と調べたのですがあまり情報が無かったので共有したいと思います。実装する上で参考になれば幸いです。   今回のUIを実現する上で解決しなければいけない課題 ・スワイプでもタブの切り替えができる ・ある一定以上スクロールした場合にタブの位置を固定する    解決策 既にiQONの仕組みとしてある、スワイプでタブの切り替えをするところから考えていきたいと思います。 【スワイプでタブの切り替えについて】 ・スワイプでタブを切り替えるにはUIPageViewControllerを使用 ・リストの部分はUIPageViewControllerのChildViewControllerとして実装 ・ユーザー情報を表示するViewはUIPageViewControllerに貼り付ける   UIPageViewControllerを使用することで以下のようなViewの構造になります。   【スクロールについて】 リストで表示する部分とユーザー情報を表示する部分が分割されたので2つのコンテンツのスクロールを連動させなければいけないという課題が増え、以下の3つの項目について考えていきます。   1: ある一定以上スクロールした場合にタブの位置を固定する 2: リストの部分をスクロールするとユーザー情報を表示する部分もスクロールする 3: ユーザーの情報を表示する部分をスクロールするとリストの部分もスクロールする   1: ある一定以上スクロールした場合にタブの位置を固定する リストのスクロール量を見て一定の位置まではユーザー情報を表示する部分のフレームの位置を動かします。   2: リストの部分をスクロールするとユーザー情報を表示する部分もスクロールする ユーザーの情報を表示す部分でスクロールした量をリストに渡してリストのscrollViewのcontentOffset.yを更新させます。   3: ユーザーの情報を表示する部分をスクロールするとリストの部分もスクロールする リストでスクロールした量をUIPageViewControllerに渡してユーザー情報を表示する部分のフレームを動かします。   実装について 以上の課題をふまえて以下の3つのクラスで実現していきます 今回は実装する上で大変だったスクロールについてフォーカスして3つのクラスでの処理を紹介したいと思います。 タブを含む詳細な実装については サンプルコード を用意しましたのでそちらを御覧ください   ・A: PageViewController (UIPageViewControllerを継承) ・B: ViewController(リスト部分) ・C: ContentView(ユーザー情報を表示する部分)   3つのクラスはそれぞれ以下の役割になっています ・PageViewControllerはViewControllerとViewの仲介役 ・ViewControllerをPageViewControllerのChildViewControllerにすればいいように実装 ・PageViewControllerの中にframeやcontentOffsetの調整を書く A: PageViewController スクロールの課題を解決するために以下の処理を書きます。 1: ある一定以上スクロールした場合にタブの位置を固定する   2: TableViewControllerスクロールするとユーザー情報を表示する部分もスクロールする   ・ChildViewControllerがスクロールされるたびに呼び出される ・ChildViewControllerのスクロール量を見てContentViewのフレームの位置を変更 ・ContentViewがある一定の位置まで来たらフレームの位置のを動かさない   ``` swift // ChildViewControllerがスクロールした時に呼び出され、ContentViewのフレームの位置を変更する func upadteContentViewFrame() { guard let currentIndex = currentIndex, vc = pageViewControllers[currentIndex] as? ScrollTabPageViewControllerProtocol else { return } if vc.scrollView.contentOffset.y >= -tabViewHeight { let scroll = contentViewHeight - tabViewHeight updateContentView(-scroll) vc.scrollView.scrollIndicatorInsets.top = tabViewHeight } else { let scroll = contentViewHeihgt + vc.scrollView.contentOffset.y updateContentView(-scroll) vc.scrollView.scrollIndicatorInsets.top = -vc.scrollView.contentOffset.y } } // ContentViewのフレームの位置を変更する private func updateContentView(scroll: CGFloat) { if shouldScrollFrame { contentView.frame.origin.y = scroll scrollContentOffsetY = scroll } shouldScrollFrame = true } ```   3: ContentViewをスクロールするとリストの部分もスクロールされる ・ContentViewのスクロール量を受け取りChildViewControllerのcontentOffset.yを調整する     ``` swift private func updateContentOffsetY(scroll: CGFloat) { if let currentIndex = currentIndex, vc = pageViewControllers[currentIndex] as? ScrollTabPageViewControllerProtocol { vc.scrollView.contentOffset.y += scroll } } ```   Protocolについて 下記のことを実現するためにPageViewControllerにProtocolを宣言しています。 ・PageViewController側からChildViewControllerのscrollViewを触りたい ・ChildViewControllerのUIScrollViewDelegateが呼ばれた時にPageViewControllerのメソッドを呼び出したい このProtocolを宣言することでPageViewController内でcontentOffsetやcontentInsetを調整することができるのでChildViewControllerに処理を書くことを減らせます。   ``` swift // PageViewController.swift ScrollTabPageViewControllerProtocol { var scrollTabPageViewController: ScrollTabPageViewController { ge } var scrollView: UIScrollView { get } } ```   B: ViewController ViewControllerはPageViewControllerProtocolと以下の2つの処理を書けば動くようになっています。 ``` swift // ViewController.swift override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) scrollTabPageViewController.updateLayoutIfNeeded() } func scrollViewDidScroll(scrollView: UIScrollView) { // スクロールしたらPageViewControllerに通知してContentViewのフレームを動かす scrollTabPageViewController.updateContentViewFrame() } ```   C: ContentView ContentViewは以下のような構成になっています。 scrollの量を渡すためだけならUIViewだけの実装で"userInteractionEnabled = false"にすれば動かすことは出来ますが、UIButtonなどのアクションをするパーツを置いた時の制御が難しかったのでUIScrollViewを実装しています。     ``` swift // ContentView.swift var scrollDidChangedBlock: ((scroll: CGFloat, shouldScroll: Bool) -> Void)? func scrollViewDidScroll(scrollView: UIScrollView) { // スクロールした量をPageViewControllerに渡す     if scrollView.contentOffset.y > 0.0 || frame.minY < 0.0 {         scrollDidChangedBlock?(scroll: scrollView.contentOffset.y, shouldScroll: true)         scrollView.contentOffset.y = 0.0 } else {         let scroll = scrollView.contentOffset.y - scrollStart         scrollDidChangedBlock?(scroll: scroll, shouldScroll: false)         scrollStart = scrollView.contentOffset.y } } ```   ・上の方向にスクロールする場合はscrollViewのcontentOffset.yを0にしてフレームだけを動かすようにしています ・逆に下方向にスクロールする場合はフレームを動かさず、scrollViewのスクロール量を渡すだけで調整しています   ``` swift // PageViewController.swift // スクロールした量を受け取りContentViewのフレームを動かす contentsView.scrollDidChangedBlock = { [weak self] (scroll: CGFloat, shouldScrollFrame: Bool) in     self?.shouldScrollFrame = shouldScrollFrame     self?.updateContentOffsetY(scroll) } ```     まとめ ・コンテンツを表示するViewにScrollViewを実装することでアクションを妨害せずにスクロールできる ・UIPageViewControllerを継承したクラスにスクロールの調整の処理を書くことでコンテンツを表示するViewController側は少ないコードを書くだけで済む 一定以上スクロールするとタブの位置が固定されるUIの実装についての紹介でした。 まだ改善しないといけないところはありますが、概ねやりたいことを実現することが出来ました。 最後に VASILYでは、エンジニア&学生インターンを募集しています。少しでもご興味のある方は是非 こちら からご応募よろしくお願いいたします。
こんにちは。VASILYデザイナーの半澤です。 今回はデザインに必要不可欠な写真についてご紹介します。 クオリティの高い写真素材はネットにたくさんありますが、自社のコーポレートサイトや求人用となると、自分たちで撮影せざるを得ません。 VASILYでは撮影をデザイナーが担当しており、絵のキレイさだけではなく「伝わる写真」であるかどうかを意識して撮影しています。 伝えたいことを写真でどう見せているのか、求人のバナーを例に撮影の工程とポイントをご紹介します。 STEP1:ヒアリング 求人のバナー制作依頼が来たら、まず話を聞くことから始まります。 伝えたいことを明確にすることが1番重要です。デザインする上で伝えたいことを言語化すると思いますが、写真も同じことが言えます。 今回の例は求人なので、以下の項目を依頼者からヒアリングします。 ・募集する職種 ・実際にどんな仕事をするのか ・年齢/性別 ・人柄 以上の項目をヒアリングする理由は、「来て欲しい人物像により近いイメージにするため」です。 極端ですが、以下の例を御覧ください。 このバナーはエンジニアの募集に見えますよね。でも、右側では「PUBLIC RELATIONS(広報)」と書いてあります。 広報の募集で黒い画面を開いているPCが写っているこの写真では、イメージが離れすぎてて広報希望の方は応募しにくいと思います。  応募する人にとって「自分が働く姿」をイメージしやすい絵作りが大事 なので、その素材あつめの一つとしてヒアリングを念入りにしておきます。 STEP2:構図を考える STEP1のヒアリングを元に、絵作りにはいっていきます。 1.キャストを決める ヒアリングで来て欲しい人物像を明確にしたので、その人物像に近しい年齢、性別のキャストを決めます。 2.撮影場所を決める キャストが決まったら撮影場所です。求人の写真はバナーとして求人一覧にならびます。毎回社内で同じ場所では人が違うだけで印象が似てしまうので、似すぎないよう心がけています。 3.構図を絵コンテにする キャスト、撮影場所が決まれば構図を考えます。 構図を考える際にも意識することは 応募する人にとって「自分が働く姿」がイメージしやすい絵 なので、なるべく働いている場面を再現するようにしています。 働いている場面といっても、PCに向かって仕事をしている絵だけではなく、複数人で話をしている姿を撮るようにしています。 複数人だと自然な表情が撮りやすいというメリットもありますが、VASILYで働いている人たちの雰囲気を表現するためでもあります。 以上を意識して、ざっくりと絵コンテにしていきます。こちらの絵コンテはデータサイエンティスト募集の時使用したものです。 複数人出演してもらうので、それぞれのキャスト立ち位置や、メインの人物がわかるように描いています。 絵コンテを描くことによって、撮影時にキャストへの立ち位置の説明が楽になります。 4.必要であれば小道具も考える より具体的にイメージを伝えるために小道具の使用も効果的です。例えば「データサイエンティスト」の求人用に撮影したときはホワイトボードに計算式を書いてもらいました。 キャストの年齢や性別、表情だけでは伝えきれないことは小道具も使って雰囲気作りをしましょう。 構図については以上ですが、加えて忘れていけないのは キャストへの事前の連絡 です。 服装や髪型も雰囲気作りにとても重要なので、どういう絵を撮りたいのか求人の概要と共に伝えどのような格好で参加してほしいのか伝えます。 例えば営業の求人用に撮影したときには男性は襟つきのシャツ着用で参加のお願いをしました。 実際に職種が営業の人が写真に出ていても、文脈を知らないひとには伝わりません。 「営業っぽさ」を演出する必要があるので、襟つきのシャツを着用してもらいました。 以上のことから、キャストへ事前に撮影の概要と共に、服装や髪型も事前に伝えておきます。 STEP3:撮影 いよいよ撮影です。 1.撮影準備 キャストを集めるということは、それだけ コストがかかっている ということです。 なるべく時間がかからないよう、撮影前の準備は念入りにしておきたいです。 決めた撮影場所でどの角度で撮るのか、明るさはどれくらい、邪魔なものがないか、あらかじめ確認し準備をしておきます。 2.撮影 撮影は30分以内で終わらせるようにしています。 概要説明   5分 テスト撮影  5〜15分 本番     10分 キャストに概要説明をする際に便利なのが、構図を考えたときのコンテです。 コンテを見せながら、「こういう人材がほしいから、こういう構図で、◯◯な雰囲気でとりたいです。」とキャストに伝えます。 しかし、紙に書いた構図で一発できまるわけではありません。 私はテスト撮影をしながらキャストからもフィードバックをもらうようにしています。 フィードバックをもらう時に便利なのが Eye-fi です。Eye-fiとはWi-fi内臓のSDカードで、撮った写真がリアルタイムで好きなデバイスに転送することができます。 カメラの小さいモニターでキャストに都度見てもらうのではなく、PCの画面で見てもらう事が可能です。 小道具で使用しているPCに転送すると撮影を止めることなく、キャストからフィードバックを貰えます。 フィードバックを受け、構図が決まれば本番です。ここまで念入りに準備をしたので、本番は実際10分もかからずサクッと終わります。 3.デザイン 撮影が終わればチェックです。撮影時には気にならなかった細かい髪の乱れや、洋服のヨレなど、レタッチする箇所をチェックします。 レタッチが完了すれば、デザイン作業です。 VASILYの求人バナーではデザインを統一するようにしています。 写真で職種ごとのイメージを伝えつつ、VASILYという会社のイメージもバナーで表現するためです。 最初にデザインテンプレを複数作り、どれがVASILYらしいのか検討し、現在掲載しているデザインになりました。 しかし、一部デザインが違う例もあります。 上の例はiOSエンジニアの求人バナーです。 アプリエンジニアは、それぞれのOSの専門だとわかりやすくする為に筐体を入れています。 必要に応じてレイアウトを変えていますが、一覧で見た時に統一感が出るようにデザインしています。 おわりに いかがでしたでしょうか。 求人のバナーを例に「伝わる写真」の撮影フローを紹介しました。 あくまで写真は伝える手法の1つで、目的は 応募しようとする人へ自分が働いているイメージが伝わり一緒に働きたいと思ってもらうこと です。 撮影を重ねる度にデザインと同様に伝えるべき内容を明確にし、写真でどう表現するのかが重要だと感じています。 VASILYではクリエイティブを通してユーザーに最適な価値を届けるために、あらゆるアプローチをしてくれる新卒デザイナーを募集しています! ご応募お待ちしております! https://www.wantedly.com/projects/5485 連絡先:info[at]vasily.jp
   Carthageとは? こんにちはVASILYでiOSエンジニアをしているニコラスです。 今回はCarthageの紹介をしたいと思います。 Carthage はiOS / OS X開発のための分散型の (decentralized) パッケージマネージャーです。 既にプロジェクトでCocoaPodsを使っていても、同じプロジェクトでCarthageを使うことができます。 CarthageはCocoaPodsと違って中央レポジトリ型ではないので高速です。 また、100%Swiftで作られているため信頼性が高く、欧米のプログラマーコミュニティで特に盛んに利用されています。 Carthageの基本を学べば(たった5分で)すぐ使えるようになります。   Carthageを使うメリット コミュニティの活発さ Carthageのコミュニティはとても活発で、バグがあればだいたい1日以内に修正のPRが送られています。 環境依存性の低さ Carthageは100%Swiftでできているので、CocoaPodsと違って、環境ごとのRubyバージョンの違いによる問題に悩まされることがありません。 Swiftでできているので、Rubyが書けなくてもiOS開発者がOSSに貢献することができます。 (私も Pull Request を送りました。) シンプルさ Carthageはダウンロードしたらすぐに使うことができます。CocoaPodsの場合はRubyやRubyGemの依存関係により、CocoaPodsのコマンド実行に失敗することがあります。   インストール   Homebrewでインストールする方法 Carthageをインストールする一番簡単な方法は Homebrew です。 [bash] $ brew install carthage [/bash]   直接ダウンロードする Homebrewがなくても、GitHubから carthage.pkg をダウンロードすることができます。 Releases Page   ソースコードからインストール 最新の開発バージョンをインストールすることもできます。 [bash] $ git clone git@github.com:Carthage/Carthage.git $ cd Carthage $ make install [/bash]   ライブラリをインストール   Cartfileを作成 Cartfileに含めたいライブラリを書いてください。 CartfileはCocoaPodsのPodfileやBundlerのGemfileと似たようなものです。   データフォーマット CocoaPodsではインストールするライブラリのリストをPodfileというRubyのDSLで管理します。 Carthageでは、Cartfileというファイルに記述します。 これは OGDL というデータフォーマットで書かれていて、 YAML と似たようなフォーマットです。 Cartfileの文法 一つ目のキーワードに github か git を指定します。 GitHubでホストされている場合は github を使い、それ以外でホストされている場合は、 git を使います。 github キーワードでは、ブランチ名の指定やバージョンを制限することができます。 [bash] github "Username/RepositoryName" "BRANCH_NAME" OR == / >= / <= / ~> [VERSION_NUMBER] [/bash]   実例 [bash] github "ReactiveCocoa/ReactiveCocoa" "master" #最新のmasterブランチをインストール github "rs/SDWebImage" ~> 3.7 #v3.7.xx の最新版をインストール github "realm/realm-cocoa" == 0.96.2 #v0.96.2をインストール [/bash]   コマンドを実行してライブラリをインストール プロジェクトのルートディレクトリで下記のコマンドを実行してください。 [bash] $ carthage bootstrap #ios, mac, watchos, tvosなど、プラットフォームを指定することもできます。 $ carthage bootstrap --platform ios [/bash] 正常に完了すると、[bash] Carthage/Build , Carthage/Checkouts [/bash]という2つのフォルダが作成されます。   Carthage/Build .framework , .framework.dySYM がここに生成されます。   Carthage/Checkouts ライブラリのソースコードファイルがここにダウンロードされます。   Xcode設定 Run Script Phase を追加する プロジェクトファイルを開く TARGETS内のプロジェクト名を選択 Build Phasesを選択 左上の「+」ボタンをクリックから"New Run Script Phase"を選択 コマンド入力欄に /usr/local/bin/carthage copy-frameworks と入力 Input Filesのところにインストールするフレームワークを追加します。   .frameworkをリンクする プロジェクトファイルを選択 Generalを選択 Linked Frameworks and Libraries にCarthageフォルダからフレームワークを選択して追加します。 これでライブラリのインストールが完了し、アプリをビルドすることができます。   Git管理 - Carthage運用のコツ 基本的にgitにコミットするべきものは Cartfile と Cartfile.resolved と Carthage フォルダです。 その理由として、いくつかのメリットがあります。 git clone した時や、ブランチを切り替えるたびに carthage update をする必要がありません。 また、プロジェクトに新しいメンバーが入った時など、 carthage update するタイミングごとにCartfileのバージョンが違ってしまうといったことが避けられます。   まとめ 今回はCarthageの導入方法を紹介しました。 CocoaPodsは根強い人気がありますが、個人的にはiOS開発の本質とは関係ないところでいろいろ問題がありました。 (RubyGemsのバージョンごとのコンフリクトであったり、CocoaPods自身のアップデートでPodsのレポジトリが破損したりなど) CarthageはSwiftで作られているため、Swiftのライブラリしか依存関係がありません。 CocoaPodsの運用で苦労している方は、ぜひCarthageを使ってみてください。
こんにちは、神崎です。今年の6月頃にアドサーバーのアプリケーションサーバ群をAWS ElasticBeanstalk w/ Dockerに置き換えをおこないました。 これにより、アプリケーションレイヤーのauto-scaling環境の構築、deployのフローの自動化、rubyなどのMiddlewareの入れ替えが比較的容易にできる環境になりました。 既存の環境は、以前ブログで紹介したとおり、EC2上にruby2.0でunicorn+sinatraで構築していました。 -  iQONの広告配信システム - VASILY DEVELOPERS BLOG     構成 Containerの内部構成は下記のようになっており、中央にある、ad-serverがアプリケーションの実態のContainerになります。それ以外は、nginx,mackerel,fluentdのContainerがあり、それぞれが特定の役割を担ってアドサーバー全体を構成しています。   デプロイフローは以下のようなフローになり、特定のbranchのgithubにpullrequestをおくりmergeされると、hookされて、eb deployがはしり、docker containerがdeployされます。   またアプリケーション用のDocker imageは、baseとなるrubyとsupervisorをbuildしたimageを作成し、さらにアドサーバで必要なgem類を構築したimageを作成しています。baseのimageとアプリケーションのimageを分けているのは、アドサーバー以外のアプリケーションでも流用しやすくするためです。 外部サービスの利用 コードの管理にはgithub、DockerRegistryに quay.io 、CIに CircleCI を利用しています。これらを連携させることで、上記のように自動でdeployしたり、auto-scalingさせたりを実現しています。 トラブル ElasticBeanstalk+Docker環境に移行してから、概ね大きな問題も起きずにいたのですが、数回トラブルも発生しました。 docker-pullに失敗してダウン 一番大きなトラブルとしては、人的ミスが大きな原因ですが、DockerRegistryとして利用しているquay.ioのパスワードを変更したために、docker pullに失敗して、デプロイができず、アドサーバー全体がダウンすることがおきました。 dockercfgファイルにquay.ioのマスタのaccountを設定してしまっていたため、quay.ioのパスワードを変更したタイミングで、invalidな状態となり、docker pull時にログインに失敗。 また、auto-scalingにより大量にdocker pullが行われるタイミングがあったため、quay.ioのaccountがロックされ、しばらくの間、docker pullができない状態が発生してしまいました。 quay.ioにはdeploy用のrobot accountが設定できるので、そちらを利用することで、上記のような問題は起きないようにしました。 deployに失敗 eb deploy prod でebコマンドがデプロイ時にs3にputするソース一式のファイルの名前がconflicして、デプロイに失敗しました。ebコマンドのs3にputするファイル名は、gitのcommithashの4文字で固定されているので、それが重複して発生しました。 このときは、ElasticBeanstalk+Docker環境を構築中にebコマンド以外で作ったファイルと重複してしまった様子だったので、常にebコマンドでdeployを実行すれば防げるものだと思います。デプロイに失敗するだけならまだ良いのですがフロー的には、コードがデグレードした状態でデプロイされることも考えられるので、そこは気を使う必要があります。 また、Elasticbeanstalkのapplication versions(ソース一式)は500個が上限なので、定期的にWebConsoleから掃除してあげたほうが安心です。 ruby2.2.3へ Docker化したので、rubyの入れ替えもやりやすくなりました。そこで先日ruby2.2.3へ入れ替えを実施しました。 下準備としては、ベースとなるDocker imageをruby2.2.3でdocker buildしてそれを元にしてアドサーバーに必要なものをセットアップしたDocker imageを作成することで完了です。 デプロイには、ruby2.2.3を含んだDocker imageのsha1 hashを、Dockerrun.aws.jsonで指定してあげるだけで、入れ替えのデプロイ作業は完了です。アドサーバーはもともと、広告の配信と各種イベントの処理だけで、そこまで複雑なことはしていないので、ruby2.2.3にしたことによるコードの変更は、利用しているgemのupdateの対応を一箇所しただけでした。 ruby2.2.3による変更で遅くなっては意味が無いのですが、今回は、GCの最適化の恩恵をうけることができました。アドサーバーは50msec以下のレイテンシを最低ラインにしており、GC Executionが5~10msecかかっていたのが1msec以下に縮小しました。 以上、アドサーバーをElasticBeanstalk+Dockerに移行し、ruby2.2.3へ入れ替えの紹介でした。 最後に VASILYでは、新しい技術が大好きなエンジニアを募集しています。少しでもご興味のある方は是非 こちら からご応募よろしくお願いいたします。