Zeplin + Prismを用いて、開発で利用する色の情報を簡単に生成・管理する

はじめに

こんにちは、LINE スタンプメーカーでiOSアプリの開発をしているfreddiです。

LINE スタンプメーカーでは、Zeplinというツールを使ってデザイナーからの成果物をチーム間で共有しており、開発時でもデザインを反映させるために利用しています。Zeplinでは、開発者向けのツールの作成も容易にでき、Zeplin上の情報を簡単に開発者の手元に落とし込むことができます。その例の一つとして、Gettという会社がOpen Sourceで公開しているPrismというツールがあります。Prismでは色情報をZeplinから取得して、好きなフォーマットに変換することができます。

私のチームでは、人為ミスやレビューコストを削減するため、Prismで取得した情報を自作Python Scriptに渡し、Color Assetとコードを自動で生成しています。これにより、色情報を様々な構造・形式で利用することができ、ある程度のテストコード自動生成も行うことで安全性を担保しています。今回はPrismの紹介も含め私のチームの色の自動化について共有します。

記事中で利用したコード・ツールの検証バージョンは以下です。

  • Xcode 12.4
  • Python 3.7
  • Prism 0.5.0

デザイン用データの整理によって出た課題

LINE スタンプメーカー では、一度デザイン用データの情報の整理を行いました。その一環として、色情報の統一があります。以前までは、デザインデータでの各画面で利用されている色は、それぞれの画面の中ごとで定義されていました。これでは、RGBが少々違うだけの色が点在している、同じ色でも名前がないのでわかりにくい、色がそれぞれのデザインで統一されていない・それぞれの色の目的が不明瞭になっている、などといった問題がありました。

そこで、デザイン用データの整理を行ったことにより、ある程度色やデザインは統一されるようになりました。具体的には、似ている色はできるだけ統合し、それぞれの色に役割を持った名前をつける、などわかりやすくするためのアクションを行いました。

ミスの多い「写経作業」

これで、デザイン面での色の問題はいくつか解決しました。しかし、いざ開発を始めたとき、新しい色情報に合わせるための課題がいくつか浮上しました。

その中の一つに、「色情報の反映の手間」があります。私達はこれまで、Zeplinにある色の情報をColor AssetとしてXcodeに手作業で登録していました。しかし、この種の手作業は大きなレビューコスト、見落としによるミスの発生、作業の手戻りが発生する、などの問題があります。

当然、この写経作業の時点で多くのミスが発生しました。たとえば、「カラーコードのRが違う」、「存在しない色を登録している」といった凡ミスが多く発生していました。また、開発側でレビューを通してもデザイナーのテスト中に違うことが判明したり、各画面に色を適用する時に写経し忘れがあったりしました。

Prismの登場

さて、冒頭でも触れましたが、Zeplinでは開発者向けAPIを利用してデザインの情報を取得することができます。もしみなさんが既にZeplinのアカウントを持っている場合、こちらから開発に必要な情報を得ることができます。Zeplinが開発者向けに提供しているAPIを通じて、自身のTeamのWorkSpace上にある、様々な情報を取得することができます。

前述の課題に立ち向かうため、開発者側でツールの作成を検討しました。もちろん、既にZeplinのアドオンとしていくつかSwift/UIKit開発向けに便利なものはいくつか公開されていました。しかし、生成するコードのフォーマットを強いるアドオンが多いので、プロジェクトにマッチする様なものがなく、また自由度の高いものはありませんでした。

色々と模索をしていく中、同時期にこのZeplinのAPIを利用した画期的なツールが発表されました。それがPrismです。

Prismについて

Prismは、Gettという会社がOpen Sourceで公開しているSwift製のCLIツールです。PrismからZeplinの情報を同期して、特定のテンプレートに合わせて変換することができます。他の既存のアドオン群とは違い、サポートしているフォーマットの自由度が高く、使い様によっては独自に発展させることができます。また、既存プロジェクトへの導入も、特殊なことをしなければ非常に簡単です。

このエントリではiOSアプリ開発における応用を解説していますが、Prismは基本的にプラットフォーム関係なく、Androidなどでも利用できます。

Prismを利用してみる

PrismはREADMEに基本的な使い方が書いており、どなたでも簡単に使うことができます。このエントリでも、どのようにしてZeplinのプロジェクト上にある色の設定を取得するかについて軽く説明します。

