Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

UIPickerViewをUIControlを使用してキーボードの様に表示する

こんにちは。 入社して一ヶ月が経過したiOSエンジニアの弘田です。
今回はUIPickerViewをキーボードの様に表示する方法を解説します。

なぜそんなことをするの?

昔のiPhoneでしたら画面の中心などにUIPickerViewを表示しても画面サイズが小さかったので片手で操作できていましたが、最近は大画面化が進み片手での操作が難しくなってきました。
操作性を損なわずにUIPickerViewを使用してもらう為にも今回の方法が役にたつと思います。

Human Interface GuidelinesでもPickerついて触れているページがありこの様な記載があります。

Avoid switching screens to show a picker. A picker works well when displayed in context, below or in close proximity to the field being edited.

翻訳
ピッカーを表示するように画面を切り替えることは避けてください。ピッカーは、編集中のフィールドの下、または近くにコンテキストで表示されたときにうまく機能します。

今回目指すもの

※シミューレーターなのでキーボードを閉じるアニメーションが少しおかしいです

実装

1. UIControlを継承したclassを作る

class pickerKeyboard: UIControl {

}

2.イニシャライザを作成し、自身がタップされた時にinputViewを出す処理を作る

class pickerKeyboard: UIControl {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        addTarget(self, action: #selector(tappedPickerKeyboard(_:)), for: .touchDown)
    }
    
    @objc private func tappedPickerKeyboard(_ sender: PickerKeyboard) {
        self.becomeFirstResponder()
    }
}

3.canBecomeFirstResponderをtrueで返して自身をFirstResponderにする

canBecomeFirstResponderのデフォルトはfalseになっていて、
trueを返さないと後述のinputViewで指定したViewが表示されません。

class pickerKeyboard: UIControl {
    
    //~~~省略~~~
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    
}

4.FirstResponderになった上でinputViewをoverrideする

ここでは表示したいViewを返します。
今回はUIPickerViewをaddSubviewしたUIViewを返します。
UIViewを返す理由はSafeAreaに対応するためです。
inputViewについて(Apple公式)

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputView: UIView? {
        let pickerView: UIPickerView = UIPickerView()
        pickerView.delegate = self
        pickerView.dataSource = self
        pickerView.backgroundColor = UIColor.white
        pickerView.autoresizingMask = [.flexibleHeight]
        
        // SafeArea対応をする為にUIViewを挟む
        let view = UIView()
        view.backgroundColor = .white
        view.autoresizingMask = [.flexibleHeight]
        view.addSubview(pickerView)
        
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        pickerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
        
        return view
    }
}

5.inputAccessoryViewをoverrideしてUIPickerViewを閉じるボタンを作る

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputAccessoryView: UIView? {
        
        let view = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
        view.frame = CGRect(x: 0, y: 0, width: frame.width, height: 44)

        let closeButton = UIButton(type: .custom)
        closeButton.setTitle("閉じる", for: .normal)
        closeButton.sizeToFit()
        closeButton.addTarget(self, action: #selector(tappedCloseButton(_:)), for: .touchUpInside)
        closeButton.setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 1.0), for: .normal)

        view.contentView.addSubview(closeButton)
        
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        closeButton.widthAnchor.constraint(equalToConstant: closeButton.frame.size.width).isActive = true
        closeButton.heightAnchor.constraint(equalToConstant: closeButton.frame.size.height).isActive = true
        closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
        closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true

        return view
    }

    @objc private func tappedCloseButton(_ sender: UIButton) {
        resignFirstResponder()
    }
}

6.通常のUIPickerViewと同様にUIPickerViewDelegateとUIPickerViewDataSourceを継承してデータを表示

class pickerKeyboard: UIControl {
    let array:[String] = ["A","B","C","D","E"]
        
    //~~~省略~~~
}

extension PickerKeyboard: UIPickerViewDelegate, UIPickerViewDataSource {
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return array.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return array[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // delegateなどでViewControllerに選択された情報を渡す
    }
}

7.StoryboradやXibでUIViewのCustomClassとして設定する

まとめ

手順4のUIPickerViewを作った時にpickerView.backgroundColor = UIColor.whiteとしているのでわかり難いですが、 別の色に変更するとSafeArea対応できていることが確認できます。

記事で紹介したコードはGithubで公開しています。
https://github.com/srknra/PickerKeyboard

今回はUIPickerViewをキーボードの様に表示してUXを低下させない様な工夫でしたが、
Studyplusのアプリでは他にもユーザーの事を考えて様々な工夫をしてるので今後もブログで紹介していこうと思います。

Visual Studio Codeの拡張「GitHub Pull Requests」を使ってみた

こんにちは。7月に入社したStudyplus開発部の田口です。
先日Microsoft社が発表したVisual Studio Codeの拡張のGitHub Pull Requestsを試してみたのでその記事を書きます。

