ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

ユニファのQAになってフラットにものづくりに関われた時の思い出

ごあいさつ

こんにちは。

ユニファアドベンドカレンダーをご覧いただきありがとうございます。 QAチームの斉藤と申します。 今回は私がユニファに入社して「ものづくりに関わってるなぁ」と実感した思い出をふりかえります。

技術的なことは全く書いてないので、のんびり読んで頂ければと思います。

読んでほしい人

  • ユニファのQAに興味がある人
  • QAのものづくりってどういうことだろう?って思ってる人

ご注意

言葉ってふしぎなもので、使う人によって意味が全く違います。 あなたの「ものづくり」と私の「ものづくり」、たぶん意味が違う可能性の方が高いです。 ここでは私が考える「ものづくり」とはこういうことで、具体的にはこんなことしたんだな、と読んで頂けたら嬉しいです。

自己紹介

最初に少し自己紹介させてください。

2019年1月にユニファに入社しました。プライベートでは小学生と保育園児のママです。 QA歴は5年と少し。 ユニファでは、比較的規模が大きいプロジェクト内でのテスト設計と実行、探索的テストなどを担当しています

前職でのQA活動

ユニファ入社前は、ファッションECサイトを運営する会社で、主にコーディネートアプリやサイトのQAとして働いていました。

仕様書やデザインはほぼ確定していて、それをテストベースにテスト設計を行い、テストケース作成、テスト実行、不具合があれば報告、 修正されたものがあれば修正確認テストと、いうのが基本的な流れです。

開発とQAは拠点が違うため、基本的にやり取りはslack。

開発者さんは皆いい人ばかりで、定期的にMTGで顔を合わせてはいましたが、 サービスについてQAから気軽にアイディアを出したり、ということはあまりありませんでした。 (これは、QAとしてもっと頑張ればよかったなーという心残りもあります。)

ユニファでのQA活動

ユニファに入社後、ルクミーフォトのリニューアルプロジェクト(園管理画面)にアサインされました。 プロジェクトではスクラム開発を取り入れており、QAもテスト自体はスクラムの外で行いましたが スクラムイベントには参加していました。 私が「ものづくりにかかわれたなぁ」と感じたのは、次の3つです。

思い出① QA視点でデザインルールを変更してもらえた

f:id:unifa_tech:20191217222450p:plain
へっぽこデザインイメージ

ルクミーフォトの園管理画面では、ある情報を登録するためのコードを発行する画面があります。 いくつかの項目を選択し、「発行する」というボタンをクリックすると、上のような印刷画面に遷移するためのウィンドウがでます。 当初のデザインルールでは、上のようなウィンドウが表示された場合

  • ウィンドウ内の右上の✕ボタンをクリック
  • ウィンドウ外のグレー背景をクリック

すると、ウィンドウそのものを閉じるルールでした。

スプリントレビュー(ユニファではおさわり会と呼んでます。そこはかと漂うギリギリ感がすてき)で公開後、私が触ってみて感じたことは「グレー背景のとこ、うっかり結構クリックしちゃう」でした。

一応、仕様をしっているQAの私ですらうっかり結構クリック、そして閉じられるウィンドウ、すると項目数は少ないけどもう再度情報入力→クリックという作業を、園の先生に強いてしまう…

これ、ちょっと気になるなぁ…と、おずおず聞いてみたところ、なんとその場で「そうだねーたしかにー」となり、以降

ウィンドウ外のグレー背景をクリック

は撤廃されました。

え?

いいの?

まだ入社1か月の新参者で、みんながここまで作り上げてきた背景とか知らないんだけど、いいの?

とびっくりしたのを覚えています。

思い出② QAが開発者さんにアイディアだしたら採用された

f:id:unifa_tech:20191217222411p:plain
へっぽこ画面イメージ

ある日のデイリースクラム、開発者さんがちょっぴりお悩みをかかえていました。

ルクミーフォトの園管理画面では、ある設定①を登録する時にオプションを選ぶと、区分がちがう設定②も同時に登録することができます。

設定①と設定②は、区分が違う以外はほぼ同じ情報として扱われます。

開発者さんが悩んでいたのは、設定①と設定②を編集する時。DBの都合上、設定①と設定②が連動している構成にはなっていないが、片方を編集した時、もう片方にも反映させたいのです。

プロダクトオーナー、スクラムマスター、開発者さん、デザイナーさん、QA全員でしばしうんうん悩みました。

みんなが悩んでいるので、私も素人ながら頭をぞうきんしぼるような気持ちで考えました。

そして、ふと「登録が同じタイミングなので、IDは連番になるはず。設定①を編集時に、設定①の前後のIDかつ設定①の編集前の各項目で検索かけて、同じものがヒットしたらそれは設定②なので、同時に更新してはどうですか?」とあきれられちゃうかなーと思いながら、話してみました。

結果、開発者さんの方でこのアイディアを元に、もう少し手を加えて実装に採用してもらえることになりました。

採用決定時の開発者さんのおほめの言葉が、すごくつぼなのできいてください(笑)

「一番実現可能な小手先でどうにかする方法だった」

思い出③ QAが仕様書つくってステークホルダーあつめて仕様確定までしちゃったよ

ルクミーフォトのリニューアルプロジェクト、実装完了のバックログがたまってきて、QAもテストで忙しくなってきた2019夏。

新しく入社したテストマネジメントに強いチームメンバー(以下、野生のクマと呼称します)と私は少し困っていました。

なぜなら、リニューアルプロジェクトのテスト範囲のあるアプリの仕様書ができておらず、テスト設計に入れなかったからです。

リニューアルプロジェクトでは、スクラムマスター(ディレクター)が仕様書やデザインワイヤーを作成し、プロダクトオーナーや開発者さんと仕様を詰めるのですが

この時、スクラムマスター(ディレクター)は多忙で 、なかなかアプリの仕様書まで手が回っていない状況でした。

以前の私なら、「忙しいならしょうがない。できるまで待とう」と思っていましたが、この時はWACATE(詳しくはこちら)参加後でやる気が加速モードだったので

