アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

iOS プロジェクトの開発効率を上げる為の準備体操

アドベントカレンダー 9 日目の記事を担当させてもらう namikata です。アクトインディで iOS のアプリ開発を担当してもうすぐ 4 年が経過します。

adventar.org

この記事は React Native や Flutter を使ってマルチプラットフォームで開発効率をあげよう、とか RxSwift/MVVM でがんばろうとか、そうゆう記事ではなく、とても地味な話なんですが、新規でアプリを作成する機会が来たら、また継続してやろう、使おうと思っている基本的な事をまとめたいと思います。

沢山のコードを書いては、沢山のコードを捨ててきましたが、 実際に長らく運営させてもらった中で、プロジェクト初期から実施することで、ものすごくボディーブローのように効いてきている施策です。

  1. Storyboard から編集できる View
  2. 行間
  3. fastlane の TestFlight 配信
  4. チームに簡単に画像や動画を提供できるツールの準備
  5. Xcode のキャッシュ関連ファイルを削除するシェルスクリプト
  6. project.pbxproj のコンフリクトを防ぐ mergepbx の導入

デザインを見て Storyboard を作り上げる。何度やったか覚えていません。最近はデザインを見れば、各 UIKit のパーツに頭の中で置換できるぐらいまで iOS 脳になる事ができました。デザインから Storyboard を作る過程で、大きく作業効率を上げた事前準備が 2 つあります。そのうちの 1 つが、とても基礎的な事ですが、アプリ内で利用するカラーを

  • コード(動的に変更する用)
  • カラーパレット(StoryBoard から指定する用)

のそれぞれで、簡単に設定できるようにする事です。

f:id:t-namikata:20191204105555p:plain

enum ColorType {
    case blue
    case deepGray
    case lightGray
    case middleGray
    case orange
    case red
    var color: UIColor {
        switch self {
        case .blue: return UIColor(hex: "FFFFFF")
        case .deepGray: return UIColor(hex: "FFFFFF")
        case .lightGray: return UIColor(hex: "FFFFFF"))
        case .middleGray: return UIColor(hex: "FFFFFF")
        case .orange: return UIColor(hex: "FFFFFF")
        case .red: return UIColor(hex: "FFFFFF")
        }
    }
}

extension UIColor {
    convenience init(hex: String, alpha: CGFloat) {
        let v = Int("000000" + hex, radix: 16) ?? 0
        let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255
        let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255
        let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255
        self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1))
    }

    convenience init(hex: String) {
        self.init(hex: hex, alpha: 1.0)
    }
}

これは一種の、デザイナーとプログラマー間でのユビキタス言語の色バージョンです。

アプリで利用するカラーを定義しておけば、あがってきたデザインを見た時に、赤、青、緑、といった認識をするだけで、コードや storyboard に色を反映する事ができます。

また、見たことない色がデザインに上がってきた時には、

  • その色は新しくテーマカラーとして採用すべき色なのか(今後も繰り返し利用する色か)
  • 試験的に試してみたい色なのか(一時的に利用する色か)

というような、アプリの運用といった側面からデザインについて議論を行う事ができるようになります。自分の場合は、色々知りたがりなので、なぜこの色を採用したのか、今まで使っていた色が新しい色に変更になったのはなぜなのか、的な理由を聞くだけでプロジェクトに参加している感が上がります(笑)

カラーパターンの登録方法はこちらの記事に詳しくのっています。アプリで使うカラーパターンをStoryboardでも定義する

合わせて imageMagick を Mac にインストールして、それぞれの色の背景用画像を作っておくのもオススメです。なぜかというと UIButton の状態で背景の色を変えたい場合、Storyboard で指定するには、画像が必要だからです。コードで BackgroudColor を変更する方法もありますが、Storyboard でてきた方が楽なので、背景画像を利用する事も多いです。

convert -size 1x1 xc:"#FFFFFF" white.png と terminal で実行するだけで必要が画像が出来るので重宝してます。

Storyboard から編集できる View

Storyboard を作る過程で、大きく作業効率を上げた事前準備のもう一つが View の IBInspectable の利用です。 IBInspectable を利用する事で cornerRadius や borderColor のような指定がやりやすくなります。決められたパターンのボタンなどは UIButton を継承したカスタムクラスを作っちゃう方が使い回しが効くと思いますが、角丸だけの場合など、カスタムクラスを作らず、ちゃっちゃと行いたい場合は多くありました。

いこーよアプリの場合は SwifterSwift というライブラリを利用していますが、ライブラリに依存したくない場合は UIView の Extension を自作してもいいと思います。

extension UIView {

    /// SwifterSwift: Border color of view; also inspectable from Storyboard.
    @IBInspectable var borderColor: UIColor? {
        get {
            guard let color = layer.borderColor else { return nil }
            return UIColor(cgColor: color)
        }
        set {
            guard let color = newValue else {
                layer.borderColor = nil
                return
            }
            // Fix React-Native conflict issue
            guard String(describing: type(of: color)) != "__NSCFType" else { return }
            layer.borderColor = color.cgColor
        }
    }