まずは弊社のエンジニア陣が普段なんのエディタを使っているのか、アプリチーム以外でアンケートを取ってみました。

  • RubyMine … 2人
  • IntelliJ IDEA … 1人
  • Atom … 1人
  • Visual Studio Code … 1人
  • Vim … 3人
  • Emacs … 1人

Vim強しですね。Visual Studio Codeは自分しか使っていないようでした。
また、アプリチームの方々もXcodeやAndroid Studio以外で、普段よく使うエディタを聞いてみたところ

  • Sublime Text … 1人
  • Atom … 1人
  • Visual Studio Code … 2人

でした。こちらはVisual Studio Codeを使ってるエンジニアが2人いました。嬉しいですね。

RubyやRailsを書くエディタとなるとまず思いつくのがJetBrains社のIDEであるRubyMineですが、前にReact/Reduxを書く際にVisual Studio Codeを使って以来すっかりお気に入りのエディタになったのでずっと使っています。
GitHub社がMicrosoft社に買収されてから、マイクロソフト製のプロダクトにGitHub関連の機能が追加されていくのかなと予想していたのですが、まさにそういったパッケージだと思います。
今回は、GitHub Pull Requestsを実際に使ってみた様子を記載していきます。

インストール

基本的には、Visual Studio Codeの拡張機能が検索できるVisual Studio Marketplaceで「GitHub Pull Requests」と検索すれば出てきます。
Image from Gyazo

使い方

サインイン

拡張を追加すると、画面右下にGitHubへのサインインを求めるダイアログが出るので、クリックしてGitHubにサインインします。

追加項目

GitHub Pull RequestsをインストールしてからVisual Studio Codeのソース管理タブを開くと、「GITHUB PULL REQUESTS」という項目が追加されています。デフォルトは閉じていて最下にあるのでちょっと見落としがちです。
GitHubで管理されているプロジェクトをルートとして開くと、自動的に「GITHUB PULL REQUESTS」に以下の項目が追加されます。
Image from Gyazo
ここではGitHub上にあるプルリクエストを項目別で見ることができます。自分が作成したプルリクエストのみ、自分がアサインされたプルリクエストのみといった項目があります。
例として、RailsのGitHubリポジトリに上がっているプルリクエストを見てみます。
Allを選択すると、今見ているGitHubリポジトリの全プルリクエストが表示されます。
Image from Gyazo
Railsには2018/09/25現在で700以上のプルリクエストが上がっていました。基本的には最新の20件までがデフォルトで表示されるみたいです。

Conversationを見る

確認したいプルリクエストをクリックして開き、さらにその下の「Description」をクリックすることで、GitHub上の「Conversation」タブで見れるページがエディタ部分に表示されます。
こちらも、Railsのリポジトリを例に見ていきます。
Image from Gyazo
ブラウザでGitHubを確認することなくここでプルリクエストの概要やレビューのやり取りを確認できます。Visual Studio Codeで設定したカラースキームでプルリクエストが確認できるのが個人的に嬉しいポイントです。
コミットハッシュのリンクをクリックするとブラウザでGitHubの当該コミットのページを開きます。
右上のCheckoutボタンで、当該ブランチをワンクリックでチェックアウトできたりもします。
また、コメントやレビュー、プルリクエストを閉じたりもVisual Studio Code上でできます。便利ですね。
Image from Gyazo
現状ではプルリクエストのマージはできないみたいですね。

差分を確認する

GitHub上の「File Changed」も簡単に見れます。「Description」以下がファイルごとの差分になっており、それをクリックするとエディタ部分に表示されます。
Image from Gyazo
アイコンは

  • A … Add
  • D … Delete
  • M … Modify

だと思われます。直感的ですね。
行番号の右にある+ボタンをクリックすることで、コメントをつけることもできます。
Image from Gyazo

今後改善してほしい点

非常に便利なこの拡張ですが、個人的に今後のバージョンアップで改善してほしい点としては

  • マルチルートワークスペースに対応してほしい
  • マージできるようにしてほしい
  • レビュアの設定などができるようになってほしい

という感じでしょうか。
GitHub上でできることがすべてVisual Studio Code上でできるようになってくれれば最高なんですが、さすがに高望みな気もしています。
個人的に特に気になる点はマルチルートワークスペース非対応なところです。複数のリポジトリを横断的に見るためにマルチルートワークスペースを利用しているのですが、現状はマルチルートワークスペースだとGITHUB PULL REQUESTSの項目が表示されないようになっています。
ここが改善されて対応してくれるようになるとさらに虜になると思うので、期待したいところです。

まとめ

GitHub Pull Requestsを触ってみて、自分はかなり便利だと感じました。
まだバージョンも0.1.6(2018/09/25現在)なので、今後のアップデートに期待したいと思います。

buildersconに行ってきた

