machio Development Diary

思いつきで技術的なことをつらつらと

初期スタートアップにおける社内文化の在るべき形とは ~3ヶ月開発チームをマネジメントしながら本気で考えた道のり~

はじめに

この記事は6人の小さなチームのマネジメントを3ヶ月齧っただけの僕が、それでも外部のメンターさん方に猛烈に相談に乗っていただいたりしながら 社内文化 について試行錯誤した道のりについて綴っています。拙文ですが温かく見守ってくださると幸いです。

背景

簡単に背景を共有させていただきたいと思います。

僕が3ヶ月間エンジニアのマネージャーを経験したこと

僕が所属している 株式会社Flatt のエンジニアチームは

12月まで 1月以降
実働人数 3人 2倍の6人に
社長の関わり方 手も動かしつつマネジメント 現場を離れて社長業に専念
マネジメント層が不在

というように 2017年 => 2018年 という年の変わり目を境に大きく環境が変わりました。

そこで純エンジニアポジションでは1番の古株だった(それが理由かはわかりませんが)僕がエンジニアのマネージャーに抜擢され、この3ヶ月間 6人のエンジニアチームのマネージャーとしてエンジニアの組織全体を見る経験 をしました。

社内文化に対する考え方の変遷

きっかけになったのはマネージャー就任

当初僕は「マネージャーになると言っても人々のタスクを管理してみんなが働きやすい環境を作ればええんかな?」くらいの甘い認識でいました。

ところが始めてみると驚くことに、1日にしなければいけない意思決定の数が大小合わせて今までの10倍くらいになりました

一介の平エンジニアとして働いていた時は、社長から降りてくる指示に従って、目の前の作業を最大限のパフォーマンスで完遂していれば評価されていました。

マネージャーになると『ユーザーのこと(一番大事!)』『リソース』『期日』『メンバーのモチベーション』などの多くのファクターに思いを馳せながら、テンポよくメンバー全員分の意思決定をしていかなければなりません。

しかしいかに小さなチームとはいえ、この意思決定が一過性のものであったり属人的であったりすると組織全体の脆弱性に繋がります。

そこで僕が意思決定の拠り所としたのが 会社の文化やコアバリューでした(当然の流れだと思います)。

(※ 今後便宜のために『正確な』意思決定という言葉を使いますが、これの意図は打ち手として有効かどうかではなく、組織としての軸に沿っている(つまり一過性がなく属人的でない)という意味です)

社内文化は何のために存在するのか?

社内文化やそれに属するマインドに関する記事を見ていると「個人の成長のため」みたいなメンバー側から見た表面的な切り口で自己啓発のような文脈で語られているものが散見されます。

しかし、個人の成長 / 迅速で正確な意思決定 / コミュニケーションの円滑さ など社内文化の浸透による恩恵は様々ですが、それもひっくるめて社内文化の目的は必ず『長期的な組織の利益の最大化』に帰着します(当たり前のことなんですが)。

『長期的な組織の利益(どう定義するかは組織によります)の最大化』が全ての社内文化の上に存在しており、社内文化はその潤滑油にすぎない という事実を常に念頭に置いておかないと、社内文化はすぐに形骸化してしまいます。

社内文化の形骸化とは

端的に言うと 社内文化の言葉だけが一人歩きして、その本質への理解が疎らになり、結果パフォーマンスの低下に繋がる ことを表現しようとしています。

例えば『自責』という文化があった時に、長期的な組織の利益の最大化を考えると他の人に投げた方がいいトラブルシューティング(その問題について考察し、パフォーマンスを最適化するために他の人にアサインした時点で自責)を、『自責』という言葉の言葉尻だけ捉えて全部自分で抱えた場合、それは本来の目的に適っていないので形骸化していると言えると思います。

これが進行してしまう原因は大きく分けて2つあると思います。

組織の拡大に伴う形骸化

これは規模に応じて避けられなくなってくると思います。