「ならQAで作っちゃおう!」となって、

  • 現行のアプリの仕様
  • リニューアル後のデザイン(ペイント)
  • リニューアル後の仕様
  • 未検討事項

などをまとめた仕様書を作ってしまいました。

f:id:unifa_tech:20191217222322p:plain
へっぽこ仕様書イメージ

ついでに、スクラムマスター、プロダクトオーナー、デザイナーさん、アプリの開発者さん、サーバサイドの開発者さんを集めて仕様確定MTGもセットして、そのMTGで仕様ほぼ確定させてもらえました。

まとめ

以上、私がユニファに入って「ものづくり」に関われたなぁと感じた思い出3つでした。

ユニファのQAって、品質をあげるための活動は何でもできるところが魅力だなと思います。

ながながとおつきあい頂き、ありがとうございました。

おまけ

ユニファのQA、もしくはユニファに興味のある方は、こんなイベントもやってます! ご興味あるかたはチェックしてみてください!

SwiftUIを使ってみた

こんにちは。iOSエンジニアのキムです。

今回はSwiftUIを使ってみた感想を書きたいと思います。

Xcode - SwiftUI - Apple Developer

SwiftUIが発表されてからしばらく経ちました。最初はすごい革新的で今までStoryboardやXibなどで開発していた時に抱えていた課題(XML分かりづらい、コンフリクト起きやすいのでチーム開発が大変など)を解決してくれるのではないかと思いましたが、新しい文法や技術に関する情報も少なくてなかなか手が出せませんでした。

しかし、今ではかなり情報も増えて、チュートリアルなどもたくさんあるので、私も軽く触ってみることにしました。

準備

SwiftUIを使うためには、macOSは10.15以上でXcode 11以上が必要です。macOS 10.14以下でも開発はできますが、Preview Canvas機能が使えないなど、SwiftUIが完璧にサポートされてないので、10.15以上がおすすめです。

処理内容

今回はSwiftUIのListを作ってみました。既存のUIKitのTableViewのようなものです。おそらくListは一番よく使われるのではないかと思います。 まずはリストで表示するモデルとテストデータを用意します。 

struct RainbowColor: Identifiable {
    var id: String = UUID().uuidString
    let name: String
    let color: Color
    
    init(name: String, color: Color) {
        self.name = name
        self.color = color
    }
}

struct RainbowColorList: Identifiable {
    var id: String = UUID().uuidString
    var rainbowColor: RainbowColor
    var description: String
    
    init(rainbowColor: RainbowColor, description: String) {
        self.rainbowColor = rainbowColor
        self.description = description
    }
}

// テストデータ
struct TestData {
    static let redColor = RainbowColor(name: "Red", color: Color.red)
    static let orangeColor = RainbowColor(name: "Orange", color: Color.orange)
    static let yellowColor = RainbowColor(name: "Yellow", color: Color.yellow)

    static func lists() -> [RainbowColorList] {
        let item1 = RainbowColorList(rainbowColor: redColor, description: "Red Color \nSome description space")
        let item2 = RainbowColorList(rainbowColor: orangeColor, description: "Orange Color \nSome description space")
        let item3 = RainbowColorList(rainbowColor: yellowColor, description: "Yellow Color \nSome description space")
        return [item1, item2, item3]
    }
}

次は、UITableViewCellに当たるRowを作成します。

struct ColorRow: View {
    
    let list: RainbowColorList
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8){
            VStack(alignment: .leading) {
                HStack(spacing: 10) {
                    RoundColorView(color: list.rainbowColor.color, size: 50)
                    Text(list.rainbowColor.name).font(.headline)
                }
                Text(list.description).lineLimit(5).font(.body)
            }
            .padding(.leading, 32)
            .padding(.trailing, 32)
        }
        .padding(.top, 16)
        .padding(.bottom, 16)
    }
}

最後に上記で作ったものを一覧で表示するList画面です。

struct ContentView: View {
    let lists = TestData.lists()
    var body: some View {
        NavigationView {
            List {
                ForEach(lists) { list in
                    ColorRow(list: list)
                }
            }
            .padding(.leading, -20)
            .padding(.leading, -20)
            .navigationBarTitle(Text("Color List"))
        }
    }
}

イメージ

f:id:unifa_tech:20191204145737p:plain
List

感想

既存のUITableViewと比べるとかなりシンプルに書けました。 今までの開発ではStoryboardにUIViewControllerを作ってUITableViewを設置してUITableViewCellを設置してAutoLayoutの設定をする作業などが必要でした。また、SwiftファイルではUITableViewのDelegateを宣言したり、StoryboardのオブジェクトとのOutletの設定をするなど、色々と手間がかかりました。 SwiftUIではこれらが簡単に設定できてコーディングもシンプルになっているので開発にかかる時間も短縮されるのではないかと思いました。 ただ、UIをコードで作成するのでUIが複雑になる場合はコード全体を理解するまで時間もかかりそうだなと思いました。

最後に

今すぐSwiftUIが既存のUIKitを代替できるとは言えませんが、これからSwiftUIの進化が楽しみです。

Ruby Bizグランプリ2019!

こんにちは。ユニファのエンジニアの田渕です。

このあいだの休日、優雅に家で読書をしていたところ何やら水音が……。 嫌な予感がしてそっと洗面所を覗いてみたところ、我が家のドラム式洗濯機が前面ドア下方からダバダバと水を吐いているではありませんか! 慌てて洗濯機の運転を止めましたが、あたり一面びしゃびしゃ。 せっせと雑巾で拭き掃除をし、洗濯機の修理依頼をし、なんとも慌ただしい週末でした。 (ちなみにドアについてるパッキンが割れてました。。。) 思いがけず早めの洗面所大掃除ができたことだけが救いです。。。

そんなことが起こる数日前、私は帝国ホテルにてRuby Bizグランプリの表彰式に出席しておりました! 本日はその時の模様をお届けします。

Ruby Bizグランプリって?

公式サイトでの案内はこちらのようになっています。

ビジネスの領域においてプログラム言語 Ruby の特徴を活かして、新たなサービスを創造し世界へ発信している企業、団体及び個人を対象としたグランプリです。

rubybiz.jp

今年は午睡!