スタディプラスCTOの島田です。 今回は9/6~8に開催されたbuilderscon tokyo 2018へ行ってきた感想を書きます。

f:id:yo-shimada:20180912200947j:plain

最初に

buildersconとは

buildersconは、「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭りです

という趣旨のもと、インフラ・IoT・サーバー・デザイン・etcと本当に幅広いジャンルのセッションが聞けます。

builderscon.io

スタディプラスは今回スポンサーとして協賛をさせて頂きました。 ノベルティグッズとしてペンと付箋を提供しました。

f:id:yo-shimada:20180912201804j:plain

当日は私を含め3名(島田、石上、田口)でbuildersconに行ってきました。 どのセッションも大変興味深かったのですが、その中でもこの3名のがそれぞれ特に印象に残ったセッションの感想を書かせてもらいます。

島田

IoT開発の闇

SNS等でのシェア禁止の内容という事で詳細は触れられないのですが、闇(笑)を物凄く堪能できました。これだけで前夜祭行って良かったかも。

パスワードレスなユーザー認証時代を迎えるためにサービス開発者がしなければならないこと

speakerdeck.com

パスワード認証の現状は、色々と漏洩リクスがあり課題・問題ありという整理に共感。 ではパスワードレスを実現するための最新動向はどうなっているかという点での、FIDO 2.0、WebAuthn APIの紹介にまだまだ普及への時間がかかりそうだが、未来を感じた。

「Web とは何か?」 - あるいは「Web を Web たらしめるものは何か?」

Webの黎明期の動向から丁寧に説明があり、Webに関わる人の視点から、どうWebが変化したが整理されていた。 そして、今後のWebがOSに近づいていくのでは、という流れが秀逸。

石上

ForSchool事業部の石上です。 私は普段ウェブアプリケーションのエンジニアなので、それに関連ありそうな発表を中心に聞いてきました。

個人的に1日目、2日目でそれぞれ特に面白かったのは以下の発表です。

Electronによるアプリケーション開発事情2018

ElectronベースのMastodonクライアントWhalebirdを個人で開発されているh3potetoさんの発表。 趣味でここまでできるのすごい...。

リリース直後はElectron製ということで評判が悪かったそうですが、原因を調べてパフォーマンスの問題を解決したりして、今では多くの人に快適に使ってもらえているそうです。

こちらの発表では、実装面でのSwiftでiOSアプリを開発していたときとの比較があり、勉強になりました。

私はiOSのことを全然知らないので、なんとなくSwiftのほうがGUI設計の考え方は進んでるんだろうなくらいに思っていました。 そのため、Fluxを使えることがElectron選択のメリットとなることに少し驚きました。

一方で、TootのStreamingによる描画パフォーマンスの劣化の話では、iOSのUITableViewの描画の最適化みたいなところは便利にできてるんだなと感心しました。

その他もMac App Storeへの配布周りのつらみなど、お試しデモアプリだけ作っていては出てこないような問題について聴けて貴重でした。

デザイナーとうまく協働する方法

buildersconの中では異色な、デザインをつくる際のコミュニケーションについての発表でした。 偉い人の意見を優先してしまう、あるいはデザイナーの作ったものを無条件で正しいとしてしまったりせず、論理的にデザインを作っていく方法について話されていました。

「よい」というニュアンスを自分の中ではなく組織内で共有するためのプロセスが必要で、それに基づいてデザインの評価は行われるべき。そうでないと、デザインがセンスや好みだったり偉い人が決めるみたいな状況になってしまう。そうならないように、徹底した言語化と文書化が必要という話でした。

普段開発ばかりしていると、ドキュメントをあくまでも補助的な、「あったらいいよね」くらいのものとして考えてしまいがちです(少なくとも私はそうでした)。しかし、こちらの発表を聞いて、ドキュメントは議論のベースに使ったり手戻りを防ぐための重要なツールであると認識することができました。

田口

1日目、2日目それぞれで自分が良いと思ったセッションの感想を書きます。

1日目: ブロックチェーン(DApp)で作る世界を変える分散型ゲームの世界

speakerdeck.com

この発表は、発表者の緒方さんご自身がブロックチェーンを用いて作っているゲームの説明を元に、分散型ゲームの世界を解説していくというものでした。 発表が丁寧でわかりやすい説明だったので、ブロックチェーンをほとんど知らない自分はとても助かりました。

ブロックチェーンは様々な分野で活用される可能性がありますが、まだまだ実験段階のようで、現在は主に「投資」の面で利用されていることが多いそうです。 そんな中、「投資」ではなく「利用」に重きを置いたブロックチェーンの活用という点で、ブロックチェーンを利用した様々なゲームが紹介されていました。 発表内では、ブロックチェーンゲームの特徴として ・ゲーム内で購入や投資したものが資産になる可能性がある ・セキュアで公平な取引 ・「トークンエコノミー」と呼ばれるトークンの互換性 の三つが挙げらています。 個人的には特に三つ目のトークンエコノミーが気になります。価値をトークンに落とし込むことで、様々なものに応用できそうです。 シームレスな接続のために規格が統一されてきているそうなので、今後が楽しみです。