Prismのテンプレートのフォーマット

その前に、Prismのテンプレートのフォーマットについて説明します。 Prismのコード生成のテンプレートは、xxxx.yyy.prism (xxxxはファイル名、yyyは拡張子)と名前をつけます。例えば、Swiftのコードを生成したいなら、xxxx.swift.prism と名付けます。

では、テンプレートのフォーマットを見ていきます。テンプレートのExampleはこちらで公開されています。ここではSwiftのコードを載せます。

// This file was generated using Prism
import UIKit

public extension UIColor {
    {{% FOR color %}}
    static let {{% color.identity.camelcase %}} = UIColor(r: {{% color.r %}},
                                                          g: {{% color.g %}},
                                                          b: {{% color.b %}},
                                                          alpha: {{% color.a %}})
    {{% END color %}}
}

private extension UIColor {
    convenience init(r: Int, g: Int, b: Int, alpha: Float) {
        self.init(red: CGFloat(r) / 255.0,
                  green: CGFloat(g) / 255.0,
                  blue: CGFloat(b) / 255.0,
                  alpha: CGFloat(alpha))
    }
}

FOR color による各色へのアクセス方法や RGBの各パラメータへのアクセス方法等を見て頂きたいのですが、私はこのフォーマットの定義はかなり直感的だと思いました。他の書き方については、JSONのExampleが参考になると思います。

このように、Scriptベースの簡単な書き方で、様々なフォーマットのコードが自動生成させることが可能です。Exampleには、iOSだけではなく、Androidのテンプレートの例も定義されています。

詳しくは後述しますが、予め扱いやすいJSONのテンプレートを用意して生成し、別でスクリプトを用意してさらに複雑なものに変換する、ということも可能です。

1. Prismをインストールする

Prismをbrew経由でインストールします。他にも、Mint経由や、ソースから直接したものからでもインストールできます。

$ brew install GettEngineering/tap/prism  

2. PrismをSetupする

Prismのセットアップを行います。こちらから、Personal access tokensを発行して、ZEPLIN_TOKENという名前の環境変数に設定します。 その後、prism initを例えば利用したいProjectのルートディレクトリなどで叩けば大丈夫です。

$ export ZEPLIN_TOKEN=xxxxx.xxxxxxxxx....
$ prism init

叩いたディレクトリの同じ階層に、.prism/config.yml が生成されます。Tokenを発行したアカウントが複数のProjectに参加している場合、prism initをしたときにどちらのProjectを使うか聞いてきますので、適切に選択してください。

3. 必要なconfigを設定して、テンプレートを用意する

config.yml の中身は以下の様になっています

project_id: "xxxxxxxxxxxxxxxxxx"
templates_path: ".prism"
output_path: "./"

それぞれの値を説明すると、

  • project_id: Zeplin上のどのprojectの情報を見るか。基本的にこちら側で変更しない
  • templates_path: 利用するテンプレートがどこにあるか。先ほど説明した xxxx.yyy.prismがあるディレクトリを指定。通常は.prism以下に入れる
  • output_path: 生成されたコードをどこに配置するか。ExampleApp/Resources などわかりやすいところに入れると良い

4. コードを生成する

最後に、以下のコマンドを.prismがあるディレクトリで叩けば、config.ymlで設定したとおりコードが自動生成されます。

$ prism generate

以下の様に、config.ymlが違う場所にあったり、テンプレートが違う場所にあったりした場合などは、optionで指定できます。

$ prism generate --config-file {ymlのディレクトリ}/config.yml --templates-path {Templateのあるディレクトリ} --output-path {出力先のディレクトリ}

例えば、Xcodeで生成されたコードを利用したい場合は、生成されたコードをXcode Projectに追加してください。(一度追加したら、output_pathを変更しない限りは追加作業をしなくても大丈夫です)

Prism + Python Scriptによる、Color Asset/Swiftコードの生成

さて、Prismを利用することにより、どうにか色情報の反映の自動化は見込めるようになりました。あとは現在利用しているプロジェクトにマッチしたテンプレートを作れば完成です。しかし、LINE スタンプメーカーのプロジェクトに合わせる場合には、いくつか課題がありました。

Prism単体での課題

その一つとして、Prismだけではディレクトリ構造などを伴う、少々複雑な構造を持ったものに変換することが難しい、という問題がありました。