「今年は」と書いたのは、実は以前にPhotoでも応募させて頂いたことがあるためです。 その時も、ブログのネタとしてこちらで書かせて頂きました。

tech.unifa-e.com

前回は特別賞を頂き、大変嬉しかった記憶があります。 今回は午睡チェックサービスでグランプリが取れたらいいな!ということで、応募期限ギリギリで応募書類を提出。(事務局のみなさま、申し訳ありません。。。) 今年は33のサービスが応募したということでした。 後日、表彰式への招待=ノミネートされたことの連絡を頂き、「もしかして今年こそ!?」という期待を胸に会場へ。 しかし今回の私は、のほほんとしている訳にはいきませんでした。 なぜなら、私が登壇することになっていたからです……。。。

いざ会場へ。

会場の帝国ホテルは、実は現在のオフィスから徒歩数分の距離。 偶然にも暖かい日だったので、とても助かりました。

f:id:unifa_tech:20191216182009p:plain
会場入り口にある紹介用のボード
今回も普段はお会い出来ないような方々がいらしていて、それだけでどきどきです。 私は登壇者でしたので、登壇者用の控室がありまして、そこでコーヒーを頂きつつ時間まで待っていました。 控室では和やかにお話しされる方、プレゼンの練習をされている方、など色々いらっしゃいまして、嫌が応にも緊張感が高まります。

念願は叶うのか。

オープニングセレモニー、色々な方々のご挨拶に続き、表彰の時間がやってきました。 Ruby Bizグランプリは最終ノミネート10サービスのうち、大賞2本、特別賞3本、それ以外の賞が5本という構成です。 大賞が一番最後の発表なのですが、当日のこの発表の時間まで本当に、自分のところが何を受賞したのか知らされていないので、いつ呼ばれるかとどきどきしながら待ちます。

結果は……

f:id:unifa_tech:20191216184054p:plain
特別賞を頂きました!
慣れないパンプスでワタワタしながら壇上へ。
f:id:unifa_tech:20191216184456p:plain
緊張で顔がこわばっております。

受賞された他の会社さんがみなさま、とても素晴らしいコメントをされる中、私は「実は今年はグランプリが欲しいと思ってました。」と素直すぎるコメントをし(すみません。。。)、合わせてその場でまた別のプロダクトで今度こそはグランプリを取れるように頑張ろうと思います!と言うお話をさせて頂きました。

しかしながら、今回大賞を取られた2プロダクトのお話を受賞者プレゼンで拝聴し、その内容の素晴らしさ(Rubyを利用したことの意義や、Rubyの特徴をよりよく活用しているなど)に納得してオフィスに戻りました。

実はもっと話したいことがあった。

実は、コメントの内容はいくつか考えていたのですが、最終的に持ち時間を考えて削った内容がありました。 せっかくなので、その一部をこちらで書こうと思います。

このサービスの構想自体は2016年の年末頃には既に存在しており、前回のフォトでの受賞の折のプレゼンテーション資料の中にも今後の展望の一つとして記載をされていました。そこから企画や構想を一年ほど行い、開発を三、四ヶ月、プロトタイプを利用した数ヶ月のフィジビリティの中での微調整を重ね、ようやく正式なサービスを開始したのが2018年の4月という構想開始からの期間の比較的長いプロダクトでした。 そのような背景もあって、特にサービスの初期開発に携わったメンバーにとっては大変に思い入れも深く、同時にたくさんの新しい経験と苦労をしたプロダクトです。 企画、デザイン、テスト工程を含む開発のみならず、売り方やご利用開始の後のサポート、フォローの仕方など、どれひとつとってもそれまでのPhotoサービスとは異なり、試行錯誤を重ねながら今日までやって参りました。 2018年4月に開始し、現在では数千の園でご利用いただくまでに成長出来たことは大変に喜ばしく、また身が引き締まる想いでおります。

まとめ

  • 時々しかお外に出ませんが、こうして表彰して頂けると言うのはひとつの区切りとしてとても良いなと思いました。 (なかなか難しい午睡と言うプロダクトの企画、デザイン、ディレクション、開発、テストに携わってくださっているみなさま、本当にお疲れ様です。)

  • 今後もより一層、Rubyの発展に尽力すると共に、新しい技術を取り入れながらまたいつか表彰式に呼んでいただけるように頑張ろうと思います。

Adobe MAX Japan 2019

デザイナーの三好です。

先日パシフィコ横浜で行われたハイパーテンションウルトラクリエイティブイベント「Adobe MAX Japan 2019」に参加してきましたのでレポートを書きたいと思います。

Adobe MAX Japanについて

f:id:unifa_tech:20191206111907j:plain

https://maxjapan.adobe.com/

Adobe社主催で年に一度アメリカ、ヨーロッパ、日本で行われる世界最大級のクリエイティブイベントです。 Adobeソフト最新機能の発表や活用方法の解説、というのが名目となっているようですが、トップクリエイター達のセッションの他にもワークショップが行われていたり、グッズ販売、クレープやケバブの出店、Beer Bash、風船を片手に彷徨うおじさん達(スタッフ)、Adobeロゴがくまなく印刷された全身タイツの男がうろついていたりする狂乱のお祭りでした。

デザイナー経験の日が浅い私でさえ高鳴りましたので、Adobe信者と呼ばれるクリエイターの方々のその心中たるや、さぞクリスマスの朝を迎える寝不足の子どものような気持ちだったことでしょう。今年は過去最高の5000人以上が動員したようです。

クリエイティブの未来像

まず驚いたのがAdobe製品の膨大な数と進化のスピードです。特に私はまだひよっこで日常的に使えるソフトが限られているので、恥ずかしながら会場で初めて触れる製品がたくさんありました。

デジタルで完ぺきな水彩画表現を可能とするAdobe Fresco、直感的に扱える3DグラフィックデザインソフトAdobe Dimension、フォトグラファーとして活動する私としては強い関心のある最新アプリPhotoshop Camera。これらの超未来的ソフトウェア群がアドビAI(人工知能)Adobe Senseiの大活躍により、従来のPhotoshopのような専門的な知識と経験を必要としなくてもわりと気軽に扱えることができるというのが凄い。