2日目: RDB THE Right Way ~壮大なるRDBリファクタリング物語~

speakerdeck.com

普段自分たちが利用しているRDBの設計・リファクタリングをどのようにやっていくかという発表でした。 アンケートシステムの回答を保存するという具体的な例を元に、どのような罠があり、どう対応していくかが明確でわかりやすかったです。

データベースで扱うのは「データ」、アプリケーションで扱うのは「情報」であり、どのようにデータを保存するかを設計する「データ設計」と、データをどのように加工・利用するかを設計する「情報設計」を、まずそもそも認識できていなかったなと反省しました。 情報を優先してデータ設計をするとデータに矛盾が生まれるので、まずはモデリングをしっかり行うことも常に念頭に置いていこうと思います。 Entityの定義やそれの関連付けなど、とても勉強になることばかりでした。 そーだいさんが紹介されていたSQLアンチパターンの本をしっかり読もうと思います。

最後に

3名とも初めて参加したのですが、どのセッション大変面白く是非とも来年も参加をしたいと思います!

記念撮影ブースで石上、田口が撮影してもらった素敵な写真をあげておきます。

f:id:yo-shimada:20180912201421j:plain

iOSDC2018に参加しました

こんにちは。スタディプラスに9月からiOSエンジニアとして入社した弘田です。

Kotlin festの記事でも紹介しましたが、 弊社には 勉強会・カンファレンス参加補助があるので、iOSDC2018に参加させていただきました。ありがとうございます。 弊社のiOSエンジニアも登壇したので弊社からは2名参加しました。 内一名は「iOSでグラフを描くために必要な知識について」というタイトルで登壇しています。

ちなみにスタディプラスは今回シルバースポンサーとして協賛して、ノベルティ(付箋とボールペン)を提供しております。

iOSDC 2018とは

iOSDC JapanはiOS関連技術をコアのテーマとした技術者のためのカンファレンスです。 去年の開催を大盛況のもと終え、みなさまのBlogやSNSでのシェアのおかげで無事、帰って参りました! そして、今年は3日+前夜祭の3.5日開催です!祭りです!予定を空けておいてください!

iOSDC公式ページから引用

参加したセッション

セッションの数が多いのでいくつか抜粋してご紹介させていただきます。 ※登壇者の敬称略

「標準アプリから学ぶ、HIGが教えてくれないiOSデザインのこと」

Ryo Usami : https://speakerdeck.com/usa619/biao-zhun-apurikaraxue-bu-higgajiao-etekurenaiiosdezainfalsekoto

最近よく見るSemi-ModalViewの解説があり、ふわっとした認識が固まったので聞けて良かった。


MicroViewControllerで無限にスケールするiOS開発

tarunon : https://www.icloud.com/keynote/0vgTYDXyHQTd0l1FKTiF1jT7g#MicroViewController-en

ベストトーク賞1位のセッション まだちゃんと理解しきれてないので、スライドの読み直しやビデオを見たいと思います。


5000行のUITableViewを差分更新する

ばんじゅん🍓 : https://speakerdeck.com/banjun/difference-update-uitableview-with-5000-rows

最近流行っている差分更新 5000行もあれば遅くなるので、差分更新ライブラリなどを比較して早くするのが非常に面白かった。


UIViewとUITextInputで作る縦書きのTextView

六々 : https://speakerdeck.com/cc4966/vertical-textview-based-on-uiview-with-uitextinput

標準では用意されていない縦書きのTextViewを自作するセッション 文字の描画エンジンも自作だそうで開発に数年かかっている大作らしい。 基本的に縦書きで表現したいことは少ないが需要はあると感じた。


iOSでグラフを描くために必要な知識について

須藤将史 : https://speakerdeck.com/masashi_sutou/iostekurahuwomiao-kutamenibi-yao-nazhi-shi iOSDC2018で「iOSでグラフを描くために必要な知識について」というタイトルで発表しました

弊社iOSエンジニアのセッションです。 Studyplusではグラフが多く使用されており、その全てをライブラリを使用せず自前で書いています。 グラフを書く際に必要な知識などがセッション内で解説されています。


その他のセッション

Qiitaにまとめてくださっている方がいました。 iOSDC 2018 セッション資料まとめ

雑感

  • 今回が3回目の開催 回を重ねるごとに参加者が増えている 参加者が増えているのにスタッフの対応の質などが毎年上がっている 会場のネットワークチームが快適なWi-Fiを提供してくれていた!

  • ランチは毎日違う物が提供されていた

  • スポンサーブースの力の入れ方がすごかった

まとめ