プロジェクトではInterface Builderを使っている部分も多く、そのいったケースではColor Assetの方が都合の良い場面が多くありました。

その例として、XcodeのColor Assetがあります。Color Assetは以下の様な構造になっています。

{色の名前}.colorset // ディレクトリ
   |
   +--- Contents.json // 色の情報があるJSON

このように、「複数のディレクトリに色情報を分けて、その中のJSONで色の情報を定義する」ということが必要になります。しかし、Prismのテンプレートの形式だと、複数のファイル/ディレクトリに別れた構造まで定義するのは現状の機能では難しいと考えました。以下のように、static なUIColorのオブジェクトを一つのファイルですべて定義すれば良い、と考えるかもしれません。

enum ColorList { 
   static let lineWhite = UIColor(r: 255.0, g: 255.0, b: 255.0, a: 1.0)
   static let lineRed = UIColor(r: 255.0, g: 0.0, b: 0.0, a: 1.0)
   ...
}

しかし、このようにコード上で定義されたUIColorをInterface Builder上で色として利用するのは難しいです。 コードで指定する部分はUIColorで良いが、Interface Builder向けにColor Assetを作らなければならない、というのがプロジェクトの現状です。

しかし、PrismのZeplinから色情報を好きなフォーマットに落とし込むことができる機能は強力であり、どうにかしてうまく活用したいと思いました。そこで、思いついたのが、Prismで取得したデータをもとに、Python Scriptを利用してコードを生成する手段です。

PrismとPython Scriptによるコードの生成

以前よりプロジェクトでは外部リソースが関わる様なタスクには、Python ScriptやSwiftGenを使ってコード生成をしていました。たとえば、チームメンバーの一人は、半角全角や特殊文字の判定に使うUnicodeの情報を簡単にコードから使える様に、公式のUnicodeの定義リストからコードを生成するScriptを作りました。他にも、トラッキングイベントのコード・Localization・一部画面などは、自動でコードが生成されるようにScriptが作られています。

今回は、PrismからZeplin上にある色のデザインデータを取得し、その色情報をもとにPython ScriptでColor Asset・Swiftのコード・テストコードを生成できるようにしました。詳しいコードの解説は省き、データ取得やコード生成と言った各フェーズにフォーカスを当ててお話します。

Prismによるデータの取得時のアプローチ

Color Assetの生成Scriptを作るに当たり、最低限Color Assetを作るために必要な情報だけをZeplinから取得します。Color AssetのContent.jsonは以下の様な構成になっています。

{
  "info" : {
    "version" : 1,
    "author" : "xcode"
  },
  "colors" : [
    {
      "idiom" : "universal",
      "color" : {
        "color-space" : "srgb",
        "components" : {
          "red" : "31",
          "alpha" : "1.000",
          "blue" : "31",
          "green" : "31"
        }
      }
    }
  ]
}

つまり、

  • RGB + Alpha
  • ディレクトリにつけるための色の名前

がColor Assetを生成するときに最低限必要になります。また、ダークモード対応を検討している場合、モード別の情報も取るようにします(今回は省きます)。

以上により、Prismで定義するテンプレートは、以下の様になれば良いことがわかりました。

[
    {{% FOR color %}}
    {
        "name": "{{% color.identity %}}",
        "r": {{% color.r %}},
        "g": {{% color.g %}},
        "b": {{% color.b %}},
        "a": {{% color.a %}}
    }{{% IF !color.isLast %}},{{% ENDIF %}}
    {{% END color %}}
]

Color Assetの生成

後は、prism generate で生成されたJSONをPython Scriptに渡して、Color Assetの生成コードを書くだけです。

生成したJSONは、{色の名前}.colorset という名前のディレクトリで囲み、 プロジェクトのAssets.xcassets 以下に配置すれば、Xcode Projectが自動で読み込んでくれます。

実際に利用しているPythonコードを改変したものを掲載しますが、ご覧の通り、 os や json といったPythonで標準であるものだけを利用すれば、以下の様に簡単に生成コードを記述できます。

import json
import os

# 以下のAssetのJSONは、Xcodeが生成するものからコピーしてきたもの
assetDataTemplate = """
{
  "info" : {
    "version" : 1,
    "author" : "xcode"
  },
  "colors" : [
  ]
}
"""

componentTemplate = """
{
  "idiom" : "universal",
  "color" : {
      "color-space" : "srgb",
      "components" : {
      }
  }
}
"""