初期のメンバーは特に、その組織に対して並々ならない思いがあります。皆が空気を吸うように「長期的な組織の利益の最大化」を念頭に置いて考え、能動的に行動します。

しかし、規模の拡大に伴って組織全体をマクロに捉える見方は薄れていき、だんだんと部署・チーム・個人のようなミクロな視点が蔓延していきます。

これは当然の動きであり、非難する意図は全くありません。全員が全員、前述のような長期的な組織の利益の最大化を第一に考えられる巨大組織の構成は正直不可能だと思います。

結果として生まれる個人がミクロな見方でバラバラな方向を向いている状態の下で、社内文化の形骸化を防ぐマネジメントはとても難易度が高いと思います。

現に大企業やメガベンチャーに就職した先輩方に企業の文化やコアバリューについてお話を聞くと、「言葉は覚えているが特に深い意味も考えていない人」、「なんとなく小馬鹿にしている人」や「そもそも存在自体を知らない人」がほとんどでした。

認識共有の不足からくる形骸化

前述の通り、組織がかなり大きくなってくると話は変わりますが、今後の組織の在り方を強く定義する存在である経営陣・初期のメンバーについては、この企業文化は高いレベルで正確に共有されていなければなりません

このメンバーは今後、この文化やコアバリューを他のメンバーに伝えていくハブの役割を果たして行くことになるからです。

しかしメンバーが少ない中でも、ただでさえめまぐるしく状況が変化する初期のスタートアップの中では綿密なコミュニケーションの努力を怠っていると文化やコアバリューへの認識は容易に分岐していきます(体験談です)。

それぞれが大きな責任を持ち、かなりの量と質の意思決定を繰り返していると、大きなパラダイムシフトが頻繁に発生します。怖いのはそれが無意識化で進行するということです。

なので、社内の文化やコアバリューを擦り合わせて再定義する作業は、継続的にしていかないといけないのだと思います。

スタートアップ初期の社内文化の在り方

前述の通り、文化やコアバリューの形骸化は時間や規模に伴って大なれ小なれ必ず進行してしまいます。

しかし、堅牢な組織を保ち続けるために、これに抗う最大限の努力は継続的に行うべきだと考えています。

特に初期スタートアップに関しては以下の2つを心がけることが有効であると感じました。

最小人数で定義して共有には全体で取り組む

文化やコアバリューを定義する初期の段階で決めるための話です。

f:id:k0mach1:20180420111836p:plain

Fablic CEOの堀井さん が会社の納会にお話をしに来てくださった時に、

社内文化やコアバリューは社長の独断で定義していい、そしてそれをより多くのメンバーに(初期メンバーは特に)より深く浸透させることに対してのリソースは惜しまず割くべき(コアバリュー合宿など)

とおっしゃっていました(堀井さんはこの記事でも企業文化やコアバリューの大切さについて触れています)。

正直、この社内文化やコアバリューに関しては正解が存在しないと考えています。そんな抽象的な議論に多くの人数が関わると船頭多くして船山に上ってしまい、徒らに時間が過ぎていってしまいます。

それに加えて、お互いの主張が交錯して、最終的な成果物がそれぞれを折衷した何ともいえないものになってしまうというケースも往々にしてあります。

なので、定義は最小人数(できれば社長が1人で)して、他のメンバーはそれを理解するための施作・努力を継続的に行っていくというスタンスの方が効率よく軸を安定させられると思います。

(たまにこれらの定義を社外の人々に任せるケースを見ますが、とてもうーんという気持ちになります。)

共感レベルでの採用

(社内の文化やコアバリューの形骸化に)抗う最大限の努力は継続的に行うべき

と言及しましたが、やらなければいけないことが無限にあるスタートアップの中で、これにコストをかけすぎるのはとても痛手です。

なのでここの共有にコストがかかる人間は(初期は特に)採用の段階で弾くべきだと考えています。

事業の表面的なスピード感を重視するとスキルセットや経歴を最重要視しがちですが、僕は長期的な視点で見た時にこちらの方が圧倒的に重要だと思います。