    ...

}

f:id:t-namikata:20191204105814p:plain

行間

「行間を調整してほしい」

デザイン調整でごく当たり前の注文ですが、iOS の行間の調整のやり方はすごく嫌いです。attributedText を利用しますが、動的に text を変更する場合 label.text = 'わっしょい' するのか、 label.attributedText = 'わっしょい' するのか考えないといけないのは、考える事が多くなり、すごい嫌です。その為、特に対応する機会の多い行間調整は UILabel に行間を調整するメソッドを追加し

  1. label.text = 'わっしょい' と常に text に値を代入するようにする
  2. 行間の調整が必要な label には label.autoLineSpacing() を呼び出す

という風な実装をしています。 autoLineSpacing() の具体的な実装は以下です。

extension UILabel {
    func autoLineSpacing() {
        guard let text = text else {
            return
        }

        let lineHeightMultiple: CGFloat = 1.2
        let paragraph = NSMutableParagraphStyle()
        paragraph.alignment = textAlignment
        paragraph.lineHeightMultiple = lineHeightMultiple
        let attributes = [NSAttributedString.Key.paragraphStyle: paragraph]
        attributedText = NSAttributedString(string: text, attributes: attributes)
    }
}

まずは、プロジェクトでの行間のルールを決めましょう。フォントサイズに関係なく 一律 1.2 空けるでもいいですし、フォントサイズに合わせて enum で lineHeightMultiple を指定してあげてもいいと思います。行間の定義を予め決めて置くことで、デザインをプロジェクトに反映する際の効率が一段アップすると思います。

fastlane の TestFlight 配信

プロジェクトスタート時に CI 環境を必ず用意しようという話ではなく、まずはローカルマシンの Mac からでもいいので、 fastlane のコマンドで TestFlight と Debug ビルドを確認できる環境 ( Deploygate や Firebase ) へ簡単にアップロードできる環境を整えるべきです。

Xcode の機能でやると

  • 「あれ? Archive したいのだけどグレーアウトしている何でだろう?」
  • 「ああ、ビルドにシミュレーターが選択されてるからだ。Generic Device iOS に変えなきゃ。」
  • 「よし、Archive 実行できたぞ。次のステップに進むまでに時間かかるから Slack 確認したり、他の作業しとこう。」
  • カタカタカタ(他の作業に集中...)
  • なんと Organizer が おきあがり なかまに なりたそうに こちらをみている!
  • カタカタカタ(他の作業に集中...)
  • カタカタカタ(他の作業に集中...)
  • 「あ、TestFlight にアップ作業中だった事を忘れてた!しまった!」
  • 「Organizer すまない!アップロード実行だ!アップロードも時間かかるから、他の作業しとこう。」
  • カタカタカタ(他の作業に集中...)
  • Bundle バージョン が更新されていないので、アップロードを受付られません。
  • カタカタカタ(他の作業に集中...)
  • カタカタカタ(他の作業に集中...)
  • 「あ、TestFlight にアップ作業中だった事を忘れてた!しまった!」
  • 「なに?アップロード失敗?ファッツ?ファーッツ!? Bundle バージョンのアップし忘れか。一番先に教えてよ。なんで最後なの?もっかい Archive からやり直しだ、最悪だ。。。」
  • カタカタカタ(他の作業に集中...)
  • なんと Organizer が おきあがり なかまに なりたそうに こちらをみている!
  • カタカタカタ(他の作業に集中...)
  • カタカタカタ(他の作業に集中...)
  • 「あ、TestFlight にアップ作業中だった事を忘れてた!しまった!」
  • 「Organizer すまない!アップロード実行だ!アップロードも時間かかるから、他の作業しとこう。」
  • カタカタカタ(他の作業に集中...)

なんて事になります。絶対になります。

アップロードが完了しても、この後に待っているのが、輸出コンプライアンスの暗号化機能に関する設定。これはアップロードが完了してから数十分経過しないと設定できません。いくつもの山場を乗り越え、やっと TestFlight へのアップロードが完了する訳です。これは良くありません。TestFlight へのアップロード作業をやりたくない、と自然に体が学習していきます。この精神的苛立ちは、開発効率の低下にボディーブローのように効いてきます。

各種デプロイ作業は、簡単に行えるように設定をしていきましょう。

Debug 環境へのデプロイ(例: Deploygate)

fastlane は deploygate へのアップロードをサポートしているので、とても簡単です。

  lane :dg do
    gym(
      scheme: schema,
      configuration: 'Debug',
      export_method: "development"
    )
    branch_name = sh("git symbolic-ref --short HEAD")
    deploygate(
      api_token: api_token,
      user: user,
      message: "Build #{branch_name}",
    )
  end