def parseColorAsAsset(color):
    asset = json.loads(assetDataTemplate)
    component = json.loads(componentTemplate)

    component['color']['components']['red'] = "{}".format(color['r'])
    component['color']['components']['alpha'] = "{}".format(color['a'])
    component['color']['components']['blue'] = "{}".format(color['b'])
    component['color']['components']['green'] = "{}".format(color['g'])

    asset['colors'].append(component)

    return asset

def gen():
    with open('colors.json') as f:
        # Prismが生成したJSONを読み込む
        colors = json.load(f)
        for color in colors:
            colorName = color['name']
            data = parseColorAsAsset(color)

            # ↑で解説したColor Assetの構造通りにディレクトリをつくってJSONを配置する
            os.mkdir('{}.colorset'.format(colorName))
            with open('{}.colorset/Contents.json'.format(colorName), 'w') as jsonFile:
                # Contents.json JSONをダンプ
                json.dump(data, jsonFile, indent=2, ensure_ascii=False, separators=(',', ' : '))

if __name__ == "__main__":
    gen()

強いて言うなら、Contents.jsonとして書き出す際に json.dump にXcodeが生成するものとおなじスタイルにするためのオプションを加えています。

Swiftコードの生成と、テストコード生成による保証

ここで、上記のScriptで自動的に追加されたColor Assetに対して、正確に読み込めるかテストを行うべきだと考えました。UIColorなどから読み込む際に、生成したJSONファイル等の名前の不整合などの問題がありクラッシュしたりすることがありますし、そもそもフォーマットに何かしら問題・変更があると読み込めない問題もあるためです。

ここに関しては、Prismのコード生成だけで十分なので、そのテンプレートを紹介します。ここでは、UIColorから呼び出すコードも生成しています。Color Assetは UIColor(named:) という関数から呼び出して、UIColorとして扱うことができるのでそれを利用しています。

// Color Asset -> UIColor

//
//  Colors.swift
//
import UIKit

enum Colors {
    {{% FOR color %}}
    static let {{% color.identity.raw %}} = UIColor(named: "{{% color.identity.raw %}}")!
    {{% END color %}}
}
// UIColorによるテスト

//
//  ColorsTest.swift
//
@testable import AppName // 適切に変える
import XCTest

class ColorsTest: XCTestCase {
    func testReferenceTest() { // Color Assetから適切に呼び出せるか?問題があるならエラー
        {{% FOR color %}}
        _ = Colors.{{% color.identity.raw %}}
        {{% END color %}}
    }
}

これで、Zeplinからの色情報の取得・Color Assetとテストを含んだコード生成の自動化の流れは完成しました。色情報も直接Zeplinの色定義からとっているので、正確性は写経よりも十分に上がっており、レビューコストもかなり下がりました。また、考えられるある程度の問題も生成されたテストケースでカバーできています。Projectの構成の都合上R.swiftを利用していませんが、R.swiftを利用するとさらに利便性は上がると思います。

LINE スタンプメーカーでもほぼ同じ構成・内容で運用していますので、Zeplinを利用している方は、是非この方法を実践してみてください。

他のPrismの利用例

2020年4月に開催された Bitrise User Group #1 では、開発元のGettに以前所属していたエンジニアのShai Mishali氏がGettアプリでの利用例を紹介しています。CI/CDと交えた話もあるので、完全にデザイン関係を自動化したいと思う方にとっては有益な情報だと思うので是非チェックしてください。


最後に

今回は、Prismを交えたデザイン(色情報)に関する自動化について書きました。本記事はiOSの開発にフォーカスしたものですが、Androidなどの他のプラットフォームだけでなく、フロントの開発など別分野でも応用できると思います。Zeplinの様な開発者向けに選択肢を広げているツールと、デザインの自動化は相性が良いと思うので、ぜひみなさんもデザインの自動化にtryしてみてください。

また、前述の通り、LINE スタンプメーカーでは他にも様々な工夫をして開発をより楽にしています。Unicodeのデータ、トラッキングイベント、Localizeなど様々な場面でコード生成などの自動化が役に立っているので、また別の機会にご紹介したいと考えています。

お知らせ

LINE Fukuokaでは、LINE スタンプメーカーを始めとするProjectの開発エンジニアを募集しています。 興味がある方は是非こちらからご応募ください。