聞きたいセッションが被っていることが多かったので、「まだiOSDCは終わっていない!」というのが個人的な感想。 いつになるのかは不明だが、全セッションの動画がYouTubeに上がるはずなので早く見たいです。

懇親会にも参加し美味しい料理やお酒を楽しみながら、交流をできたのも良かったです。

来年も是非開催してくれることを願っています。

スタディプラスでは現在、Swift化を進めています。是非、興味ある方はご連絡お待ちしています

GraphQLを導入しようとしている話

こんにちは。Studyplusでサーバーサイドを担当している金澤です。 弊社ではいまapiの一部にGraphQLを導入するべく取り組んでいます。

GraphQLってなんだという話や導入手順などはweb上にすでに沢山あると思います。 なのでそのへんはあっさりめで、検証にあたってどのような実装をしているかという話をします。

で、GraphQLってなんだ

公式ページから言葉を借りれば、

A query language for your API

です。

apiに対する問い合わせをクライアントで組み立てて柔軟にできます。

上記ページのデモがとても分かりやすいのでピンと来ない方は是非ご覧ください。

GraphQLでできる3つのこと

query

  • データの問い合わせ
  • 今回はこの話だけします

mutation

  • データの変更

subscription

  • いわゆるpub/sub

なぜGraphQLなのか

動機としては

  • パフォーマンスの改善のため、各種apiを一つにまとめようという話が以前からあった
    • GraphQLによってパフォーマンスがよくなるという話ではなく、apiが細分化しすぎていてインデックスも複雑(あるいは効いてない)というケースを整理しましょうという流れ
  • 画面をトライアンドエラーするとき、いちいち微調整にコミュニケーションコストがかかるのは勿体無い
    • ある程度好きにできるapiを用意するので、開発フェーズによってはそれを柔軟に使って完結してほしい
    • クライアントとサーバを同じ人が作るような体制の場合はこういう凝った仕組み必要ないかもしれない
  • ナウい
    • たまには新しいものを取り入れないと澱む

といったところです。

導入方法

まっさらなrailsプロジェクトにGraphQLを導入する手順です。

railsプロジェクトを作る

$ rails new graphql_test

graphql-rubyのインストール

https://github.com/rmosolgo/graphql-ruby

gem 'graphql'
$ bundle install
$ rails generate graphql:install
  • 上記コマンドでGemfileにgem 'graphiql-rails', group: :development というのが追加されるので、使う場合はもう一度bundle install

    • graphiqlは開発用のインタラクティブな画面です
    • 多分graphicとgraphqlの言葉遊びなのかな?
  • app/graphql以下にいろいろ関連ファイルができます

    • application.rbconfig.eager_load_paths << "#{config.root}/app/graphql" を足すと便利

graphiql

rails s して http://localhost:3000/graphiql から見れます

Hello, World!

インストールが終わったら、graphiql画面に{ testField }と入力して実行しましょう。 いつものが返ってきます。

Queryで頻出しそうなパターンをどう実装したかの話

ここからが本題です。 データの問い合わせapiを作るにあたって、よく登場するパターンをどう解決したかという例を三つほどご紹介します。

ユーザー権限をチェックしたいパターン

正しくインストールされていると、app/controllers/graphql_controller.rb というコントローラができてます。 下記のような感じです。

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]

    context = {
      # Query context goes here, for example:
      # current_user: current_user,
    }
    result = GraphqlTestSchema.execute(
      query, variables: variables, context: context, operation_name: operation_name
    )
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end
  # 以下略

コメントにあるように、コンテキストを意識したい場合はcontext に追加し、後から取り出せます。 権限チェックのためにログインユーザーが欲しい、というような場合は例えば下記のように実現できます(実際のコードではないです)。

GraphiQL::Rails.config.headers["Authorization"] = -> (context) { "Bearer hogehoge" }
    # 前略
    auth_header = request.headers["Authorization"]
    session = find_session_by_header(auth_header)
    context = {
        session: session
    }
    # 以下略

こうすると、query_typeやobject_type内でresolveする際にctxからセッションを取得できるようになります。

field :test, types.String do
    resolve ->(obj, args, ctx) {
        ctx[:session] # コントローラでセットしたオブジェクト
    }
}

ページングするパターン

データの問い合わせにページを指定したり、検索文字列を指定したりなどよくあると思います。

以下のようにクエリにはargumentを設定することができます。

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :page, !types[types.Int] do
    description "ページングテスト"
    argument :per_page, types.Int, default_value: 5, prepare: ->(limit, _ctx) { [limit, 5].min }
    argument :page, types.Int, default_value: 1
    resolve ->(_obj, args, ctx) {
      page = args[:page]
      limit = args[:per_page]

      arr = %w(1 2 3 4 5 6 7 8 9 10)
      offset = [(page - 1), 0].max * limit
      last = offset + limit
      arr[offset...last]
    }
  end