TestFlight へのデプロイ

まず Bundle バージョンを自動で更新する設定を入れましょう。こちらの記事に良くまとめられています。 fastlane で build/version number をインクリメントする

そして、次に輸出コンプライアンスの確認をスキップする設定です。これは fastlane の設定ファイルではなく、Xcode での設定になります。 アプリサブミット時の輸出コンプライアンスの確認をスキップする

fastlane 用の Fastfile は以下のようになりました。

  lane :beta do
    increment_build_number
    gym(scheme: schema)
    pilot(skip_waiting_for_build_processing: true)
  end

あとは、デプロイしたいタイミングで fastlane dg や fastlane beta を terminal から実行するだけです。

参考までに、いこーよアプリの場合、ローカルマシン(CPU 2.4G, メモリ 32GB)では deploygate への反映は 2 分、 fastlane への反映はおよそ 10 分で完了します。

f:id:t-namikata:20191204105938p:plain f:id:t-namikata:20191204105948p:plain

CI ツールを用意するかどうか、プロジェクトの規模による所だと思うので、導入によりプロジェクトがよりスムーズに回りそうであれば、その時に導入すれば良いです。多くの CI が fastlane のプロセスをサポートしているので CI での実行は楽に行えると思います。

チームに簡単に画像や動画を提供できるツールの準備

試しに組んでみたデザインのフィードバックを受けたい、大雑把な動きに問題がないか確認したい、などなど、実際に手を動かして開発をしていると、チームに確認したい事がもりもり出てきます。この時に大事なのは、キャッチボールの速さです。返答をもらえないと先に進めないみたいなケースも時にはあるので、キャッチボールを早く行える方法を準備しておくのはスムーズな開発に欠かせません。会話のキャッチボールで一番最速なのは Face to Face で画面見ながら話合う事ですが、直接話しを出来ない状況もあったりして、次点として Slack のようなチャットツールが上がると思います。物事を相談する時には、実際に画像で見てもらったり、動作を撮影した動画を見てもらったりするのが理解が早いので、Slack で、画像と動画を共有する際の便利な方法を紹介したいと思います。

簡単にキャプチャを取って URL を発行できる Gyazo

Mac でシミュレーター起動して、気になる所を Gyazo で撮影し Slack に URL を投稿する。とても楽で重宝してます。(※ 個人情報が絡むような機密情報には利用しないでね )

簡単に動画Gifを作成できる LICECap

シミュレーターの動きを LICECap で録画して Slack に共有するのに使っています。動画だと容量が大きくてアップロードの時間がかかるので、動画Gif にして容量抑えて、早く共有できるようにしています。ただ、画質は悪いです。実機で不具合を発見して、パッと再現手順を撮影して送りたい時も、 Mac と iPhone をつないで QuickTime で 新規ムービー撮影 を選択することで iPhone の画面を簡単にミラーリングできるので、それを QuickTime で録画せずに LICECap 重ねて録画して Slack に報告するといった事でも良く利用しています。

Xcode のキャッシュ関連ファイルを削除するシェルスクリプト

Xcode で作業していると、良くわからない理由でビルドが上手くいかなかったり、 Refactor -> Rename でメソッド名を変更する時に、候補を出す際にエラーになったりすることがよくあります。 Clean しても解決しない事が多く、 Xcode のキャッシュファイルを削除するまでエラーが出続けるなんて時もあるので、削除する手順を簡略化しておくことは大事です。

ls -l ~/Library/Developer/Xcode/iOS\ DeviceSupport/
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/
ls -l ~/Library/Developer/Xcode/DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData
ls -l ~/Library/Developer/Xcode/Archives
rm -rf ~/Library/Developer/Xcode/Archives

大抵は DerivedData を削除すれば OK だと思いますが、ローカルでデプロイを頻繁に行う場合は Archive したデータが蓄積していって HDD を専有していくようになるので、一緒に削除しちゃってます。

project.pbxproj のコンフリクトを防ぐ mergepbx の導入

project.pbxproj がコンフリクトするのを防ぐツールです。

https://github.com/simonwagner/mergepbx

ツールの導入はMacにセッティングする形でプロジェクトを問いません。プロジェクトの途中からでも、いつでもツールの導入は可能です。ツール導入前に merge で conflict が発生した場合は merge を取り消し mergepbx を導入した上で再マージを試してみてください。

インストールは以下に詳しくまとめられています。

pbxprojファイルのマージが便利になるmergepbxをインストールするスクリプト書いた

まとめ

目新しいものは一つもありませんが、どれも長期に渡る運用でとても助けられたものばかりです。実践している人も多いと思いますが、もし参考になれば。

いこーよアプリチームでは、アプリの成長を加速させる為に、アプリエンジニアを積極募集しています!是非お気軽にご連絡ください^^

actindi.net