これは僕が勝手に脳内でイメージしているオレオレ共感度グラフ的なものです。

f:id:k0mach1:20180421151010p:plain

社長の脳みそを完全再現しているレベルでの共感は実質無理なので、初期のメンバーはできる限りピンクレベルであるべきであり、最低でも黄色レベルでないといけないと思います。

また組織が巨大化していった時に、全員の(特に責任を持っている人々の)共感レベルがどれくらいのレベルで保てるかというのが、堅牢な組織作りの大きな指標になると考えています。

社内文化に対する現在の僕なりの結論のまとめ

・文化やコアバリューの形骸化は時間や規模に伴って大なれ小なれ必ず進行する

・しかし、これは組織の脆弱化に繋がるため、抗うための最大限の努力は継続的に行うべき

・社内の文化やコアバリューを擦り合わせて再定義する作業は継続的にしていかないといけない

・初期スタートアップでは『最小人数での定義』と『共感レベルでの採用』が有効である

終わりに

感想

最後まで読んでいただきありがとうございました。正直この3ヶ月間においては日本トップクラスで労力を組織への考察に割いたと思います。

かなりストレスフルでしたが、その過程で働くということへのマインドもかなり研ぎ澄まされたのでかなりいい経験になったと思います。

こんなぺーぺーの悩みを真剣に聞き、相談に乗ってくださった業界の先輩の方々には感謝の気持ちでいっぱいです。

特に納会に来てくださった堀井さんの他に、組織についてたくさんのFBをくださった DeNAの千條さん、eurakaのkaneshinさん、Mercariのosamingoさんには感謝してもしきれません。

今後もエンジニアとしての力量を磨きつつ、考察を続けていこうと思います。

イルミネーションもいいけど僕の『正弦関数 in Swift』で作ったハート群もめっちゃ綺麗だから見て見て

この記事は Hakusan mafia Advent Calendar 2017 の24日目の記事です。どの辺がマフィアなのかはよくわかってません。。。

はじめに

クリスマスイブですね(記事出した時はそうなんです)。街には笑顔が溢れて、活気に満ちていることと思います。カップルで幸せな時間を共にする人、家族で温かい食卓を囲む人、孤独を噛みしめる人など、色々な過ごし方の人がいると思います。

中には クリスマスなのにSwiftでハートがセクシーに動くAnimationが作れなくて困ってる人 もいるでしょう。今回はそんな人たちのための記事です。

Swiftでは基本的なAnimationは数行で実装できるようになっている(本当に素敵)のですが、今回は標準では積まれていないゆるやかな往復運動正弦関数を用いて実装したいと思います。

https://gyazo.com/fe84349e823b9a2ab50167daac931885

これから触れるAnimationは実際に僕の会社のサービス PinQul でも使われているものの一部で結構我ながら気に入ってます。

今回はこのハートの動きの部分が純Swiftでどうやって動かされているのかを見ていきたいと思います。

方針

このハートのAnimationは3つの小さなAnimationに分解できます。

  1. だんだんopacityが小さくなるAnimation
  2. y方向に一定速度で上昇していくAnimation
  3. x方向に緩やかに振動するAnimation

です。1と2に関してはCABasicAnimationというswift組み込みのAnimationで簡単に実装できるので、今回は3をメインに触れていきたいと思いますが、一応Animationの基礎についても触れていきます。

CAAnimationの基礎

僕の記事なんて読まなくても、素晴らしい記事がQiitaにたくさん落ちています。

qiita.com

その中でも今回必要なものだけを抜粋しました。

CABasicAnimation