2020年にリリースされるPhotoshop Cameraに関しては、スマートフォンアプリとしての提供ということから、プロ向きではなく一般的なSNSユーザーをターゲットにしてることがわかります。当然他のカメラアプリとは一線を画す超高度なアーティスティックな写真が誰でも撮れるようになるでしょう。

自身の立場で考えると「このままではいつかAdobe Senseiに仕事奪われちゃうのでは…」と不安がよぎりそうですがそこはクリエイターの味方Adobe。「Adobe Senseiは人間の仕事を奪うものではない。助けるものだ」と偉い人がおっしゃっているように、あくまでユーザーがこれまで以上にクリエイティブに専念できる環境を作る為の超優秀なアシスタントという役割だそうです。

アシスタントに負けないよう吐血しながら日々勉強あるのみですね。

f:id:unifa_tech:20191206113547j:plain

まとめ

まるで大きなクラブのような(会場内の暗さとライティングの派手さ然り)雰囲気で本家がロサンゼルスであることも納得のバブリーなイベントで、Adobeのスローガンである ”Creativity for all” の精神を至るところで触れることができとても良い刺激になりました。

そしてこれは技術職と呼ばれるもの人全てに共通することかもしれませんが、この仕事を続けていくには瞬きをする間に進化していく業界に常に笑って食い下がっていかなければならないということを再認識しました。

笑えるくらい愛せること、永遠に学ぶことを恐れないというのがクリエイティブに携わる絶対条件で、その覚悟を改めて感じることができたのが1番の収穫です。

f:id:unifa_tech:20191206113528j:plain

Vapor. Sharing the code between an iOS app and... the server?

By Vyacheslav Vorona, iOS developer at UniFa.

Being an iOS developer, have you ever been in a situation when you were thinking: "Alright, I want to write this kind of app. Now!" But then you instantly realize that your pet-project would also need some kind of backend and you are too busy/lazy to learn all that "server stuff"... so your enthusiasm disappears as quickly as it popped up five minutes ago. Happens to me all the time. 😅