end

上記query_type.rbを上記の内容にし、下記クエリをqraphiql画面から入力することで、ページングが確認できます。

{ page(per_page: 3, page: 2) }
{
  "data": {
    "page": [
      4,
      5,
      6
    ]
  }
}

lazyに呼ぶパターン

GraphQLはapi呼び出し側でクエリを自由に書けるので、呼び出される側が無駄な計算をしないように気をつける必要がありました。

例えば、下記のようなTypeがあったとします。

Types::Test::LazyType = GraphQL::ObjectType.define do
  name 'Lazy'

  field :karui, !types.String
  field :omoi, !types.String
  field :sinu, !types.String
end

このtypeを使った返事を以下のように実装した場合、

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :lazy, !Types::Test::LazyType do
    resolve ->(_obj, _args, _ctx) {
      karui = "軽い!"
      omoi = -> () {
        sleep(3)
        "重い..."
      }.call # ここが重い
      sinu = ->() {
        raise "死ぬ"
      }.call # ここで死ぬ

     OpenStruct.new(karui: karui, omoi: omoi, sinu: sinu)
    }
  end
end
  • クエリで必要なものだけ取得できる意味がない
  • というかこの場合は必ず死ぬ
  • しかも順番に実行されていくので重い作業を待った末に死ぬ

という本末転倒なことになります。

そこで下記のように、必要な分だけ計算するようにしました。

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :lazy, !Types::Test::LazyType do
    resolve ->(_obj, _args, _ctx) {
      res = OpenStruct.new
      def res.karui
        "軽い!"
      end

      def res.omoi # ここでは重くない
        sleep(3)
        "重い..."
      end

      def res.sinu # ここでは死なない
        raise "死ぬ"
      end

      res
    }
  end
end

こうすることで、

  • { lazy { karui } } 実行時は軽い
  • { lazy { karui omoi } } 実行時は重いものを取得したいのでしょうがない
  • { lazy { sinu karui omoi } } とすることで一瞬で死ぬため計算資源を無駄にしない
    • ただし、{ lazy { karui omoi sinu} } とすると順番に実行されてやはりomoi分が無駄になる

当たり前の話ですが、個別に実行できるようにし、必要なものを必要なだけ取るようにできると無駄がありませんでした。

終わりに

まだ手探り状態なので、実運用に乗せたら何が起きるかなどはわかっていませんし、テスト運用して採用を取りやめる可能性もゼロではないと思っています。

起きそうな問題としてパッと思いつくのは、

  • cartesian product問題やN+1問題に代表されるようなデータアクセス上の典型的諸問題が、レイヤーを一段またいで脳みそが二つになることによってより厄介になったりしないだろうか
    • たくさん取得してクライアントで選別しよう、みたいなコードを書こうと思えば書けてしまう
    • もちろんapiの作りっぷりで制限することは可能で、そういう意味では従来の作りとあまり変わっていないかもしれない
  • endpointが一つになるので、NewRelicなどでパフォーマンス計測する際よくわからないことにならないか?
    • クエリ別に集計する必要がありそうだが対応しているのか?
      • { hoge fuga piyo}fuga piyo hoge は同じものとして集計されてほしいのか違うのかなど場合によって違いそう
    • 関係ないけどNewRelicの無料プランがだいぶ制限されるようなのでどなたか良い代替プロダクトを教えてください

といったところでしょうか。

とはいえ、うまく使えば開発効率が上がりそうな手応えも感じています。 色々な導入例や運用例から、ベストプラクティスやアンチパターンが出来上がっていくと思いますし、追随していきたいと思います。

Kotlin festに行ってきました

弊社には 勉強会・カンファレンス参加補助 があります。 その制度を使って会社のお金でKotlin Festに参加させていただきました。補助といいつつ基本全額サポートしてくれました。 ありがとうございます。

image.png (42.8 kB)

Kotlin Festとは

「Kotlinを愛でる」をビジョンに、Kotlinに関する知見の共有と、Kotlinファンの交流の場を提供する技術カンファレンスです。(connpassから引用)

愛でる と言われる言語なんて今まであっただろうか。 Kotlinかわいいとも言われている。Kotlinかわいい。

そんな素敵な言語のカンファレンスです。

ざっくりとKotlinをご紹介

色々な方がKotlinについて説明してくれているので、詳しいことはそちらにお願いするとして。 オープニンセッションでお聞きしたKotlinの哲学をそのまま流用してます。

  • 実用主義: Javaの考えがそのまま使える、学習が容易!
  • 簡潔: 読みやすさ重視でボイラープレートを減らす
  • 安全: 静的型付け、Null安全
  • 相互運用性: Javaと相互に行き来、Javaライブラリの豊富な資産が使える!

より良いJavaなイメージです!

セッション

※登壇者の敬称略

image.png (11.4 MB)

オープニングセッション