SwiftでUIViewの属性の1つをAnimationで変化させたいと思った時、大概の場合は

  • 何を変化させるか(keyPath
  • 初期値(fromValue
  • 終了値(toValue
  • 期間(duration

を指定するだけで簡単に実装できます。これを可能にしているのが CABasicAnimation です。

例)

//
// 透明からだんだんViewが現れるAnimation
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300
verticalAnimation.duration = 3.0
heartView.layer.add(verticalAnimation, forKey: nil)

https://gyazo.com/8200a6b47e063a18dd059bb725e07d97

※CABasicAnimationを用いてAnimationで変化させられる要素(keyPath)は以下を参照してください。

developer.apple.com

これでも十分綺麗ですが、まだちょっと物足りないですね。

CAAnimationGroup

実はCAAnimationはいくつでも組み合わせることができます。CAAnimationGroupというクラスを使っていきます。

CAAnimationGroupのインスタンスがもつ animations プロパティにCAAnimationの配列を渡すことで、それらのAnimationが1つに集められた1つのAnimationを作成することができます。

例)

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let animationGroup = CAAnimationGroup()
animationGroup.duration = 3.0
animationGroup.animations = [opacityAnimation, verticalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/a4dbe9ca175efcc04483ddd394397b67

透明度を追加したことで儚げな感じがプラスされましたね。人の心は移ろいやすいものです

もちろん配列に追加していけば3つでも4つでもAnimationを組み合わせることができます。

本題の正弦関数Animationについて

Swiftでの正弦関数

Swiftの標準ライブラリである Foundation は豊富に数学関数を含んでいます。

三角関数関連だけでも以下のラインナップです。完璧に揃ってます。

let x = CGFloat(1.0) //単位はラジアン

// 通常のサイン・コサイン・タンジェント
sin(x)
cos(x)
tan(x)

// アークサイン・コサイン・タンジェント
asin(x)
acos(x)
atan(x)

// ハイパボリックサイン・コサイン・タンジェント
sinh(x)
cosh(x)
tanh(x)

// アークハイパボリックサイン・コサイン・タンジェント
asinh(x)
acosh(x)
atanh(x)

今回はその中でも正弦関数(sin)をつかって滑らかなAnimationを実装していきます。

CAKeyframeAnimation

CAKeyframeAnimation は経過時間の配列とそれに対応する値の配列を与えることで、その途中経過をよしなに補完したAnimationを作成してくれます。

例) 公式Documentのサンプル

let colorAnimation = CAKeyframeAnimation(keyPath: "backgroundColor")
colorAnimation.values = [UIColor.red.cgColor, UIColor.green.cgColor, UIColor.blue.cgColor]
colorAnimation.keyTimes = [0, 0.5, 1]
colorAnimation.duration = 2

値や経過時間の差分に変化をもたせることで、CABasicAnimationより自由度の高いAnimationを作成することができます。

今回は正弦関数を使って position.x の値を変則的に動かすことでゆるやかなカーブを描かせていきます。

ゆるやかな往復運動の実装

あらかじめAnimationの duration(時間の長さ) と frameCount(コマ数) を指定しておきます。

そして horizontalAnimation という名前で宣言しておきます。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

keyTimes(値を指定する先の経過時間)を設定する

今回 keyTimes に関しては均等に与えたいので duration をframeCountで均等に割った経過時間を指定します。

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

values(keyTimesに対応する値の組み合わせ)を設定する

ここで正弦関数を用いて動きに遊びをもたせます

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

// let duration = 3.0
// let frameCount = 30
// let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
// horizontalAnimation.duration = duration

// horizontalAnimation.keyTimes = (0...frameCount).map({
//    let dividedTime = Double($0) * duration / Double(frameCount)
//    return NSNumber(value: Double(dividedTime))
// })

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

完成形

//
// heartViewは今回Animationを付けたいUIViewのインスタンス
//

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.duration = duration

horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

heartView.layer.add(horizontalAnimation, forKey: nil)

https://gyazo.com/d3f70babd6d6e27fad5bf7049a34b47b

いい感じでゆらゆらしてます!

完成!

今までの2つの Animation と組み合わせて再生してみましょう。

let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
        
let verticalAnimation = CABasicAnimation(keyPath: "position.y")
verticalAnimation.fromValue = heartView.center.y
verticalAnimation.toValue = heartView.center.y - 300

let duration = 3.0
let frameCount = 30
let horizontalAnimation = CAKeyframeAnimation(keyPath: "position.x")
horizontalAnimation.keyTimes = (0...frameCount).map({
    let dividedTime = Double($0) * duration / Double(frameCount)
    return NSNumber(value: Double(dividedTime))
})

horizontalAnimation.values = (0...frameCount).map({
    let x = sin(CGFloat($0)) * 25 // 振幅: 25
    return heartView.center.x + CGFloat(x)
})

let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = [opacityAnimation, verticalAnimation, horizontalAnimation]
heartView.layer.add(animationGroup, forKey: nil)

https://gyazo.com/07a1abac976314c59d1092b4ab451003

素敵なAnimationが完成しました!!

まとめ

記事書いてて思ったけど "hyperbolic" ってカタカナで ハイパボリック って書いた瞬間急にダサいよね

Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけた話(iOS編)

これは iOS Advent Calendar 2017 の17日目の記事です。

はじめに

はじめまして、Flatt という会社でエンジニアをしている machio(まちお) と申します。iOSはまだ書き始めて半年もたたないひよっこですが、これくらいのプレッシャーでもないと勉強しないだろうということで今回エントリーしました。

現在 PinQul というライブショッピングのアプリを運営しています。作り始めた時はiOS初学だった僕ですが、最高のメンターさんと今をときめくFirebaseの力のおかげでこのアプリを2ヶ月で仕上げることができました。

タイトルの通りPinQulでは "Firebase Authentication の idToken をサーバーの認証に使い自サービスのUserと紐づけて使用" しています。

このケースに関する記事は僕の知る限りこの世にあまり存在しないので、Firebaseへの恩返しの意味もこめて、この認証のiOS側の実装の方法とそこで苦労したことについて書こうと思います(Firebaseのcalendarでやれ)。

Firebase is 何?

http://www.tristatetechnology.com/blog/wp-content/uploads/2016/09/firebase11.jpg

Firebase は俗にいう mBaaS(Mobile Backend as a Service)の1つです。2014年にGoogleに買収されて一躍有名になりました。詳細は以下に素晴らしいQiitaの記事を添付して割愛させていただきます。

qiita.com

正直 使い勝手が良すぎる & なんでもできてしまう のでもう本格的にサーバーレスで生きていけるなと思ってしまうほど素敵なサービスです(唯一欠点を挙げるとすれば日本語の公式リファレンスが古いのでサンプルコードがたまにアレです)。

Firebase Authenticationとは

公式のリファレンス によると

Firebase Authentication には、バックエンド サービス、使いやすい SDK、アプリでのユーザー認証に使用できる UI ライブラリが用意されています。Firebase Authentication では、パスワード、電話番号、一般的なフェデレーション ID プロバイダ(Google、Facebook、Twitter)などを使用した認証を行うことができます。

です。いろんなログイン方法が簡単に実装できるだけではなく、それらでログインしたUserをFirebaseのsdkを通すだけで一元に管理できるのでとても素敵です。

f:id:k0mach1:20171217015956p:plain

そしてその方法で認証したUserには idToken なるものが発行されます。これは他のFirebaseのサービスへのアクセスに使用するというのがスタンダードな使い方だと思うのですが、公式のsdkを使ってあれこれするだけで自サービスのサーバーサイドの認証にも使えます

実際の実装

手順としては

  1. Firebaseを通してログイン完了
  2. クライアントでidTokenを取得
  3. それに該当するJWT(JSON Web Token)をリクエストにのせて送信
  4. サーバー側でそれを検証することで認証

という感じです。1の実装に関しては 公式リファレンスに詳しく書いてある & すでに世にたくさん記事があるので割愛したいと思います。

1さえ超えてしまえばclientでやらなければいけないことはとても単純です。idTokenはとても簡単に取得することができます。

import Firebase

Auth.auth().currentUser?.getIDToken { idToken, error in
    if let error = error {
        // エラーハンドリング
        return
    }

    if let idToken = idToken {
        // idTokenが使える
    }
}

そしてリクエストのheaderにtokenを配置します。PinQulではHTTPクライアントに @ishkawa さんの APIKit を使わせていただいているので、以下のような感じになります。

import APIKit

struct HogeRequest: APIKit.Request {

    var idToken: Stringvar headerFields: [String: String] {
        return ["Authorization": "Bearer \(idToken)"]
    }

    …
}

Auth.auth().currentUser?.getIDToken に渡すコールバックの中でhogehogeしたり、RxつかったりでRequestにidTokenをつけることができれば、あとは送るだけです。とんでもなく簡単です。

あとはこのidTokenを検証した後decodeしてuuidなどの情報を得ます。

単に実装するだけならばこれだけなのですが、PinQulでは一点めんどくさいところがあったので最後にそれに触れて終わりたいと思います。

ログインが必要な機能とそうでない機能がある。。。

これは少しめんどくさかったです。世の中のライブストリーミングのサービスのほとんどがライブを見るだけであればログインをスキップできるようになっています。PinQulも御多分に洩れずそういった実装にしているので認証(idToken)が必要な場合とそうでない場合があります。

なのでリクエストを飛ばす時にそのリクエストに認証が必要かを判断する必要があります。なのでその処理に特化したprotocolを用意して、各Requestにそれを継承させ、認証がいるかいらないかの判断をジェネリクスを使って行わせることにしました。

import APIKit

protocol PinQulRequestProtocol: APIKit.Request {

    …

    var needLogin: Bool { get set }
    var idToken: String? { get set }

    …

}

struct HogeRequest: PinQulRequestProtocol {

    var needLogin
    var idToken: String?
    …

    var headerFields: [String: String] {
        if needLogin,
            let idToken = idToken {
                return ["Authorization": "Bearer \(idToken)"]
        }
    }

    …
}

実際に認証が必要かを判断してリクエストを送る処理は APIKit の Session にextensionを貼って実装しています。

import APIKit

extension Session {
    class func sendPinQulRequest<T: PinQulRequestProtocol>(pinQulRequest: T, handler: @escaping (Result<T.Response, SessionTaskError>) -> Void) {
        if pinQulRequest.needLogin {
            Auth.auth().currentUser?.getIDToken { idToken, error in
                if let error = error {
                    // エラーハンドリング
                    return
                }

                var pinQulRequest = pinQulRequest
                pinQulRequest.idToken = idToken
                self.send(pinQulRequest, handler: handler)
            }
        } else {
            self.send(pinQulRequest, handler: handler)
        }
        
    }
}

これでiOS側の実装は完璧です!

あとはサーバー側でこれを検証して、正常に認証できたらdecodeしてデータを取り出しましょう。 公式のリファレンスに詳細に書いてあるので問題ないと思います。

ID トークンを確認する  |  Firebase

おわりに

Xcodeも今ではとても使いやすくなっていますし、Firebaseなんていうとても素敵なサービスが現れてアプリの開発のハードルがとても下がり、僕のような初学者でもゴリゴリアプリが作れるようになりました。

この記事で利益を被るのは本当にごくわずかな人だと思いますが、最後まで拙い文章をよんでいただきありがとうございます。まだまだ本当に力不足なのでマサカリ大歓迎です。バンバンお願いします!

PinQulは以下のURLからインストールできます。ぜひ手にとって使って見てください!

「PinQul(ピンクル)」をApp Storeで

多人数でのiOS開発におけるStoryboardとの向き合い方(1画面1Storyboard)

※僕が試行錯誤したストーリーが前半を占めてるので興味がなければ読み飛ばしてください

Storyboardと付き合う

StoryboardやAutoLayoutの出現によって、iOSアプリケーション開発に置けるViewの実装のハードルは確実に低くなりました。弊社のプロダクトでも惜しみなくStoryboardを使用しています。

しかし多くの方がStoryboardとの向き合い方について頭を悩まされてきたと思います。

例として以下のCookPadさんの記事では

1つのStoryboardにViewControllerを複数配置して、Segueで画面遷移を実装(しかしViewは再利用したいのでxibファイルの形式で切り分ける)

という手法が取られています。

techlife.cookpad.com

一瞬でこれだ!と思い、採用させていただきました。似たようなUIを何度も再利用していくアプリとの親和性は相当高い思います。

この設計で一生開発していくんだと思っていました。最初は、、、

Storyboardの闇に触れる

正直1人で開発を進めていくのであれば、1Storyboardに全て詰め込んでしまうのが一番シンプルで楽かなと思います。

しかし複数人開発になった瞬間にそういうわけにはいきません。 恐るべきことに異なるbranchで同じStoryboardを触るとほぼ確実にconflictが起きます

しかもこのconflictが厄介で、conflictが発生した瞬間そのファイルはxcodeで開くことができなくなり、コードを見て愚直に1つ1つ問題を解消するしかなくなります。それも実力不足の僕の場合高確率で失敗するので、泣く泣く片方の変更を捨てることもしばしばです。

PRをだすたびに赤く染まるGithubの画面。「いっそのことStoryboardなんて使わなければよかった」、そんな考えが頭をよぎりました。

1画面1Storyboardという選択

そんなある日、某グルメサービスのiOSエンジニアさんとお話する機会があり、この悩みをぶつけたところ、「弊社は1画面につき1Storyboardでやってますね」という答えが帰って来ました。

確かに画面ごとにファイルを分けて画面ごとにタスクを分担すれば、まずconflictは起こりません。最初は「何を言ってるんだ、Segueを殺して何が楽しい」と憤ってた僕ですが、これが最高でした。

以下で詳しく説明します。

具体的な方法

別のStoryboardのViewControllerに移動するのは実は大して難しくありません。

let storyboard = UIStoryboard(name: "Hoge", bundle: nil) // storyboardのインスタンスを名前指定で取得
let nextVC = storyboard.instantiateInitialViewController() as! UIViewController // storyboard内で"is initial"に指定されているViewControllerを取得
self.present(ViewControllerVC, animated: true, completion: nil) // presentする

みたいに3行でいけます。基本的には

  • 1ViewControllerにつき1Storyboardを用意してあげる
  • 各Storyboardに適切な名前をつける
  • 上記のコードで画面遷移!

これで大丈夫でしょう。ですがStoryboard経由でViewControllerを取得してる感が鬱陶しいので、弊社ではもう少し精錬して使用しています。

以下の3STEPです。

1. 以下のようなprotocolを定義します。

protocol StoryboardInstantiable {
    static var storyboardName: String { get }
    static var bundle: Bundle? { get }
}

extension StoryboardInstantiable where Self: UIViewController {
    static var bundle: Bundle? {
        return nil
    }
    static func instantiate() -> Self {
        let storyboard = UIStoryboard(name: storyboardName, bundle: bundle)
        return storyboard.instantiateInitialViewController() as! Self
    }
}

2. これを各ViewControllerに継承させる

storyboardNameをそれぞれで定義しましょう。

class HogeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 諸々の処理
}

extension HogeViewContoller: StoryboardInstantiable {
    static var storyboardName: String {
        return "Hoge"
    }
}

3. 遷移のコードは2行に

let nextVC = HogeViewController.instantiate() // これだけでStoryboardに紐づいたHogeViewControllerを取得
self.present(ViewControllerVC, animated: true, completion: nil) // presentする

かなりシュッとしたと思います!ViewControllerベースで次の画面取得してる感が好きです。

終わりに

このやり方はあくまで一例であって、他にも色々な設計があり、それぞれが長所・短所を持っていると思います。その開発の環境に応じた最適なものを採用するのが一番ですが、多人数開発をするとき、この手法をチラッと思い出していただけると本望です。