Luckily, there are technology choices allowing you to write your backend code using the language you are used to. I looked through some of them and decided to give Vapor a try. IMHO, it is risky to use Vapor in any big and serious project right now, but for hobby programming... it is perfect. Today I am going to take a closer look at it and I hope you will join me (don't forget tea and cookies ☕️).

What are the goodies?

  • You use your favorite Swift for coding
  • You are doing it in your favorite Xcode with all it's debugging tools available
  • Cmd + R and your server is up locally
  • Type checking during the compilation
  • An ability to have code/models shared between your iOS app and the backend

The last point is especially intriguing and provides some opportunities you wouldn't have with a backend written with any commonly used server-side programming language. Just imagine: you change your server code and an iOS app is already aware of it. Depending on the case your changes are going to be handled automatically (and you may not even need to change anything in your client code) or your app's compilation is going to fail letting you know something is wrong. Sounds like an attractive opportunity to me. Unfortunately, there is almost no example code implementing such solutions to be found on the Internet. So the only way we have is to try to implement it by ourselves, which is what we are going to do today.

Sounds cool! What do I need?

  • Vapor requires you to have Xcode 9.3+ and Swift 4.1+ (for this particular project we will need Swift 5.1 though)
  • We will also need Homebrew to install Vapor

Optionally: I am going to use Postman client to test API calls, but you may use any tool you like (or not use any at all).

That's it. Let's dive into it!

Setting up the projects

Alright, first things first, since we are going to have two projects sharing some code, we need to create a workspace in Xcode. I am going to call it SchrodingerApp.workspace. Got it? Schrodinger's cat is both dead and alive at the same time, and our code will be in the iOS app and the server at once... ah, never mind. 🙄

Next, let's download and install Vapor. Let me copy and paste some console commands from it's 'Hello, world' page for you...

Installing Vapor using Homebrew:

brew tap vapor/tap
brew install vapor/tap/vapor

Easy, right? Then I will run the command below in the same folder where I've put my workspace file.

vapor new SchrodingerAppServer

It will create a new folder with the name we entered, but if you look inside, you will notice it doesn't have an .xcodeproj file we are used to. Don't worry, Vapor can generate it for you:

cd SchrodingerAppServer
vapor xcode

When everything is done, it will suggest you to open Xcode, but we still need to put our new project into the workspace, so let's refuse for now. Open SchrodingerApp.workspace in Xcode and just drag-and-drop SchrodingerAppServer.xcodeproj from Finder to Xcode.

You may notice that now you have a scheme called 'Run'. Also, make sure you have your Mac selected as a device to build. Go on and run it! (just Cmd + R, as promised)

f:id:unifa_tech:20191125183937p:plain

In the Xcode console we see the message which says that our brand new server is up and waiting.

Server starting on http://localhost:8080

Since iOS Simulator uses the same network as the Mac it's beeing ran at, I will use the simulator to check the localhost. So, does our server work?

f:id:unifa_tech:20191125184059p:plain

Great!

We will take a closer look at the contents of our server app in a moment. But first, let's set up our client app to finish the boring part.

I am creating a new Single View iOS project named SchrodingerAppClient and pulling it's .xcodeproj file into our workspace. Make sure you have two schemes: one for the server and one for the client.

For the next step let's designate a place to put our shared code to.

Sharing is Caring

I consider the server app as a primary one, which means the actual files containing the shared code are going to be stored inside of it's project.

To do so I'm going to create a folder called SharedAPI ("New Group" in the Xcode) in SchrodingerAppServer/Sources/App. It will contain the code common for both of our apps.

Now I will go to the client project and add a "New Group without Folder" named SharedAPI into SchrodingerAppClient. This group is going to hold a reference to the SharedAPI folder we previously created for the server app. To add the reference select the newly created group, in the "File Inspector" click a tiny folder icon and search for a SharedAPI folder in the SchrodingerAppServer.

f:id:unifa_tech:20191125185426p:plain

Finally, we are ready to write some code! Yay! 🚀

Schrodinger's Model

I feel like our SchrodingerApp is missing something... A Cat data model, of course! I am adding a Cat.swift file into the SchrodingerAppServer/Sources/App/SharedAPI and here is a code for it:

final class Cat {
    var id: Int?
    var name: String
    var age: Int
    private var breedString: String

    var breed: Breed? {
        return Breed(rawValue: breedString)
    }

    init(id: Int? = nil, name: String, age: Int = 0, breed: Breed) {
        self.id = id
        self.name = name
        self.age = age
        self.breedString = breed.rawValue
    }
}

enum  Breed: String {
    case  persian = "persian"
    case  russianBlue = "russian blue"
    case  britishShorthair = "british shorthair"
}

I guess it is pretty straightforward for now, just a couple of things to note:

  • var id: Int? is in fact a requirement of the SQLiteModel protocol which I'm going to talk about in a second
  • I've declared private var breedString: String to see how Vapor is handling private properties

Getting Fluent

Vapor provides an Object-relational mapping framework called Fluent to work with databases. It supports several database drivers:

  • PostgreSQL
  • MySQL
  • SQLite
  • MongoDB

For our project, I decided to try out SQLite as it is simple and is just nice for prototyping.

To make Cat a full-fledged server-side model we have to adopt several protocols:

  • SQLiteModel. Allows your class to represent a table in an SQLite database. Its only requirement is to have var id: Int? which we already implemented in our model.
  • Migration. Allows us to perform dynamic migrations of our model (or revert them).
  • Content. Makes a model convertible to content of an HTTP message.
  • Parameter. Makes a model capable of being used as a Restful route parameter.

Latter three protocols do not have any requirements by default, we just need to declare the conformance.

But here is a thing: to do so, we need to import Vapor and import FluentSQLite which we obviously can't do on the iOS side. To make Cat model accessible from both server and client code, I'm going to use a small trick added in Swift 5.1. - canImport(). Add the code below right after the enum Breed in Cat.swift:

#if  canImport(Vapor) && canImport(FluentSQLite)

import FluentSQLite
import Vapor

extension Cat: SQLiteModel {}
extension Cat: Migration {}
extension Cat: Content {}
extension Cat: Parameter {}

#endif

This way Cat will conform to these four protocols only in case when Vapor and FluentSQLite are able to be imported. This means our data model is going to be treated differently: as an SQLite model on the server-side and just as a simple object on the client-side.

To make sure everything works I'm going to add a reference to Cat.swift into the SchrodingerAppClient and try running both schemas ("Run" for the server and "SchrodingerAppClient" for the client). If everything is set up correctly, both should compile successfully.

Two things left to do:

  • Add the Cat model to a MigrationConfig which is a Fluent structure managing database migrations in Fluent. To do it I will add the following line into the configure function located in configure.swift, right before services.register(migrations).
migrations.add(model: Cat.self, database: .sqlite)
  • (Optional) Right now the project we created doesn't persist any data between two launches of the server. To make it do so, we need to replace the this line in configure.swift
let sqlite = try SQLiteDatabase(storage: .memory)

with this

let sqlite = try SQLiteDatabase(storage: .file(path: "db.sqlite"))

So that the database is stored in a file.

We are done with the model. Now let's teach our apps to do something with it.

POSTing Cats

Server

In this project, we will implement an endpoint for our iOS app to POST new Cat models to the server via REST API. With Vapor it is really simple. Just find a file named routes.swift and add the code below to the bottom of the routes function located there:

router.post("api", "cat") { request -> Future<Cat> in
    return try request.content.decode(Cat.self).flatMap(to: Cat.self) { cat in
        return cat.save(on: request)
    }
}

Let's take a closer look at this code and see what it actually does.

router.post("api", "cat") registers a new route at the path /api/cat using the POST method. When a request is received, it's JSON gets decoded into Cat using Content protocol. Since decode(_:) returns a Future we need flatMap(to:) to unwrap a Cat when it is decoded. Then a Cat gets saved into the database using Fluent.

Now I`m going to run the server app and try out our new endpoint using Postman:

  • The URL is: http://localhost:8080/api/cat
  • My request's JSON is:
{
    "name": "Murka",
    "age": 5,
    "breedString": "russian blue"
}
  • Make sure to set Content-Type header to application/json

And here is the server response:

{
    "age": 5,
    "id": 1,
    "breedString": "russian blue",
    "name": "Murka"
}

As you can the breedString was handled properly (we made it private in the model, remember?). Also, note that Fluent have set an id for our freshly POSTed Cat.

Client

To POST some Cats from the iOS app I'm adding the method below right to the default ViewController.swift in SchrodingerAppClient:

private func post(_ cat: Cat) {
    let jsonData = try? JSONSerialization.data(withJSONObject: cat.json)
    var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!)
    request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
    request.httpMethod = "POST"
    request.httpBody = jsonData

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data, error == nil else { return }
        let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
        if let responseJSON = responseJSON as? [String: Any] {
            print(responseJSON)
        }
    }
    task.resume()
}

I will not describe what's going on here in detail as I believe you understand it if you are reading this article. 😉 Just a couple of things:

  • Note that I am setting application/json as a value for Content-Type
  • Cat.json is just a simple mapping I've added to the model. It is a computed property of type [String: Any] representing a Cat instance to use in an API call

GETing Cats

Server

Next, I am going to add a GET endpoint which is going to be called by an iOS app to present a list of the Cats we've put into our Schrodinger's box. Again, with Vapor it us quite straightforward:

router.get("api", "cat") { request -> Future<[Cat]> in
    return Cat.query(on: request).all()
}

When the server receives a GET request at the /api/cat, it just queries all the Cats from the database and provides them to the client.

Testing new endpoint using Postman, here is the response:

[
    {
        "age": 5,
        "id": 1,
        "breedString": "russian blue",
        "name": "Murka"
    }
]

As you can see, we received an array containing one element - the Cat we added using POST endpoint.

Client

Here is some ugly code from the client-side to get a list of the cats for presentation. I've added it as a separate method into the ViewController.swift:

private func getCats() {
    var request = URLRequest(url: URL(string: "http://localhost:8080/api/cat")!)
    request.httpMethod = "GET"
    let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error **in**
        guard let data = data, error == nil else { return }
        let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
        if let responseJSON = responseJSON as? [[String: Any]] {
            self?.cats = []
            for catJson in responseJSON {
                guard let cat = Cat.make(from: catJson) else { continue }
                self?.cats.append(cat)
            }
        }
    }
    task.resume()
}

The only thing worth explaining here is Cat.make(from:) method. It is just a static function decorating the initialization of Cat and able to return nil if some necessary data is missing.

Final preparations

User Interface I am adding some simple UI to test the features we implemented:

  • Three UITextFields to add new Cats via POST (yeah, there should be pickers or something similar, but this project not going to the AppStore, alright? 😅)
  • UITableView to display the Cats the client recieves via GET

Info.plist Another important thing to do is to modify Info.plist in SchrodingerAppClient as follows:

f:id:unifa_tech:20191125185650p:plain

We need to set NSAllowsArbitraryLoads key under the NSAppTransportSecurity dictionary to YES. Otherwise your iOS app will not be able to connect to the localhost.

Note that this is an anti-pattern and you should never do it in a real project. Apple is strongly against doing it and your app may be rejected from the AppStore.

But it's ok now, since we are just playing around. 😎

Now let's run our app and look at the results, even though they don't look as exciting as some mechanisms we implemented under the hood:

We can input some data for the new Cats, POST it... f:id:unifa_tech:20191125185756p:plain

... and then GET the Cats to see all of them in the list: f:id:unifa_tech:20191125185806p:plain

And all of it is done using a single Cat data model! 🎉

Conclusion

Let's be honest, I doubt Vapor (and backend Swift overall) is going to become a somewhat serious competitor for all those languages used for server-side development any soon. Yes, Swift is incredibly fast, has strict types and a bunch of other advantages, but it needs something more than just support from its community (even though they are awesome guys) to become a "real thing" .

BUT! Does it mean you shouldn't use it for your cute little pet-project you are writing every weekend with a beer in your hand? No! You definitely should. Finally, you can write everything from A to Z without leaving your comfort zone.

Besides, today we've learned a bit about some opportunities which mobile platform developers don't usually have. With Vapor you don't have to feel that you are writing bad code just to fit some weird architecture on the server-side. Your backend code influences your iOS app directly. And if you need to add something to the client, the server code is already aware of it. It's a great feeling, especially if you don't have much experience with backend.

I hope I could inspire you to give Vapor a try.

Happy coding! 😉

Project repo (make sure to check README)

iPodカメラによる顔検出

はじめに

iOSエンジニアをやっていますわたなべと申します。 現在私は、フォト事業部の方で自動撮影アプリというアプリの開発に携わっています。 自動撮影アプリというのは、保育園で保育士さんがBluetoothカメラとiPodを使って園児の様子を自動で撮影するアプリになります。いろいろと課題があるので、下記のようなことを考えてみました。

やりたい事

iPodには、カメラが付いているので、そのカメラを使って撮影できないか? という訳で、iPodの背面カメラを使用し、カメラ内に顔が検出できたら、数秒間、動画データから全てのフレームを画像として保存する。ということをやりたいと思います。

カメラを使用する

今まで、カメラを使用したアプリを作った事がなかったので、ここから軽く説明していこうと思います。 カメラを使用するにはAVFoundationをインポートします。 そして、入力と出力を管理するAVCaptureSessionを生成します。 まず、入力はAVCaptureDeviceを用いて、使用するカメラを設定します。(今回は背面カメラを使用して、フレームレートを1/30秒に設定しています。) デバイスの設定が終わったら、これを入力デバイスとしてAVCaptureSessionに追加します。 続いて、出力です。出力は、カメラの入力を動画データとして扱いたいので、AVCaptureVideoDataOutputを使用します。 AVCaptureVideoDataOutputでは、ビデオ出力などを設定し、setSampleBufferDelegate(〜)を設定すると AVCaptureVideoDataOutputSampleBufferDelegateプロトコルで、メソッドcaptureOutput(〜)がフレーム毎に呼ばれますので、ここで顔検出を行います。(1フレームづつ画像に変換して顔検出を行います。) こちらも設定が終わったらAVCaptureSessionに追加します。

そして、キャプチャした映像をプレビュー表示するために、AVCaptureVideoPreviewLayerを設定して、

キャプチャースタートです!

最後に、Info.plistにカメラを使用する事を忘れずに…

顔検出方法

顔の検出方法は、iOS11から搭載されているVisionフレームワークを使用します。

Vision.フレームワークでは、いろいろな検出ができるので、 まず、どのような検出を行うかを要求して、(Request) 何に対して、検出処理をするか指定して実行、(RequestHandler) そして、検出結果を処理します。(Observation)

そこで、Visionで顔検出を行う際に重要になってくるクラスがあります。 今回は、顔を検出したいので、下記のクラスを使用しています。

役割 クラス 説明
Request VNDetectFaceRectanglesRequest 顔検出を要求します。
RequestHandler VNImageRequestHandler 上記のRequestに対する検出処理を実行するクラスです。
Observation VNFaceObservation Requestに対する検出結果を処理をするクラスです。

下記が実際のコードになります。(顔検出部分を抜粋)

 1: let faceRectanglesRequest = VNDetectFaceRectanglesRequest(completionHandler: { (request, error) in
 2:     for observation in request.results as! [VNFaceObservation] {
 3:         /// observationには検出した顔の矩形が入ってくる
 4:         /// 検出できた顔の部分に矩形などを表示する処理を行う
 5:     }
 6: })
 7:
 8: if let cgImage = image.cgImage {
 9:     let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
10:     try? handler.perform([faceRectanglesRequest])
11: }
  1. 1行目でどのようなRequestを要求するか指定しRequestを作成します。今回は、顔検出なので、VNDetectFaceRectanglesRequestを使います。
  2. ちょっと飛んで9行目から説明します。検出したい画像を指定しVNImageRequestHandlerを作成します。  ここでの解析したい画像は後ほど説明します。
  3. 10行目でVNDetectFaceRectanglesRequestで生成した要求に対して顔検出を開始します。
  4. 検出した結果が、2行目のrequest.resultsに入ってきます。

上記の処理を「カメラを使用する」でも説明したcaptureOutput(〜)内で処理を行っています。 captureOutput(〜)の最初で1フレームづつ画像に変換し8,9行目で使用しているimageがそれになり、検出を実行しています。

最後に

当初の目的の顔検出ができた時、そのあと数秒間のフレームを画像に書き出すという処理を追加しているので、目的は達成できたかとは思うのですが、まだまだ、改良の余地ありですね。

今回は、駆け足で説明してしまったので細かい所、顔を検出した所に枠を出すとか、検出した結果の顔の座標は反転しているとか、いろいろと注意しなければいけない点が多々ありますが、今後の課題としていろいろと試していければと思っています。

2019年のプロジェクトをふりかえる 〜そのとき何があったのか、何をしたのか〜

スクラムマスターの渡部です!

はじめに、この記事はUniFa Advent Calendar 2019の記事でもあり、ふりかえり Advent Calendar 2019の記事でもあります。

今回の記事では、12月ということもありますので、この1年で関わったとあるプロジェクトについてふりかえってみたいと思います。

目次
  • はじめに
  • 走り出しフェーズ
  • 予測し難いことこの上ないぞ!?フェーズ
  • 徐々に安定してきた!チームをもっと強くするぞ!フェーズ
  • 一気にベロシティ低下したぞ!フェーズ
  • ラストスパートフェーズ
  • 2020年に向けて一言
  • さいごに

はじめに

ふりかえるにあたって、対象のプロジェクトの全体像、説明で用いるグラフの見かたを軽くご紹介させていただきます。

※ 記載されている数値は全て実際のものです。

バーンダウンチャート

f:id:unifa_tech:20191206163513p:plain

<グラフの見かた>

  • 赤い線:開始日から理想の完了日まで、均等にタスクを消化した場合の残りポイントの推移
  • 青い線:実際に消化されたポイントを差し引いた残りポイントの推移

チームでは相対見積もり(ストーリーポイント)を採用しています。

一見すると、比較的安定した下降線を描いているように見えますが、要注意なのは、トータルの見積もりが500ポイントある(つまり、傾きの変化が僅かに見えても、実際はけっこう差があるぞ)ということです。

そこで、スプリントごとにどれだけのポイントを消化できたかを比較できるグラフも用意していますので、合わせて確認していければと思います。

ベロシティの推移

f:id:unifa_tech:20191206163550p:plain

<グラフの見かた>

  • 赤い線:プロジェクト遂行とは直接は関係ない、調査 / 不具合 / 採用などの作業のポイント(以降、プロジェクト外作業と呼称)*1
  • 青い線:プロジェクト遂行に関わる全ての作業のポイント(以降、プロジェクト内作業と呼称)

おや?先ほどの印象とは打って変わって、かなりファンキーな印象を受けますね。

ではさっそく、フェーズごとにふりかえっていきたいと思います。 (おこったこと一つ一つに深堀りしていくと薄い本が出せる気がするので、今回は深堀りません)

走り出しフェーズ

f:id:unifa_tech:20191206165515p:plainf:id:unifa_tech:20191206165518p:plain

一見、ベロシティは安定して右肩上がりのように見えます。 が、後述しますが、実測値ではないので、このフェーズの数値は強くは意識しないでください。

このフェーズでは(私が関わったのはスプリント4からですが)、何よりもまず優先で、チームの状況を見える化することにフォーカスしました。

やったこと、おこったこと
  • 祝 join!@スプリント4(懐かしい!)
  • 見積もりを取得した(プランニングポーカーの実施)
  • ストーリーポイント見積もりの説明・導入した
  • スプリント毎のベロシティ計測を実施しはじめた
  • バーンダウンチャートを使って、全ての関係者へ状況を見える化した
  • スプリント4以前はベロシティを計測できていなかったので、後日ヒアリングを元に工数を仮置きした(翌スプリント以降のばらつきを考えると、実際計測してたらこんななだらかな線にはならないかも)

予測し難いことこの上ないぞ!?フェーズ

f:id:unifa_tech:20191206165656p:plainf:id:unifa_tech:20191206165659p:plain

バーンダウンチャートでは比較的なだらかに見えますが、ベロシティの推移を見てみると突然の乱高下です。荒ぶっていますね。

このような状況ですと、次のスプリントでどの程度コミットするべきかの予想を立てにくく、不明確なコミットの上に成り立つその先の計画も不明確なものになりかねません。(当時は、昨日の天気*2もブレが大きすぎて参考に出来ない状況でした)

POや関係者とは、「いまの段階で着地の予想は出来なくはないがブレが大きいはずなので、リリース日の決定はまだ待つべきだろう」という話をしていたことを覚えています。

スプリント4まではカイゼンのための土台作りに100%フォーカスしており、上記の状況が明らかになりましたので、スプリント5,6から実際にアクションを実施していきました。

このフェーズでは、透明性の確保にも引き続き勤しみつつも、判明した障害の中でも重要なものから順番に取り除いていました。

やったこと、おこったこと
  • スプリント毎のベロシティが安定しなかった(おや?こいつぁ予測が困難だぞ!?)
  • ありとあらゆるプロジェクト外作業を全てチケット作成し、見積もりを取得しはじめた
  • 結果、プロジェクト外作業が頻繁に発生し、集中できる時間が作れていないことが判明した
  • プロジェクト外作業が発生するプロセスを把握した後、フローを整理して窓口を一本化した(エンジニア直ではなく、一先ず私へくるように)
  • プロジェクト外作業に対して、専任スタッフを設けた(これは非常に効果が高かった)*3
  • 仕様変更/追加が判明したとき、進行中のチケットには含めず、別でチケットを作成することにして、仕様追加/変更によるスケジュールへの影響を可視化した
  • 絶対に必要ではないMTGはキャンセルしてもらい、必要なMTGは時間を朝or夜にずらして、一日の真ん中は可能な限り集中できる時間にした

徐々に安定してきた!チームをもっと強くするぞ!フェーズ

f:id:unifa_tech:20191206165709p:plainf:id:unifa_tech:20191206165712p:plain

チームみんな・関係者全員の協力と、努力の甲斐あって、徐々にベロシティが安定してきました。

ただ、大きな障害は取り除いたものの、まだまだスムーズに作業が進まないことがあります。

最低限の透明性は確保されてきておりましたので、このフェーズでは、日々の作業がもっとスムーズになるように、あれこれと試行錯誤をしていました。

因みに、いまの私のアイデンティティの一部を担っていると言っても過言ではない「ふりかえり」に関心を抱いたのも、このフェーズでのことでした。

やったこと、おこったこと
  • 見積もりが大きいチケットは、だいたい5pt上限で分割するルールにしてみた。結果、チケットごとに何をすべきかが明確になり認識ズレが減り、翌スプリントへの持ち越しも減った
  • 仕様書の記載方法を変更した。具体的には、1画面につきコンフルエンス1ページを用意して仕様は箇条書きで記載し、そのページをみれば、決まっていることの全てが分かる状態にした
  • これまでは実装しながら仕様をFixさせていっていたが、着手するスプリントの前には仕様がFixされている状態にした(前々から準備してはいたが、このタイミングで仕様の整理がようやく追いついた)
  • PO確認待ちで作業が止まったり、複雑な説明をテキスト入力することに時間がかかっていることが判明したので、POを説得してチームの隣に席移動してもらい、いつでも確認相談して良いルールにした(これもかなり助かりました)
  • 仕様に関する質問は、コンフルエンスの該当仕様書の下部に残すルールにした(後から決定の背景を追いやすくて地味に好評。Slackだと流れて後で確認に手間がかかる)
  • アジャイルレトロスペクティブズを参考に、「ふりかえり」の抜本的改革を実施(場の設定 > データの収集 > アイデア探し > アクションの決定 > ふりかえりのふりかえり > ふりかえりの終了 という流れに組み替えた)
  • Twitterで @viva_tweet_x さんを発見、衝撃とともにフォロー & ふりかえり読本を購入
  • ワーキングアグリーメントを定めた。個人的なお気に入りは「作業報告があったら、「Great!!」と行動を称賛する」
  • ふりかえりでBGMを流した。個人的なお気に入りは 楽ジャズ~クラシック / Easy Camel Trio

一気にベロシティ低下したぞ!フェーズ

f:id:unifa_tech:20191206165721p:plainf:id:unifa_tech:20191206165725p:plain

スプリント12からプロジェクト外作業が増え始めてはいましたが、ここで一気にチームのベロシティが低下しました。

が、その後の状況の正確な把握から、PO・関係者含めて検討し、スピーディに対策を打つことができたフェーズでした。

いま思えば、関係者全員の協力による「透明性の確保」、「ふりかえりによるチームの強化・情報の共有の機会」が無ければ、ここまで迅速な対応は出来なかったのではないかなぁと思います。

やったこと、おこったこと
  • QAチームによるテストがスタート
  • テストに関連した質問、バッチの手動実行、データ作成作業などが発生し、ベロシティが急激に低下していたことが判明
  • QAチームとのコミュニケーションを最適化
  • 何でも確認相談できる枠を、朝イチに設けた
  • Bugチケットを簡単に管理するための専用のカンバンをJIRAに用意
  • Bug発見時の確認プロセス、判断のガイドラインを整備し、確認すべきものだけを適切に確認できるようにした
  • 元々の開発チームの作業をよく理解しているエンジニア2名(同じチームではあるが、同時並行で別作業をしてもらっていた)にjoinしてもらう計画をたてた

ラストスパートフェーズ

f:id:unifa_tech:20191206165734p:plainf:id:unifa_tech:20191206165737p:plain

これ以降は、リリースに向けてやる必要があることをとにかくなんでもやっていました。

(テキストに起こすとちょっと恥ずかしい気もしますが…)関係者全員が一致団結しているなぁと強く感じたフェーズでもありました。

やったこと、おこったこと
  • 私、PO、他ビジネスサイド関係者がテストに参加(主に修正確認など)
  • 前フェーズで述べた2名のエンジニアが実際に作業開始
  • POとウィークリー(最後はデイリー)で、Bug優先度チェックを実施。「修正されると嬉しい」レベルのBugは全てPendingした
  • 日々の運用作業も、緊急のものを除き、対応をストップした(関係者の皆様のご理解に感謝)

因みに、上記のような大きな変更を、弊社では「オペレーション One UniFa」と勝手に呼称していました。

そして、開発チーム、QAチーム、PO、デザインチーム、ビジネスメンバー、他関係者全員の協力のお陰で、世の中に価値を届けることができました。

2020年にむけて一言

2019年は、成功失敗、良かったこと悪かったこと問わず、非常にインプットに重きを置いた一年であったように思います。(後半は徐々にアウトプットしはじめましたが)

ですので2020年は、これまで通りインプットしつつも、よりアウトプット(社内外問わず)に重きをおいて活動していく所存です。

ひとまず、社内の「スクラム勉強会」「ふりかえり導入支援」は計画的に実施していきたいと思います。

さいごに

ふりかえってみると、本当に様々なことをやってきたなぁとしみじみ思います。

いま思い返せば「あの時こうしていたら…」という気持ちがよぎらないことも無いですが、そんなときこそノーム・カースの最優先条項を読み上げて次に活かすとし、2019年を気持ちよく終えたいと思います。

私たちが発見したものに関係なく、その時点で知っていたこと、スキルと能力、利用可能なリソース、手元の状況を考えれば、誰もが最高の仕事をしたと理解し、本当に信じています。*4

「いうても自分ら、よく頑張ったさ」と。

それでは引き続き、アドベントカレンダーをお楽しみください!

*1:グラフにどのような数字を含めるべきかについてはこちらの記事で解説していますので、よければご覧ください。tech.unifa-e.com

*2:過去3スプリントのベロシティの平均を、次スプリント時の計画の参考にします。それを「昨日の天気を見る」と言います。

*3:専任スタッフに関しては別記事でも解説しています。tech.unifa-e.com

*4:このテキストは、次の原文をGoogle翻訳したものです。The Prime Directive - Agile Retrospective Resource Wiki