長澤太郎 藤原聖

ここで発表いただいた Kotlinの哲学 は上記で引用させていただきました

参加したセッション

「Kotlin で改善する Android アプリの品質」

あんざいゆき https://speakerdeck.com/yanzm/kotlinfest

「How to Test Server-side Kotlin」

鈴木 健太 ・ 前原 秀徳 http://htn.to/kwU92M

「start from Convert to Kotlin」

望月美帆 https://speakerdeck.com/mochico/start-from-convert-to-kotlin

「Kotlin コルーチンを理解しよう」

八木俊広 https://speakerdeck.com/sys1yagi/kotlin-korutinwo-li-jie-siyou

その他のセッションなど

こちらにまとめられています。Kotlin Fest 2018 - 資料一覧 - connpass

雑感

  • 記念すべき一回目
    今回の盛り上がりを見ると第二回も開催しそう!

  • 参加者 367名

  • チケット完売!
  • 人が多すぎてwifiつながらないときもアリ
    ほんとに人がたくさんいました。会場の椅子が足りなくて床に座ってセッション聞いてたこともありました。登壇者の発表に拍手が頻繁に起きたりいい雰囲気でした。

  • ランチは会場のレストランで特別に500円ランチが。カツカレー
    ちなみに会場はコチラ

  • mixiさんのブースでやってたKotlin Quiz 5問正解でお菓子もらえた。

  • Androidエンジニア以外にもサーバーサイドでkotlin使ってますという人が意外と多かった。
  • トートバッグもらった
    友人たちに好評。 ことりかわいい→KotlinかわいいでひとつKotlinの普及

IMG_4865.JPG (81.7 kB)

まとめ

セッションの内容や途中少しお話させていただいた方、企業ブースなどを見てKotlinの盛り上がりを改めて認識しました。Kotlin正式リリース前に初めて触ってから考えると、日本語の記事も導入事例も爆発的に増えてきました。 今年GoogleがAndroid公式言語としてKotlinが選ばれたためKotlin == Android と思われがちですが、サーバーサイドでの導入事例も増えてきています。実際私が参加したセッションではサーバーサイドで使ったことある方が4割ほどいました。

AndroidのみならずサーバーサイドやJs / Kotlin nativeなどもあり、これから益々Kotlinを活用するプロジェクトが増えてくると思います。むしろKotlin好きとしては増えてくれることを願っています。

私は参加してないのですが、Kotlin Festの懇親会はとても美味しい料理が出たという噂が。 心残りはそれだけです。第二回は必ず懇親会に参加したい。

スタディプラスでは現在、Kotlin化を進めています。是非、興味ある方はご連絡お待ちしています

FirebaseでSlack Slash Commandsを楽に運用する

ForSchool事業部開発グループの松田です。

弊社ではSlackを導入しており、いわゆるChatOpsに活用しています。
Slackにはさまざまな便利機能がついていますが、その中の一つにSlash Commandsというものがあります。
今回はこのSlash Commandsを自作して、そのバックエンドにFirebaseを利用した例をご紹介します。

Slack Slash Commandsとは

Slack内で /command のようにスラッシュから始まるコマンドを発言することで、特定のエンドポイントにリクエストを送信する機能です。
実装次第では /command sugoi argument などのように引数をとることもできるので汎用性が高いです。
標準機能として /remind/invite など複数のコマンドが定義されています。
Built-in slash commands – Slack Help Center

Firebaseとは

ここのところ流行っている感の強いモバイルプラットフォームです。
Googleの展開しているプラットフォームで、最近では国内の導入事例も見かけるようになりました。
認証周りをやってくれるAuthenticationや、ファイルホスティングのCloud Storageなどいくつかのサービスがありますが、
今回はJSの関数をさまざまなトリガーで実行できるCloud Functionsと、DBを提供するFirestoreを利用して簡単なコマンドを実装してみました。

実際に作ってみる

今回は弊社のボードゲーム部で使用する /boardgame コマンドを実装してみます。
仕様は /boardgame search players 4 の入力からDBを検索しプレイ人数に合致するボードゲームを返すコマンドとします。
Slash Commandsはコマンドが発行されると設定したURLにリクエストを投げるので、その先をCloud Functionsで受ける感じでやっていきます。

Slack AppとFirebaseプロジェクトを新規登録

Slack AppとFirebaseのプロジェクトを新規に登録します。
名前などいい感じにしましょう。

Cloud Functionsを仮デプロイ

firebase/firebase-toolsを使って、ローカルにFirebaseのプロジェクトと連携するフォルダを作ります。
初回設定時に使用するサービスを選択しますが、前述の通りFunctionsとFirestoreを選択します。
f:id:matsudap:20180817154806p:plain Functionsを選択するとJSとTSのどちらで書くかを聞かれますが、なんとなくTSにしました。
この時点で ./functions/src/index.tshelloWorld というHTTPリクエストがトリガーの関数が定義されているので、 boardgame に改名して、以下のコマンドを実行します。

$ firebase deploy --only functions

すると、デプロイ結果としてトリガーになるURLが表示されるので、ブラウザ等でリクエストをして Hello, World! と表示されればここまでは完了です。

自作Appをワークスペースに追加する

先程作ったSlack Appの設定メニュー「Slash Commands」からコマンドを登録します。
Request URLには先ほどデプロイしたCloud Functionsに割り当てられたURLを設定しておきます。
あとはよしなに設定します。
f:id:matsudap:20180817154828p:plain

Slash Commandから呼ばれる関数を実装する

とりあえず手元にボードゲームのリストを用意しました。

{
  "title": {
    "ja": "アグリコラ",
    "default": "Agricola"
  },
  "minPlayers": 1,
  "maxPlayers": 5,
  "minPlayingTime": 30,
  "maxPlayingTime": 150,
  "properAge": 12
},
{
  "title": {
    "ja": "デッドオブウィンター",
    "default": "Dead of Winter: A Crossroads Game"
  },
  "minPlayers": 2,
  "maxPlayers": 5,
  "minPlayingTime": 60,
  "maxPlayingTime": 120,
  "properAge": 13
},
...

これを適当なスクリプトを書いてFirestoreに突っ込みました。
最近使えるようになった配列の array-contains オペレータを使ってみたかったので、
Firestoreのトリガーで、minPlayersとmaxPlayersを下限と上限にした数値の配列をplayersフィールドに追加するようにしています。

そして実装ですが、公式のチュートリアルを参考にざっくりと以下のようになりました。1

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'

admin.initializeApp()
const db = admin.firestore()

class RequestError extends Error {
  code: number;
}

const commands = ['search'];

const searchFunction = (args) => {
  return new Promise((resolve, reject) => {
  })
}

export const boardgame = functions.https.onRequest((req, res) => {
  return Promise.resolve()
    .then(() => {
      if (req.method !== 'POST') {
        const error = new RequestError('Only POST requests are accepted');
        error.code = 405;
        throw error;
      }
      if (!req.body || req.body.token !== functions.config().slack.token) {
        console.log(req.body, functions.config().slack.token);
        const error = new RequestError('Invalid credentials');
        error.code = 401;
        throw error;
      }
      const [command, ...args] = req.body.text.split(' ');
      if (command == 'search') {
        let query:FirebaseFirestore.Query | FirebaseFirestore.CollectionReference = db.collection('games')
        if (args.includes('players')) {
          const playersIndex = args.indexOf('players') + 1;
          const players = args[playersIndex];
          query = query.where('players', 'array-contains', parseInt(players));
        }
        query.limit(3).get().then((querySnapshot) => {
          resolve({
            text: 'Boardgame Search Result',
            attachments: querySnapshot.docs.map((doc) => {
              const game = doc.data();
              return {
                title: game.title.ja,
                text: game.title.default,
                fields: [
                  {
                    title: 'Players',
                    value: `${game.minPlayers} ~ ${game.maxPlayers}`,
                    short: true,
                  },
                  {
                    title: 'Time',
                    value: `${game.minPlayingTime} ~ ${game.maxPlayingTime}`,
                    short: true,
                  },
                  {
                    title: 'Age',
                    value: game.properAge,
                    short: true,
                  }
                ]
              }
            }),
          });
        })
      } else {
        return new Promise((resolve, reject) => {
          resolve({
            text: 'Unknown command!',
            attachments: [],
          })
        })
      }
    })
});

functions.config().slack.token にはSlack App Basic Information内のVerification Tokenを予め設定しておきます。

Cloud Functionsにデプロイする

再度を以下のコマンドを実行してCloud Functionsにデプロイします。

$ firebase deploy --only functions:boardgame

Slach Commandを実行する

Slack Appを追加したワークスペースのチャンネル上で以下を発言します。

/boardgame search players 1

f:id:matsudap:20180817154858p:plain

便利ですね。

まとめ

Firebaseは運用コストが低いので、こういったピンポイントなAppのバックエンドとしては最適だと思いました。
FirebaseのいろいろなサービスもCloud Functionsからスッと使えるので対応できる幅も広そうです。
一方で、今回使用したCloud FunctionsとFirestoreは(2018/08/20現在)ベータでの提供なので留意してください!

また、弊社では就業後にボードゲーム部が不定期活動中です!
ボードゲームがしたい、Firebaseで業務改善したいエンジニアをどしどし募集しております〜。
まずはお気軽にオフィスに(ボードゲームを)遊びに来てください〜。
採用情報 | スタディプラス株式会社

www.wantedly.com


  1. このチュートリアルはFirebaseではなくGoogle Cloudのものなので、設定値の取得方法など微妙に差異があるので注意してください。